From f5e4a94bebdf86812ab6206a7782056d8254c750 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 1 Aug 2019 15:38:03 -0500 Subject: [PATCH] first --- .env.example | 32 + .gitattributes | 5 + .gitignore | 10 + Makefile | 14 + app/Alert.php | 52 + app/AppServer.php | 72 + app/AppServerRecordCreator.php | 36 + app/Balancer.php | 144 + app/CaddyBalancerConfiguration.php | 100 + app/CaddyServerConfiguration.php | 102 + app/Callbacks/CheckActivation.php | 42 + app/Callbacks/CheckBuild.php | 42 + app/Callbacks/CheckDatabaseBackup.php | 44 + app/Callbacks/CheckDatabaseRestore.php | 42 + app/Callbacks/CheckServerTask.php | 44 + app/Callbacks/Dispatch.php | 41 + app/Callbacks/MarkAsProvisioned.php | 21 + app/Callbacks/StartBackgroundServices.php | 57 + app/Certificate.php | 45 + app/Collaborator.php | 17 + app/Console/Kernel.php | 43 + app/Contracts/Alertable.php | 13 + app/Contracts/DnsProvider.php | 41 + app/Contracts/HasStack.php | 13 + app/Contracts/Provisionable.php | 157 + app/Contracts/ServerProviderClient.php | 85 + app/Contracts/SourceProviderClient.php | 104 + app/Contracts/StackDefinition.php | 37 + app/Contracts/StorageProviderClient.php | 87 + app/Contracts/YamlParser.php | 14 + app/DaemonGeneration.php | 16 + app/Database.php | 247 + app/DatabaseBackup.php | 204 + app/DatabaseRestore.php | 86 + app/Deployment.php | 417 ++ app/DeploymentInstructions.php | 169 + app/DeterminesAge.php | 20 + app/Environment.php | 134 + app/Events/AlertCreated.php | 42 + app/Events/DatabaseBackupFailed.php | 31 + app/Events/DatabaseBackupFinished.php | 31 + app/Events/DatabaseBackupRunning.php | 31 + app/Events/DatabaseRestoreFailed.php | 31 + app/Events/DatabaseRestoreFinished.php | 31 + app/Events/DatabaseRestoreRunning.php | 31 + app/Events/DeploymentActivating.php | 43 + app/Events/DeploymentBuilding.php | 43 + app/Events/DeploymentCancelled.php | 61 + app/Events/DeploymentFailed.php | 63 + app/Events/DeploymentFinished.php | 77 + app/Events/DeploymentTimedOut.php | 60 + app/Events/ProjectShared.php | 39 + app/Events/ProjectUnshared.php | 39 + app/Events/ServerDeploymentActivated.php | 31 + app/Events/ServerDeploymentBuilt.php | 31 + app/Events/ServerDeploymentFailed.php | 31 + app/Events/ServerTaskFailed.php | 31 + app/Events/ServerTaskFinished.php | 31 + app/Events/StackDeleting.php | 48 + app/Events/StackProvisioned.php | 48 + app/Events/StackProvisioning.php | 43 + app/Events/StackTaskFailed.php | 31 + app/Events/StackTaskFinished.php | 31 + app/Events/StackTaskRunning.php | 31 + app/Exceptions/AlreadyDeployingException.php | 10 + app/Exceptions/Handler.php | 67 + app/Exceptions/ManifestNotFoundException.php | 55 + app/Exceptions/ProvisioningTimeout.php | 24 + app/Exceptions/StackProvisioningTimeout.php | 29 + app/FiltersConfigurationArrays.php | 57 + app/Haiku.php | 214 + app/Hook.php | 148 + .../Controllers/API/BalancerController.php | 64 + .../Controllers/API/CallbackController.php | 34 + .../Controllers/API/CancelsDeployments.php | 25 + .../API/CollaboratorController.php | 45 + app/Http/Controllers/API/DaemonController.php | 49 + .../API/DatabaseBackupController.php | 69 + .../Controllers/API/DatabaseController.php | 55 + .../API/DatabaseRestoreController.php | 59 + .../API/DatabaseTransferController.php | 39 + .../Controllers/API/DeploymentController.php | 92 + .../Controllers/API/EnvironmentController.php | 102 + .../API/EnvironmentHookController.php | 29 + app/Http/Controllers/API/HookController.php | 75 + .../API/HookDeploymentController.php | 46 + app/Http/Controllers/API/KeyController.php | 52 + .../API/LastDeploymentController.php | 31 + app/Http/Controllers/API/LoginController.php | 28 + .../API/MaintenancedStackController.php | 47 + .../API/OwnedProjectsController.php | 20 + .../API/ProjectCollaboratorController.php | 74 + .../Controllers/API/ProjectController.php | 86 + .../Controllers/API/ProjectSizeController.php | 22 + .../API/PromotedStackController.php | 62 + .../Controllers/API/SchedulerController.php | 53 + .../API/ServerConfigurationController.php | 22 + .../API/ServerProviderController.php | 52 + .../API/ServerProviderRegionController.php | 22 + .../API/ServerProviderSizeController.php | 22 + .../API/SourceProviderController.php | 73 + .../Controllers/API/SshBalancerController.php | 26 + .../Controllers/API/SshDatabaseController.php | 28 + app/Http/Controllers/API/StackController.php | 63 + .../API/StackDatabaseController.php | 39 + .../Controllers/API/StackServerController.php | 30 + .../API/StackSshServerController.php | 30 + .../Controllers/API/StackTaskController.php | 33 + .../API/StorageProviderController.php | 67 + .../Auth/ForgotPasswordController.php | 32 + app/Http/Controllers/Auth/LoginController.php | 39 + .../Controllers/Auth/RegisterController.php | 74 + .../Auth/ResetPasswordController.php | 39 + app/Http/Controllers/Controller.php | 13 + app/Http/Controllers/ProjectController.php | 30 + app/Http/Controllers/ScheduleController.php | 18 + app/Http/Kernel.php | 62 + app/Http/Middleware/EncryptCookies.php | 17 + .../Middleware/RedirectIfAuthenticated.php | 26 + app/Http/Middleware/TrimStrings.php | 18 + app/Http/Middleware/VerifyCsrfToken.php | 17 + app/Http/Requests/CreateBalancerRequest.php | 47 + .../Requests/CreateDatabaseBackupRequest.php | 45 + app/Http/Requests/CreateDatabaseRequest.php | 46 + app/Http/Requests/CreateDeploymentRequest.php | 74 + .../Requests/CreateHookDeploymentRequest.php | 118 + app/Http/Requests/CreateHookRequest.php | 43 + app/Http/Requests/CreateProjectRequest.php | 70 + app/Http/Requests/ProvisionStackRequest.php | 290 + app/HttpServer.php | 77 + app/InteractsWithSsh.php | 219 + app/IpAddress.php | 30 + app/Jobs/Activate.php | 82 + app/Jobs/AddDnsRecord.php | 45 + app/Jobs/Build.php | 80 + app/Jobs/CreateLoadBalancerIfNecessary.php | 55 + app/Jobs/DeleteDatabaseBackup.php | 54 + app/Jobs/DeleteDnsRecord.php | 53 + app/Jobs/DeleteServerOnProvider.php | 70 + app/Jobs/FinishTask.php | 52 + app/Jobs/HandlesStackProvisioningFailures.php | 29 + app/Jobs/InstallRepository.php | 50 + app/Jobs/ManipulatesDaemons.php | 56 + app/Jobs/MarkStackAsProvisioned.php | 49 + app/Jobs/MonitorDeployment.php | 98 + app/Jobs/PauseDaemons.php | 19 + app/Jobs/PromoteStack.php | 94 + app/Jobs/ProvisionAppServer.php | 19 + app/Jobs/ProvisionBalancer.php | 33 + app/Jobs/ProvisionDatabase.php | 33 + app/Jobs/ProvisionServers.php | 72 + app/Jobs/ProvisionWebServer.php | 19 + app/Jobs/ProvisionWorkerServer.php | 19 + app/Jobs/PruneStackTasks.php | 26 + app/Jobs/PruneTasks.php | 26 + app/Jobs/RemoveKeyFromServer.php | 63 + app/Jobs/RestartDaemons.php | 21 + app/Jobs/RestoreDatabaseBackup.php | 51 + app/Jobs/RunStackTask.php | 43 + app/Jobs/ServerProvisioner.php | 88 + app/Jobs/StartDaemons.php | 19 + app/Jobs/StartScheduler.php | 52 + app/Jobs/StopDaemons.php | 19 + app/Jobs/StopScheduler.php | 52 + app/Jobs/StoreDatabaseBackup.php | 51 + app/Jobs/SyncBalancer.php | 50 + app/Jobs/SyncBalancers.php | 43 + app/Jobs/SyncNetwork.php | 80 + app/Jobs/SyncServer.php | 44 + app/Jobs/SyncServers.php | 43 + app/Jobs/SyncStackNetwork.php | 42 + app/Jobs/TimeOutDeploymentIfStillRunning.php | 52 + app/Jobs/UnpauseDaemons.php | 19 + app/Jobs/UpdateStackDnsRecords.php | 60 + app/Jobs/WaitForDnsRecordToPropagate.php | 54 + app/Jobs/WaitForRepositoryInstallation.php | 52 + .../WaitForServersToFinishProvisioning.php | 71 + app/Jobs/WaitForStackToFinishNetworking.php | 55 + app/Listeners/CheckPendingDeployments.php | 19 + app/Listeners/CreateAlert.php | 19 + app/Listeners/ResetDeploymentStatus.php | 19 + app/Listeners/TrimAlertsForProject.php | 23 + ...dateLastAlertTimestampForCollaborators.php | 23 + app/Mail/BalancerProvisioned.php | 44 + app/Mail/DatabaseProvisioned.php | 44 + app/Mail/StackProvisioned.php | 44 + app/MemoizesMethods.php | 13 + app/Policies/AppServerPolicy.php | 8 + app/Policies/BalancerPolicy.php | 25 + app/Policies/DatabaseBackupPolicy.php | 25 + app/Policies/DatabasePolicy.php | 37 + app/Policies/DatabaseRestorePolicy.php | 24 + app/Policies/EnvironmentPolicy.php | 26 + app/Policies/ProjectPolicy.php | 59 + app/Policies/ServerPolicy.php | 8 + app/Policies/StackPolicy.php | 39 + app/Policies/WebServerPolicy.php | 8 + app/Policies/WorkerServerPolicy.php | 8 + app/Project.php | 262 + app/Providers/AppServiceProvider.php | 45 + app/Providers/AuthServiceProvider.php | 39 + app/Providers/BroadcastServiceProvider.php | 21 + app/Providers/EventServiceProvider.php | 138 + app/Providers/RouteServiceProvider.php | 101 + app/Provisionable.php | 259 + app/Prunable.php | 34 + app/Rules/DatabaseIsProvisioned.php | 57 + app/Rules/StackIsPromotable.php | 49 + app/Rules/ValidAppServerStack.php | 48 + app/Rules/ValidBranch.php | 64 + app/Rules/ValidCommit.php | 64 + app/Rules/ValidDatabaseName.php | 55 + app/Rules/ValidRepository.php | 64 + app/Rules/ValidServeList.php | 74 + app/Rules/ValidSize.php | 53 + app/Rules/ValidSourceName.php | 55 + app/Scripts/Activate.php | 77 + app/Scripts/AddKeyToServer.php | 76 + app/Scripts/Build.php | 68 + app/Scripts/DaemonScript.php | 46 + app/Scripts/GetAptLockStatus.php | 34 + app/Scripts/GetCurrentDirectory.php | 33 + app/Scripts/PauseDaemons.php | 26 + app/Scripts/ProvisionAppServer.php | 55 + app/Scripts/ProvisionBalancer.php | 45 + app/Scripts/ProvisionDatabase.php | 49 + app/Scripts/ProvisionWebServer.php | 54 + app/Scripts/ProvisionWorkerServer.php | 51 + app/Scripts/ProvisioningScript.php | 26 + app/Scripts/RemoveKeyFromServer.php | 66 + app/Scripts/RestartDaemons.php | 54 + app/Scripts/RestoreDatabaseBackup.php | 67 + app/Scripts/RunServerTask.php | 64 + app/Scripts/Script.php | 52 + app/Scripts/Sleep.php | 23 + app/Scripts/StartDaemons.php | 26 + app/Scripts/StartScheduler.php | 58 + app/Scripts/StopDaemons.php | 26 + app/Scripts/StopScheduler.php | 56 + app/Scripts/StoreDatabaseBackup.php | 66 + app/Scripts/SyncBalancer.php | 107 + app/Scripts/SyncNetwork.php | 61 + app/Scripts/SyncServer.php | 58 + app/Scripts/UnpauseDaemons.php | 26 + app/Scripts/WriteDummyFile.php | 23 + .../WritesCaddyServerConfigurations.php | 36 + app/SecureShellCommand.php | 48 + app/SecureShellKey.php | 102 + app/Server.php | 305 + app/ServerDeployment.php | 431 ++ app/ServerProvider.php | 94 + app/ServerProviderClientFactory.php | 25 + app/ServerRecordCreator.php | 87 + app/ServerTask.php | 137 + app/Services/DigitalOcean.php | 350 + app/Services/GitHub.php | 313 + app/Services/LocalYamlParser.php | 20 + app/Services/Route53.php | 141 + app/Services/S3.php | 228 + app/ShellCommand.php | 64 + app/ShellOutput.php | 41 + app/ShellProcessRunner.php | 46 + app/ShellResponse.php | 42 + app/SourceProvider.php | 60 + app/SourceProviderClientFactory.php | 25 + app/Stack.php | 916 +++ app/StackMetadata.php | 45 + app/StackTask.php | 163 + app/StorageProvider.php | 70 + app/StorageProviderClientFactory.php | 25 + app/Task.php | 167 + app/TaskFactory.php | 30 + app/User.php | 147 + app/WebServer.php | 53 + app/WebServerRecordCreator.php | 23 + app/WorkerServer.php | 63 + app/WorkerServerRecordCreator.php | 23 + artisan | 51 + bootstrap/app.php | 55 + bootstrap/autoload.php | 17 + bootstrap/cache/.gitignore | 2 + composer.json | 62 + composer.lock | 5354 ++++++++++++++ config/app.php | 235 + config/auth.php | 102 + config/broadcasting.php | 58 + config/cache.php | 91 + config/database.php | 109 + config/filesystems.php | 68 + config/mail.php | 123 + config/queue.php | 85 + config/services.php | 54 + config/session.php | 179 + config/view.php | 33 + database/.gitignore | 1 + database/factories/AppServerFactory.php | 28 + database/factories/BalancerFactory.php | 24 + database/factories/CertificateFactory.php | 21 + database/factories/DatabaseBackupFactory.php | 23 + database/factories/DatabaseFactory.php | 27 + database/factories/DatabaseRestoreFactory.php | 22 + database/factories/DeploymentFactory.php | 26 + database/factories/EnvironmentFactory.php | 22 + database/factories/HookFactory.php | 23 + database/factories/IpAddressFactory.php | 21 + database/factories/ProjectFactory.php | 23 + .../factories/ServerDeploymentFactory.php | 23 + database/factories/ServerProviderFactory.php | 23 + database/factories/ServerTaskFactory.php | 24 + database/factories/SourceProviderFactory.php | 22 + database/factories/StackFactory.php | 30 + database/factories/StackTaskFactory.php | 23 + database/factories/StorageProviderFactory.php | 27 + database/factories/TaskFactory.php | 27 + database/factories/UserFactory.php | 27 + database/factories/WebServerFactory.php | 26 + database/factories/WorkerServerFactory.php | 27 + .../2014_10_12_000000_create_users_table.php | 31 + ...12_100000_create_password_resets_table.php | 22 + ...017_03_31_170538_create_projects_table.php | 28 + ...03_31_172929_create_environments_table.php | 26 + .../2017_04_03_152027_create_stacks_table.php | 37 + ...3_193949_create_server_providers_table.php | 25 + ...17_04_07_195655_create_databases_table.php | 34 + ...17_04_10_190135_create_balancers_table.php | 32 + .../2017_04_18_203351_create_tasks_table.php | 34 + ...6_163353_create_source_providers_table.php | 25 + .../2017_04_27_193631_create_jobs_table.php | 28 + ...4_28_160656_create_project_users_table.php | 24 + ...05_08_175021_create_ip_addresses_table.php | 28 + ..._05_10_150545_create_failed_jobs_table.php | 35 + .../2017_05_10_203142_create_alerts_table.php | 27 + ..._05_11_213227_create_app_servers_table.php | 34 + ..._05_11_213349_create_web_servers_table.php | 32 + ..._11_213407_create_worker_servers_table.php | 32 + ..._05_25_194654_create_deployments_table.php | 34 + ...195216_create_server_deployments_table.php | 36 + ...154558_create_daemon_generations_table.php | 35 + ...14_151436_create_stack_databases_table.php | 33 + ...07_29_024811_create_certificates_table.php | 35 + ..._07_31_191305_create_stack_tasks_table.php | 38 + ...07_31_191310_create_server_tasks_table.php | 42 + ..._155914_create_storage_providers_table.php | 35 + ...3_145343_create_database_backups_table.php | 49 + ..._181907_create_database_restores_table.php | 42 + .../2017_08_10_151936_create_hooks_table.php | 37 + database/seeds/DatabaseSeeder.php | 33 + helpers.php | 43 + package.json | 23 + phpunit.xml | 38 + public/css/.gitignore | 2 + public/favicon.ico | 0 .../glyphicons-halflings-regular.eot | Bin 0 -> 20127 bytes .../glyphicons-halflings-regular.svg | 288 + .../glyphicons-halflings-regular.ttf | Bin 0 -> 45404 bytes .../glyphicons-halflings-regular.woff | Bin 0 -> 23424 bytes .../glyphicons-halflings-regular.woff2 | Bin 0 -> 18028 bytes public/index.php | 58 + public/js/.gitignore | 2 + public/mix-manifest.json | 3 + public/robots.txt | 2 + resources/assets/js/app.js | 25 + resources/assets/js/bootstrap.js | 54 + .../assets/js/components/ProjectList.vue | 37 + .../components/passport/AuthorizedClients.vue | 111 + .../assets/js/components/passport/Clients.vue | 354 + .../passport/PersonalAccessTokens.vue | 302 + resources/assets/js/root.js | 29 + resources/assets/sass/_variables.scss | 38 + resources/assets/sass/app.scss | 9 + resources/lang/en/auth.php | 19 + resources/lang/en/pagination.php | 19 + resources/lang/en/passwords.php | 22 + resources/lang/en/validation.php | 119 + resources/views/auth/login.blade.php | 68 + .../views/auth/passwords/email.blade.php | 46 + .../views/auth/passwords/reset.blade.php | 76 + resources/views/auth/register.blade.php | 76 + resources/views/home.blade.php | 17 + resources/views/layouts/app.blade.php | 87 + .../views/mail/balancer/provisioned.blade.php | 13 + .../views/mail/database/provisioned.blade.php | 17 + .../views/mail/stack/provisioned.blade.php | 16 + resources/views/projects/index.blade.php | 17 + .../views/scripts/app/provision.blade.php | 65 + .../scripts/balancer/provision.blade.php | 37 + .../views/scripts/balancer/sync.blade.php | 24 + .../scripts/caddy-configuration/app.blade.php | 25 + .../caddy-configuration/proxy.blade.php | 13 + .../caddy-configuration/redirect.blade.php | 7 + .../views/scripts/caddy/install.blade.php | 40 + .../views/scripts/daemon/activate.blade.php | 19 + .../views/scripts/daemon/build.blade.php | 51 + .../views/scripts/daemon/pause.blade.php | 5 + .../views/scripts/daemon/restart.blade.php | 8 + .../views/scripts/daemon/start.blade.php | 5 + resources/views/scripts/daemon/stop.blade.php | 8 + .../views/scripts/daemon/unpause.blade.php | 5 + .../views/scripts/database/backup.blade.php | 44 + .../views/scripts/database/install.blade.php | 61 + .../views/scripts/database/network.blade.php | 16 + .../scripts/database/provision.blade.php | 14 + .../views/scripts/database/restore.blade.php | 43 + .../scripts/deployment/activate.blade.php | 40 + .../views/scripts/deployment/build.blade.php | 110 + .../views/scripts/node/install.blade.php | 12 + resources/views/scripts/php/cli.ini | 22 + resources/views/scripts/php/fpm.ini | 30 + resources/views/scripts/php/install.blade.php | 58 + resources/views/scripts/php/www.conf | 16 + .../scripts/provisionable/addKey.blade.php | 10 + .../scripts/provisionable/base.blade.php | 192 + .../scripts/provisionable/removeKey.blade.php | 6 + .../views/scripts/scheduler/start.blade.php | 13 + .../views/scripts/scheduler/stop.blade.php | 4 + resources/views/scripts/server/sync.blade.php | 15 + .../s3.blade.php | 18 + .../views/scripts/tools/callback.blade.php | 21 + resources/views/scripts/tools/chown.blade.php | 13 + .../views/scripts/web/provision.blade.php | 61 + .../views/scripts/worker/provision.blade.php | 21 + resources/views/welcome.blade.php | 95 + routes/api.php | 132 + routes/channels.php | 22 + routes/console.php | 25 + routes/schedule.php | 3 + routes/web.php | 20 + storage/app/.gitignore | 5 + storage/app/keys/.gitignore | 2 + storage/app/public/.gitignore | 2 + storage/app/scripts/.gitignore | 2 + storage/framework/.gitignore | 8 + storage/framework/cache/.gitignore | 2 + storage/framework/sessions/.gitignore | 2 + storage/framework/testing/.gitignore | 2 + storage/framework/views/.gitignore | 2 + storage/logs/.gitignore | 2 + tests/CreatesApplication.php | 22 + tests/Fakes/FakeTask.php | 22 + tests/Feature/ActivateJobTest.php | 65 + tests/Feature/ActivateScriptTest.php | 33 + tests/Feature/BalancerControllerTest.php | 133 + tests/Feature/BuildJobTest.php | 58 + tests/Feature/BuildScriptTest.php | 33 + tests/Feature/CallbackControllerTest.php | 107 + tests/Feature/CheckActivationCallbackTest.php | 52 + tests/Feature/CheckBuildCallbackTest.php | 52 + .../CreateLoadBalancerIfNecessaryJobTest.php | 109 + tests/Feature/DaemonControllerTest.php | 110 + .../Feature/DatabaseBackupControllerTest.php | 165 + tests/Feature/DatabaseBackupTest.php | 81 + tests/Feature/DatabaseControllerTest.php | 117 + .../Feature/DatabaseIsProvisionedRuleTest.php | 39 + .../Feature/DatabaseRestoreControllerTest.php | 123 + tests/Feature/DatabaseTest.php | 55 + .../DatabaseTransferControllerTest.php | 91 + .../Feature/DeleteServerOnProviderJobTest.php | 37 + tests/Feature/DeploymentControllerTest.php | 212 + tests/Feature/DeploymentTest.php | 266 + tests/Feature/DigitalOceanProviderTest.php | 56 + tests/Feature/DispatchCallbackTest.php | 55 + tests/Feature/EnvironmentControllerTest.php | 225 + tests/Feature/EnvironmentTest.php | 31 + tests/Feature/GitHubTest.php | 115 + .../HandlesStackProvisioningFailuresTest.php | 45 + tests/Feature/HookControllerTest.php | 142 + .../Feature/HookDeploymentControllerTest.php | 239 + tests/Feature/KeyControllerTest.php | 119 + .../Feature/MarkAsProvisionedCallbackTest.php | 50 + tests/Feature/MonitorDeploymentJobTest.php | 139 + tests/Feature/ProjectControllerTest.php | 170 + tests/Feature/ProjectTest.php | 69 + tests/Feature/PromoteStackJobTest.php | 179 + tests/Feature/PromotedStackControllerTest.php | 159 + tests/Feature/ProviderControllerTest.php | 56 + .../Feature/ProvisionAppServerScriptTest.php | 33 + tests/Feature/ProvisionBalancerJobTest.php | 46 + tests/Feature/ProvisionBalancerScriptTest.php | 33 + tests/Feature/ProvisionDatabaseJobTest.php | 46 + tests/Feature/ProvisionDatabaseScriptTest.php | 31 + tests/Feature/ProvisionServersJobTest.php | 78 + tests/Feature/ProvisionableTest.php | 109 + tests/Feature/ReportHelperTest.php | 34 + tests/Feature/RestartDaemonsJobTest.php | 48 + .../Feature/RestoreDatabaseBackupJobTest.php | 46 + .../RestoreDatabaseBackupScriptTest.php | 34 + tests/Feature/Route53Test.php | 53 + tests/Feature/S3Test.php | 75 + tests/Feature/ScheduleControllerTest.php | 32 + tests/Feature/SchedulerControllerTest.php | 80 + .../ServerConfigurationControllerTest.php | 40 + tests/Feature/ServerDeploymentTest.php | 37 + tests/Feature/ServerTest.php | 54 + tests/Feature/ShellProcessRunnerTest.php | 49 + tests/Feature/SourceControllerTest.php | 96 + tests/Feature/StackControllerTest.php | 348 + tests/Feature/StackDatabaseControllerTest.php | 108 + tests/Feature/StackDeploymentTest.php | 112 + tests/Feature/StackServerControllerTest.php | 45 + tests/Feature/StackTaskControllerTest.php | 121 + tests/Feature/StackTaskTest.php | 175 + tests/Feature/StackTest.php | 339 + .../StartBackgroundServicesCallbackTest.php | 104 + .../Feature/StorageProviderControllerTest.php | 90 + tests/Feature/StoreDatabaseBackupJobTest.php | 46 + .../Feature/StoreDatabaseBackupScriptTest.php | 31 + tests/Feature/SyncBalancerScriptTest.php | 31 + tests/Feature/SyncNetworkJobTest.php | 117 + tests/Feature/TaskTest.php | 81 + ...tTimestampForCollaboratorsListenerTest.php | 40 + .../Feature/UpdateStackDnsRecordsJobTest.php | 55 + tests/Feature/ValidDatabaseNameRuleTest.php | 37 + tests/Feature/ValidRepositoryRuleTest.php | 48 + tests/Feature/ValidServeListRuleTest.php | 39 + tests/Feature/ValidSourceNameRuleTest.php | 39 + ...tForServersToFinishProvisioningJobTest.php | 103 + tests/TestCase.php | 54 + tests/Unit/ExampleTest.php | 18 + webpack.mix.js | 15 + yarn.lock | 6176 +++++++++++++++++ 520 files changed, 42715 insertions(+) create mode 100644 .env.example create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 app/Alert.php create mode 100644 app/AppServer.php create mode 100644 app/AppServerRecordCreator.php create mode 100644 app/Balancer.php create mode 100644 app/CaddyBalancerConfiguration.php create mode 100644 app/CaddyServerConfiguration.php create mode 100644 app/Callbacks/CheckActivation.php create mode 100644 app/Callbacks/CheckBuild.php create mode 100644 app/Callbacks/CheckDatabaseBackup.php create mode 100644 app/Callbacks/CheckDatabaseRestore.php create mode 100644 app/Callbacks/CheckServerTask.php create mode 100644 app/Callbacks/Dispatch.php create mode 100644 app/Callbacks/MarkAsProvisioned.php create mode 100644 app/Callbacks/StartBackgroundServices.php create mode 100644 app/Certificate.php create mode 100644 app/Collaborator.php create mode 100644 app/Console/Kernel.php create mode 100644 app/Contracts/Alertable.php create mode 100644 app/Contracts/DnsProvider.php create mode 100644 app/Contracts/HasStack.php create mode 100644 app/Contracts/Provisionable.php create mode 100644 app/Contracts/ServerProviderClient.php create mode 100644 app/Contracts/SourceProviderClient.php create mode 100644 app/Contracts/StackDefinition.php create mode 100644 app/Contracts/StorageProviderClient.php create mode 100644 app/Contracts/YamlParser.php create mode 100644 app/DaemonGeneration.php create mode 100644 app/Database.php create mode 100644 app/DatabaseBackup.php create mode 100644 app/DatabaseRestore.php create mode 100644 app/Deployment.php create mode 100644 app/DeploymentInstructions.php create mode 100644 app/DeterminesAge.php create mode 100644 app/Environment.php create mode 100644 app/Events/AlertCreated.php create mode 100644 app/Events/DatabaseBackupFailed.php create mode 100644 app/Events/DatabaseBackupFinished.php create mode 100644 app/Events/DatabaseBackupRunning.php create mode 100644 app/Events/DatabaseRestoreFailed.php create mode 100644 app/Events/DatabaseRestoreFinished.php create mode 100644 app/Events/DatabaseRestoreRunning.php create mode 100644 app/Events/DeploymentActivating.php create mode 100644 app/Events/DeploymentBuilding.php create mode 100644 app/Events/DeploymentCancelled.php create mode 100644 app/Events/DeploymentFailed.php create mode 100644 app/Events/DeploymentFinished.php create mode 100644 app/Events/DeploymentTimedOut.php create mode 100644 app/Events/ProjectShared.php create mode 100644 app/Events/ProjectUnshared.php create mode 100644 app/Events/ServerDeploymentActivated.php create mode 100644 app/Events/ServerDeploymentBuilt.php create mode 100644 app/Events/ServerDeploymentFailed.php create mode 100644 app/Events/ServerTaskFailed.php create mode 100644 app/Events/ServerTaskFinished.php create mode 100644 app/Events/StackDeleting.php create mode 100644 app/Events/StackProvisioned.php create mode 100644 app/Events/StackProvisioning.php create mode 100644 app/Events/StackTaskFailed.php create mode 100644 app/Events/StackTaskFinished.php create mode 100644 app/Events/StackTaskRunning.php create mode 100644 app/Exceptions/AlreadyDeployingException.php create mode 100644 app/Exceptions/Handler.php create mode 100644 app/Exceptions/ManifestNotFoundException.php create mode 100644 app/Exceptions/ProvisioningTimeout.php create mode 100644 app/Exceptions/StackProvisioningTimeout.php create mode 100644 app/FiltersConfigurationArrays.php create mode 100644 app/Haiku.php create mode 100644 app/Hook.php create mode 100644 app/Http/Controllers/API/BalancerController.php create mode 100644 app/Http/Controllers/API/CallbackController.php create mode 100644 app/Http/Controllers/API/CancelsDeployments.php create mode 100644 app/Http/Controllers/API/CollaboratorController.php create mode 100644 app/Http/Controllers/API/DaemonController.php create mode 100644 app/Http/Controllers/API/DatabaseBackupController.php create mode 100644 app/Http/Controllers/API/DatabaseController.php create mode 100644 app/Http/Controllers/API/DatabaseRestoreController.php create mode 100644 app/Http/Controllers/API/DatabaseTransferController.php create mode 100644 app/Http/Controllers/API/DeploymentController.php create mode 100644 app/Http/Controllers/API/EnvironmentController.php create mode 100644 app/Http/Controllers/API/EnvironmentHookController.php create mode 100644 app/Http/Controllers/API/HookController.php create mode 100644 app/Http/Controllers/API/HookDeploymentController.php create mode 100644 app/Http/Controllers/API/KeyController.php create mode 100644 app/Http/Controllers/API/LastDeploymentController.php create mode 100644 app/Http/Controllers/API/LoginController.php create mode 100644 app/Http/Controllers/API/MaintenancedStackController.php create mode 100644 app/Http/Controllers/API/OwnedProjectsController.php create mode 100644 app/Http/Controllers/API/ProjectCollaboratorController.php create mode 100644 app/Http/Controllers/API/ProjectController.php create mode 100644 app/Http/Controllers/API/ProjectSizeController.php create mode 100644 app/Http/Controllers/API/PromotedStackController.php create mode 100644 app/Http/Controllers/API/SchedulerController.php create mode 100644 app/Http/Controllers/API/ServerConfigurationController.php create mode 100644 app/Http/Controllers/API/ServerProviderController.php create mode 100644 app/Http/Controllers/API/ServerProviderRegionController.php create mode 100644 app/Http/Controllers/API/ServerProviderSizeController.php create mode 100644 app/Http/Controllers/API/SourceProviderController.php create mode 100644 app/Http/Controllers/API/SshBalancerController.php create mode 100644 app/Http/Controllers/API/SshDatabaseController.php create mode 100644 app/Http/Controllers/API/StackController.php create mode 100644 app/Http/Controllers/API/StackDatabaseController.php create mode 100644 app/Http/Controllers/API/StackServerController.php create mode 100644 app/Http/Controllers/API/StackSshServerController.php create mode 100644 app/Http/Controllers/API/StackTaskController.php create mode 100644 app/Http/Controllers/API/StorageProviderController.php create mode 100644 app/Http/Controllers/Auth/ForgotPasswordController.php create mode 100644 app/Http/Controllers/Auth/LoginController.php create mode 100644 app/Http/Controllers/Auth/RegisterController.php create mode 100644 app/Http/Controllers/Auth/ResetPasswordController.php create mode 100644 app/Http/Controllers/Controller.php create mode 100644 app/Http/Controllers/ProjectController.php create mode 100644 app/Http/Controllers/ScheduleController.php create mode 100644 app/Http/Kernel.php create mode 100644 app/Http/Middleware/EncryptCookies.php create mode 100644 app/Http/Middleware/RedirectIfAuthenticated.php create mode 100644 app/Http/Middleware/TrimStrings.php create mode 100644 app/Http/Middleware/VerifyCsrfToken.php create mode 100644 app/Http/Requests/CreateBalancerRequest.php create mode 100644 app/Http/Requests/CreateDatabaseBackupRequest.php create mode 100644 app/Http/Requests/CreateDatabaseRequest.php create mode 100644 app/Http/Requests/CreateDeploymentRequest.php create mode 100644 app/Http/Requests/CreateHookDeploymentRequest.php create mode 100644 app/Http/Requests/CreateHookRequest.php create mode 100644 app/Http/Requests/CreateProjectRequest.php create mode 100644 app/Http/Requests/ProvisionStackRequest.php create mode 100644 app/HttpServer.php create mode 100644 app/InteractsWithSsh.php create mode 100644 app/IpAddress.php create mode 100644 app/Jobs/Activate.php create mode 100644 app/Jobs/AddDnsRecord.php create mode 100644 app/Jobs/Build.php create mode 100644 app/Jobs/CreateLoadBalancerIfNecessary.php create mode 100644 app/Jobs/DeleteDatabaseBackup.php create mode 100644 app/Jobs/DeleteDnsRecord.php create mode 100644 app/Jobs/DeleteServerOnProvider.php create mode 100644 app/Jobs/FinishTask.php create mode 100644 app/Jobs/HandlesStackProvisioningFailures.php create mode 100644 app/Jobs/InstallRepository.php create mode 100644 app/Jobs/ManipulatesDaemons.php create mode 100644 app/Jobs/MarkStackAsProvisioned.php create mode 100644 app/Jobs/MonitorDeployment.php create mode 100644 app/Jobs/PauseDaemons.php create mode 100644 app/Jobs/PromoteStack.php create mode 100644 app/Jobs/ProvisionAppServer.php create mode 100644 app/Jobs/ProvisionBalancer.php create mode 100644 app/Jobs/ProvisionDatabase.php create mode 100644 app/Jobs/ProvisionServers.php create mode 100644 app/Jobs/ProvisionWebServer.php create mode 100644 app/Jobs/ProvisionWorkerServer.php create mode 100644 app/Jobs/PruneStackTasks.php create mode 100644 app/Jobs/PruneTasks.php create mode 100644 app/Jobs/RemoveKeyFromServer.php create mode 100644 app/Jobs/RestartDaemons.php create mode 100644 app/Jobs/RestoreDatabaseBackup.php create mode 100644 app/Jobs/RunStackTask.php create mode 100644 app/Jobs/ServerProvisioner.php create mode 100644 app/Jobs/StartDaemons.php create mode 100644 app/Jobs/StartScheduler.php create mode 100644 app/Jobs/StopDaemons.php create mode 100644 app/Jobs/StopScheduler.php create mode 100644 app/Jobs/StoreDatabaseBackup.php create mode 100644 app/Jobs/SyncBalancer.php create mode 100644 app/Jobs/SyncBalancers.php create mode 100644 app/Jobs/SyncNetwork.php create mode 100644 app/Jobs/SyncServer.php create mode 100644 app/Jobs/SyncServers.php create mode 100644 app/Jobs/SyncStackNetwork.php create mode 100644 app/Jobs/TimeOutDeploymentIfStillRunning.php create mode 100644 app/Jobs/UnpauseDaemons.php create mode 100644 app/Jobs/UpdateStackDnsRecords.php create mode 100644 app/Jobs/WaitForDnsRecordToPropagate.php create mode 100644 app/Jobs/WaitForRepositoryInstallation.php create mode 100644 app/Jobs/WaitForServersToFinishProvisioning.php create mode 100644 app/Jobs/WaitForStackToFinishNetworking.php create mode 100644 app/Listeners/CheckPendingDeployments.php create mode 100644 app/Listeners/CreateAlert.php create mode 100644 app/Listeners/ResetDeploymentStatus.php create mode 100644 app/Listeners/TrimAlertsForProject.php create mode 100644 app/Listeners/UpdateLastAlertTimestampForCollaborators.php create mode 100644 app/Mail/BalancerProvisioned.php create mode 100644 app/Mail/DatabaseProvisioned.php create mode 100644 app/Mail/StackProvisioned.php create mode 100644 app/MemoizesMethods.php create mode 100644 app/Policies/AppServerPolicy.php create mode 100644 app/Policies/BalancerPolicy.php create mode 100644 app/Policies/DatabaseBackupPolicy.php create mode 100644 app/Policies/DatabasePolicy.php create mode 100644 app/Policies/DatabaseRestorePolicy.php create mode 100644 app/Policies/EnvironmentPolicy.php create mode 100644 app/Policies/ProjectPolicy.php create mode 100644 app/Policies/ServerPolicy.php create mode 100644 app/Policies/StackPolicy.php create mode 100644 app/Policies/WebServerPolicy.php create mode 100644 app/Policies/WorkerServerPolicy.php create mode 100644 app/Project.php create mode 100644 app/Providers/AppServiceProvider.php create mode 100644 app/Providers/AuthServiceProvider.php create mode 100644 app/Providers/BroadcastServiceProvider.php create mode 100644 app/Providers/EventServiceProvider.php create mode 100644 app/Providers/RouteServiceProvider.php create mode 100644 app/Provisionable.php create mode 100644 app/Prunable.php create mode 100644 app/Rules/DatabaseIsProvisioned.php create mode 100644 app/Rules/StackIsPromotable.php create mode 100644 app/Rules/ValidAppServerStack.php create mode 100644 app/Rules/ValidBranch.php create mode 100644 app/Rules/ValidCommit.php create mode 100644 app/Rules/ValidDatabaseName.php create mode 100644 app/Rules/ValidRepository.php create mode 100644 app/Rules/ValidServeList.php create mode 100644 app/Rules/ValidSize.php create mode 100644 app/Rules/ValidSourceName.php create mode 100644 app/Scripts/Activate.php create mode 100644 app/Scripts/AddKeyToServer.php create mode 100644 app/Scripts/Build.php create mode 100644 app/Scripts/DaemonScript.php create mode 100644 app/Scripts/GetAptLockStatus.php create mode 100644 app/Scripts/GetCurrentDirectory.php create mode 100644 app/Scripts/PauseDaemons.php create mode 100644 app/Scripts/ProvisionAppServer.php create mode 100644 app/Scripts/ProvisionBalancer.php create mode 100644 app/Scripts/ProvisionDatabase.php create mode 100644 app/Scripts/ProvisionWebServer.php create mode 100644 app/Scripts/ProvisionWorkerServer.php create mode 100644 app/Scripts/ProvisioningScript.php create mode 100644 app/Scripts/RemoveKeyFromServer.php create mode 100644 app/Scripts/RestartDaemons.php create mode 100644 app/Scripts/RestoreDatabaseBackup.php create mode 100644 app/Scripts/RunServerTask.php create mode 100644 app/Scripts/Script.php create mode 100644 app/Scripts/Sleep.php create mode 100644 app/Scripts/StartDaemons.php create mode 100644 app/Scripts/StartScheduler.php create mode 100644 app/Scripts/StopDaemons.php create mode 100644 app/Scripts/StopScheduler.php create mode 100644 app/Scripts/StoreDatabaseBackup.php create mode 100644 app/Scripts/SyncBalancer.php create mode 100644 app/Scripts/SyncNetwork.php create mode 100644 app/Scripts/SyncServer.php create mode 100644 app/Scripts/UnpauseDaemons.php create mode 100644 app/Scripts/WriteDummyFile.php create mode 100644 app/Scripts/WritesCaddyServerConfigurations.php create mode 100644 app/SecureShellCommand.php create mode 100644 app/SecureShellKey.php create mode 100644 app/Server.php create mode 100644 app/ServerDeployment.php create mode 100644 app/ServerProvider.php create mode 100644 app/ServerProviderClientFactory.php create mode 100644 app/ServerRecordCreator.php create mode 100644 app/ServerTask.php create mode 100644 app/Services/DigitalOcean.php create mode 100644 app/Services/GitHub.php create mode 100644 app/Services/LocalYamlParser.php create mode 100644 app/Services/Route53.php create mode 100644 app/Services/S3.php create mode 100644 app/ShellCommand.php create mode 100644 app/ShellOutput.php create mode 100644 app/ShellProcessRunner.php create mode 100644 app/ShellResponse.php create mode 100644 app/SourceProvider.php create mode 100644 app/SourceProviderClientFactory.php create mode 100644 app/Stack.php create mode 100644 app/StackMetadata.php create mode 100644 app/StackTask.php create mode 100644 app/StorageProvider.php create mode 100644 app/StorageProviderClientFactory.php create mode 100644 app/Task.php create mode 100644 app/TaskFactory.php create mode 100644 app/User.php create mode 100644 app/WebServer.php create mode 100644 app/WebServerRecordCreator.php create mode 100644 app/WorkerServer.php create mode 100644 app/WorkerServerRecordCreator.php create mode 100644 artisan create mode 100644 bootstrap/app.php create mode 100644 bootstrap/autoload.php create mode 100644 bootstrap/cache/.gitignore create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 config/app.php create mode 100644 config/auth.php create mode 100644 config/broadcasting.php create mode 100644 config/cache.php create mode 100644 config/database.php create mode 100644 config/filesystems.php create mode 100644 config/mail.php create mode 100644 config/queue.php create mode 100644 config/services.php create mode 100644 config/session.php create mode 100644 config/view.php create mode 100644 database/.gitignore create mode 100644 database/factories/AppServerFactory.php create mode 100644 database/factories/BalancerFactory.php create mode 100644 database/factories/CertificateFactory.php create mode 100644 database/factories/DatabaseBackupFactory.php create mode 100644 database/factories/DatabaseFactory.php create mode 100644 database/factories/DatabaseRestoreFactory.php create mode 100644 database/factories/DeploymentFactory.php create mode 100644 database/factories/EnvironmentFactory.php create mode 100644 database/factories/HookFactory.php create mode 100644 database/factories/IpAddressFactory.php create mode 100644 database/factories/ProjectFactory.php create mode 100644 database/factories/ServerDeploymentFactory.php create mode 100644 database/factories/ServerProviderFactory.php create mode 100644 database/factories/ServerTaskFactory.php create mode 100644 database/factories/SourceProviderFactory.php create mode 100644 database/factories/StackFactory.php create mode 100644 database/factories/StackTaskFactory.php create mode 100644 database/factories/StorageProviderFactory.php create mode 100644 database/factories/TaskFactory.php create mode 100644 database/factories/UserFactory.php create mode 100644 database/factories/WebServerFactory.php create mode 100644 database/factories/WorkerServerFactory.php create mode 100644 database/migrations/2014_10_12_000000_create_users_table.php create mode 100644 database/migrations/2014_10_12_100000_create_password_resets_table.php create mode 100644 database/migrations/2017_03_31_170538_create_projects_table.php create mode 100644 database/migrations/2017_03_31_172929_create_environments_table.php create mode 100644 database/migrations/2017_04_03_152027_create_stacks_table.php create mode 100644 database/migrations/2017_04_03_193949_create_server_providers_table.php create mode 100644 database/migrations/2017_04_07_195655_create_databases_table.php create mode 100644 database/migrations/2017_04_10_190135_create_balancers_table.php create mode 100644 database/migrations/2017_04_18_203351_create_tasks_table.php create mode 100644 database/migrations/2017_04_26_163353_create_source_providers_table.php create mode 100644 database/migrations/2017_04_27_193631_create_jobs_table.php create mode 100644 database/migrations/2017_04_28_160656_create_project_users_table.php create mode 100644 database/migrations/2017_05_08_175021_create_ip_addresses_table.php create mode 100644 database/migrations/2017_05_10_150545_create_failed_jobs_table.php create mode 100644 database/migrations/2017_05_10_203142_create_alerts_table.php create mode 100644 database/migrations/2017_05_11_213227_create_app_servers_table.php create mode 100644 database/migrations/2017_05_11_213349_create_web_servers_table.php create mode 100644 database/migrations/2017_05_11_213407_create_worker_servers_table.php create mode 100644 database/migrations/2017_05_25_194654_create_deployments_table.php create mode 100644 database/migrations/2017_05_25_195216_create_server_deployments_table.php create mode 100644 database/migrations/2017_06_11_154558_create_daemon_generations_table.php create mode 100644 database/migrations/2017_06_14_151436_create_stack_databases_table.php create mode 100644 database/migrations/2017_07_29_024811_create_certificates_table.php create mode 100644 database/migrations/2017_07_31_191305_create_stack_tasks_table.php create mode 100644 database/migrations/2017_07_31_191310_create_server_tasks_table.php create mode 100644 database/migrations/2017_08_02_155914_create_storage_providers_table.php create mode 100644 database/migrations/2017_08_03_145343_create_database_backups_table.php create mode 100644 database/migrations/2017_08_07_181907_create_database_restores_table.php create mode 100644 database/migrations/2017_08_10_151936_create_hooks_table.php create mode 100644 database/seeds/DatabaseSeeder.php create mode 100644 helpers.php create mode 100644 package.json create mode 100644 phpunit.xml create mode 100644 public/css/.gitignore create mode 100644 public/favicon.ico create mode 100644 public/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.eot create mode 100644 public/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.svg create mode 100644 public/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.ttf create mode 100644 public/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.woff create mode 100644 public/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.woff2 create mode 100644 public/index.php create mode 100644 public/js/.gitignore create mode 100644 public/mix-manifest.json create mode 100644 public/robots.txt create mode 100644 resources/assets/js/app.js create mode 100644 resources/assets/js/bootstrap.js create mode 100644 resources/assets/js/components/ProjectList.vue create mode 100644 resources/assets/js/components/passport/AuthorizedClients.vue create mode 100644 resources/assets/js/components/passport/Clients.vue create mode 100644 resources/assets/js/components/passport/PersonalAccessTokens.vue create mode 100644 resources/assets/js/root.js create mode 100644 resources/assets/sass/_variables.scss create mode 100644 resources/assets/sass/app.scss create mode 100644 resources/lang/en/auth.php create mode 100644 resources/lang/en/pagination.php create mode 100644 resources/lang/en/passwords.php create mode 100644 resources/lang/en/validation.php create mode 100644 resources/views/auth/login.blade.php create mode 100644 resources/views/auth/passwords/email.blade.php create mode 100644 resources/views/auth/passwords/reset.blade.php create mode 100644 resources/views/auth/register.blade.php create mode 100644 resources/views/home.blade.php create mode 100644 resources/views/layouts/app.blade.php create mode 100644 resources/views/mail/balancer/provisioned.blade.php create mode 100644 resources/views/mail/database/provisioned.blade.php create mode 100644 resources/views/mail/stack/provisioned.blade.php create mode 100644 resources/views/projects/index.blade.php create mode 100644 resources/views/scripts/app/provision.blade.php create mode 100644 resources/views/scripts/balancer/provision.blade.php create mode 100644 resources/views/scripts/balancer/sync.blade.php create mode 100644 resources/views/scripts/caddy-configuration/app.blade.php create mode 100644 resources/views/scripts/caddy-configuration/proxy.blade.php create mode 100644 resources/views/scripts/caddy-configuration/redirect.blade.php create mode 100644 resources/views/scripts/caddy/install.blade.php create mode 100644 resources/views/scripts/daemon/activate.blade.php create mode 100644 resources/views/scripts/daemon/build.blade.php create mode 100644 resources/views/scripts/daemon/pause.blade.php create mode 100644 resources/views/scripts/daemon/restart.blade.php create mode 100644 resources/views/scripts/daemon/start.blade.php create mode 100644 resources/views/scripts/daemon/stop.blade.php create mode 100644 resources/views/scripts/daemon/unpause.blade.php create mode 100644 resources/views/scripts/database/backup.blade.php create mode 100644 resources/views/scripts/database/install.blade.php create mode 100644 resources/views/scripts/database/network.blade.php create mode 100644 resources/views/scripts/database/provision.blade.php create mode 100644 resources/views/scripts/database/restore.blade.php create mode 100644 resources/views/scripts/deployment/activate.blade.php create mode 100644 resources/views/scripts/deployment/build.blade.php create mode 100644 resources/views/scripts/node/install.blade.php create mode 100644 resources/views/scripts/php/cli.ini create mode 100644 resources/views/scripts/php/fpm.ini create mode 100644 resources/views/scripts/php/install.blade.php create mode 100644 resources/views/scripts/php/www.conf create mode 100644 resources/views/scripts/provisionable/addKey.blade.php create mode 100644 resources/views/scripts/provisionable/base.blade.php create mode 100644 resources/views/scripts/provisionable/removeKey.blade.php create mode 100644 resources/views/scripts/scheduler/start.blade.php create mode 100644 resources/views/scripts/scheduler/stop.blade.php create mode 100644 resources/views/scripts/server/sync.blade.php create mode 100644 resources/views/scripts/storage-provider-configuration/s3.blade.php create mode 100644 resources/views/scripts/tools/callback.blade.php create mode 100644 resources/views/scripts/tools/chown.blade.php create mode 100644 resources/views/scripts/web/provision.blade.php create mode 100644 resources/views/scripts/worker/provision.blade.php create mode 100644 resources/views/welcome.blade.php create mode 100644 routes/api.php create mode 100644 routes/channels.php create mode 100644 routes/console.php create mode 100644 routes/schedule.php create mode 100644 routes/web.php create mode 100644 storage/app/.gitignore create mode 100644 storage/app/keys/.gitignore create mode 100644 storage/app/public/.gitignore create mode 100644 storage/app/scripts/.gitignore create mode 100644 storage/framework/.gitignore create mode 100644 storage/framework/cache/.gitignore create mode 100644 storage/framework/sessions/.gitignore create mode 100644 storage/framework/testing/.gitignore create mode 100644 storage/framework/views/.gitignore create mode 100644 storage/logs/.gitignore create mode 100644 tests/CreatesApplication.php create mode 100644 tests/Fakes/FakeTask.php create mode 100644 tests/Feature/ActivateJobTest.php create mode 100644 tests/Feature/ActivateScriptTest.php create mode 100644 tests/Feature/BalancerControllerTest.php create mode 100644 tests/Feature/BuildJobTest.php create mode 100644 tests/Feature/BuildScriptTest.php create mode 100644 tests/Feature/CallbackControllerTest.php create mode 100644 tests/Feature/CheckActivationCallbackTest.php create mode 100644 tests/Feature/CheckBuildCallbackTest.php create mode 100644 tests/Feature/CreateLoadBalancerIfNecessaryJobTest.php create mode 100644 tests/Feature/DaemonControllerTest.php create mode 100644 tests/Feature/DatabaseBackupControllerTest.php create mode 100644 tests/Feature/DatabaseBackupTest.php create mode 100644 tests/Feature/DatabaseControllerTest.php create mode 100644 tests/Feature/DatabaseIsProvisionedRuleTest.php create mode 100644 tests/Feature/DatabaseRestoreControllerTest.php create mode 100644 tests/Feature/DatabaseTest.php create mode 100644 tests/Feature/DatabaseTransferControllerTest.php create mode 100644 tests/Feature/DeleteServerOnProviderJobTest.php create mode 100644 tests/Feature/DeploymentControllerTest.php create mode 100644 tests/Feature/DeploymentTest.php create mode 100644 tests/Feature/DigitalOceanProviderTest.php create mode 100644 tests/Feature/DispatchCallbackTest.php create mode 100644 tests/Feature/EnvironmentControllerTest.php create mode 100644 tests/Feature/EnvironmentTest.php create mode 100644 tests/Feature/GitHubTest.php create mode 100644 tests/Feature/HandlesStackProvisioningFailuresTest.php create mode 100644 tests/Feature/HookControllerTest.php create mode 100644 tests/Feature/HookDeploymentControllerTest.php create mode 100644 tests/Feature/KeyControllerTest.php create mode 100644 tests/Feature/MarkAsProvisionedCallbackTest.php create mode 100644 tests/Feature/MonitorDeploymentJobTest.php create mode 100644 tests/Feature/ProjectControllerTest.php create mode 100644 tests/Feature/ProjectTest.php create mode 100644 tests/Feature/PromoteStackJobTest.php create mode 100644 tests/Feature/PromotedStackControllerTest.php create mode 100644 tests/Feature/ProviderControllerTest.php create mode 100644 tests/Feature/ProvisionAppServerScriptTest.php create mode 100644 tests/Feature/ProvisionBalancerJobTest.php create mode 100644 tests/Feature/ProvisionBalancerScriptTest.php create mode 100644 tests/Feature/ProvisionDatabaseJobTest.php create mode 100644 tests/Feature/ProvisionDatabaseScriptTest.php create mode 100644 tests/Feature/ProvisionServersJobTest.php create mode 100644 tests/Feature/ProvisionableTest.php create mode 100644 tests/Feature/ReportHelperTest.php create mode 100644 tests/Feature/RestartDaemonsJobTest.php create mode 100644 tests/Feature/RestoreDatabaseBackupJobTest.php create mode 100644 tests/Feature/RestoreDatabaseBackupScriptTest.php create mode 100644 tests/Feature/Route53Test.php create mode 100644 tests/Feature/S3Test.php create mode 100644 tests/Feature/ScheduleControllerTest.php create mode 100644 tests/Feature/SchedulerControllerTest.php create mode 100644 tests/Feature/ServerConfigurationControllerTest.php create mode 100644 tests/Feature/ServerDeploymentTest.php create mode 100644 tests/Feature/ServerTest.php create mode 100644 tests/Feature/ShellProcessRunnerTest.php create mode 100644 tests/Feature/SourceControllerTest.php create mode 100644 tests/Feature/StackControllerTest.php create mode 100644 tests/Feature/StackDatabaseControllerTest.php create mode 100644 tests/Feature/StackDeploymentTest.php create mode 100644 tests/Feature/StackServerControllerTest.php create mode 100644 tests/Feature/StackTaskControllerTest.php create mode 100644 tests/Feature/StackTaskTest.php create mode 100644 tests/Feature/StackTest.php create mode 100644 tests/Feature/StartBackgroundServicesCallbackTest.php create mode 100644 tests/Feature/StorageProviderControllerTest.php create mode 100644 tests/Feature/StoreDatabaseBackupJobTest.php create mode 100644 tests/Feature/StoreDatabaseBackupScriptTest.php create mode 100644 tests/Feature/SyncBalancerScriptTest.php create mode 100644 tests/Feature/SyncNetworkJobTest.php create mode 100644 tests/Feature/TaskTest.php create mode 100644 tests/Feature/UpdateLastAlertTimestampForCollaboratorsListenerTest.php create mode 100644 tests/Feature/UpdateStackDnsRecordsJobTest.php create mode 100644 tests/Feature/ValidDatabaseNameRuleTest.php create mode 100644 tests/Feature/ValidRepositoryRuleTest.php create mode 100644 tests/Feature/ValidServeListRuleTest.php create mode 100644 tests/Feature/ValidSourceNameRuleTest.php create mode 100644 tests/Feature/WaitForServersToFinishProvisioningJobTest.php create mode 100644 tests/TestCase.php create mode 100644 tests/Unit/ExampleTest.php create mode 100644 webpack.mix.js create mode 100644 yarn.lock diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..55b5223b --- /dev/null +++ b/.env.example @@ -0,0 +1,32 @@ +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_LOG_LEVEL=debug +APP_URL=http://localhost + +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=homestead +DB_USERNAME=homestead +DB_PASSWORD=secret + +BROADCAST_DRIVER=log +CACHE_DRIVER=file +SESSION_DRIVER=file +QUEUE_DRIVER=sync + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_DRIVER=smtp +MAIL_HOST=smtp.mailtrap.io +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null + +PUSHER_APP_ID= +PUSHER_APP_KEY= +PUSHER_APP_SECRET= diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..967315dd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto +*.css linguist-vendored +*.scss linguist-vendored +*.js linguist-vendored +CHANGELOG.md export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a6b4afc4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/node_modules +/public/storage +/public/hot +/storage/*.key +/vendor +/.idea +/.vagrant +Homestead.json +Homestead.yaml +.env diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..1782cb76 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +.PHONY: test share + +test: + php vendor/bin/phpunit + +share: + ngrok http "cloud.dev:80" -subdomain=laravel-cloud -host-header=rewrite + +fresh: + php artisan migrate:fresh + php artisan passport:install --force + rm storage/app/keys/* + +default: test diff --git a/app/Alert.php b/app/Alert.php new file mode 100644 index 00000000..6bd9a463 --- /dev/null +++ b/app/Alert.php @@ -0,0 +1,52 @@ + 'json', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array + */ + protected $guarded = []; + + /** + * The attributes that should be hidden for arrays. + * + * @var array + */ + protected $hidden = [ + 'exception', + ]; + + /** + * The event map for the model. + * + * Allows for object-based events for native Eloquent events. + * + * @var array + */ + protected $dispatchesEvents = [ + 'created' => Events\AlertCreated::class, + ]; + + /** + * Get the project that the alert belongs to. + */ + public function project() + { + return $this->belongsTo(Project::class, 'project_id'); + } +} diff --git a/app/AppServer.php b/app/AppServer.php new file mode 100644 index 00000000..0e98d827 --- /dev/null +++ b/app/AppServer.php @@ -0,0 +1,72 @@ +is($this->stack->masterServer()); + } + + /** + * Determine if this server processes queued jobs. + * + * @return bool + */ + public function isWorker() + { + return true; + } + + /** + * Determine if this server is the "master" worker for the stack. + * + * @return bool + */ + public function isMasterWorker() + { + return $this->is($this->stack->masterWorker()); + } + + /** + * Dispatch the job to provision the server. + * + * @return void + */ + public function provision() + { + ProvisionAppServer::dispatch($this); + + $this->update(['provisioning_job_dispatched_at' => Carbon::now()]); + } + + /** + * Get the provisioning script for the server. + * + * @return \App\Scripts\Script + */ + public function provisioningScript() + { + return new Scripts\ProvisionAppServer($this); + } +} diff --git a/app/AppServerRecordCreator.php b/app/AppServerRecordCreator.php new file mode 100644 index 00000000..f859fc19 --- /dev/null +++ b/app/AppServerRecordCreator.php @@ -0,0 +1,36 @@ +stack->appServers(); + } + + /** + * Get the custom attributes for the servers. + * + * @return array + */ + protected function attributes() + { + return [ + 'database_username' => 'cloud', + 'database_password' => str_random(40), + ]; + } +} diff --git a/app/Balancer.php b/app/Balancer.php new file mode 100644 index 00000000..ccad8da6 --- /dev/null +++ b/app/Balancer.php @@ -0,0 +1,144 @@ + 'boolean', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array + */ + protected $guarded = []; + + /** + * The attributes that should be hidden for arrays. + * + * @var array + */ + protected $hidden = [ + 'private_key', 'sudo_password', + ]; + + /** + * Determine if the given user can SSH into the balancer. + * + * @param \App\User $user + * @return bool + */ + public function canSsh(User $user) + { + return $user->canAccessProject($this->project); + } + + /** + * Sync the balancer's configuration with the current stacks. + * + * @param int $delay + * @return void + */ + public function sync($delay = 0) + { + Jobs\SyncBalancer::dispatch($this)->delay($delay); + } + + /** + * Sync the balancer's configuration with the current stacks. + * + * @return \App\Task + */ + public function syncNow() + { + return $this->run(new SyncBalancerScript($this)); + } + + /** + * Determine if the balancer should self-sign TLS certificates. + * + * @return bool + */ + public function selfSignsCertificates() + { + return $this->tls === 'self-signed'; + } + + /** + * Dispatch the job to provision the balancer. + * + * @return void + */ + public function provision() + { + ProvisionBalancer::dispatch($this); + + $this->update(['provisioning_job_dispatched_at' => Carbon::now()]); + } + + /** + * Run the provisioning script on the balancer. + * + * @return \App\Task|null + */ + public function runProvisioningScript() + { + if ($this->isProvisioning()) { + return; + } + + $this->markAsProvisioning(); + + return $this->runInBackground(new Scripts\ProvisionBalancer($this), [ + 'then' => [ + MarkAsProvisioned::class, + new Dispatch(SyncBalancer::class) + ], + ]); + } + + /** + * Delete the model from the database. + * + * @return bool|null + * + * @throws \Exception + */ + public function delete() + { + if ($this->address) { + UpdateStackDnsRecords::dispatch( + $this->project, $this->address->public_address + ); + } + + DeleteServerOnProvider::dispatch( + $this->project, $this->providerServerId() + ); + + $this->address()->delete(); + $this->tasks()->delete(); + + parent::delete(); + } +} diff --git a/app/CaddyBalancerConfiguration.php b/app/CaddyBalancerConfiguration.php new file mode 100644 index 00000000..7489b7a8 --- /dev/null +++ b/app/CaddyBalancerConfiguration.php @@ -0,0 +1,100 @@ +stack = $stack; + $this->domain = $domain; + $this->proxyTo = $proxyTo; + $this->balancer = $balancer; + } + + /** + * Render the Caddy configuration block. + * + * @return string + */ + public function render() + { + return view($this->script(), [ + 'canonicalDomain' => $this->stack->canonicalDomain($this->domain), + 'domain' => $this->domain, + 'tls' => $this->tls(), + 'proxyTo' => $this->proxyTo, + ])->render(); + } + + /** + * Get the script that should be used to build the configuration. + * + * @return string + */ + protected function script() + { + return ! $this->stack->isCanonicalDomain($this->domain) || Str::contains($this->domain, ':80') + ? 'scripts.caddy-configuration.redirect' + : 'scripts.caddy-configuration.proxy'; + } + + /** + * Get the TLS configuration block for the script. + * + * @return string + */ + protected function tls() + { + if (Str::contains($this->domain, ':80')) { + return 'tls off'; + } + + if ($this->balancer->selfSignsCertificates()) { + return 'tls self_signed'; + } + + return 'tls { + max_certs 1 + }'; + } +} diff --git a/app/CaddyServerConfiguration.php b/app/CaddyServerConfiguration.php new file mode 100644 index 00000000..cbbe3ff9 --- /dev/null +++ b/app/CaddyServerConfiguration.php @@ -0,0 +1,102 @@ +server = $server; + $this->domain = $domain; + } + + /** + * Render the Caddy configuration block. + * + * @return string + */ + public function render() + { + return view($this->script(), [ + 'canonicalDomain' => $this->server->stack->canonicalDomain($this->domain), + 'domain' => $this->domain, + 'root' => $this->root(), + 'tls' => $this->tls(), + 'index' => ! Str::contains($this->domain, '.laravel.build'), + ])->render(); + } + + /** + * Get the script that should be used to build the configuration. + * + * @return string + */ + protected function script() + { + if (! $this->server->stack->isCanonicalDomain($this->domain) || + Str::contains($this->domain, ':80')) { + return 'scripts.caddy-configuration.redirect'; + } + + return 'scripts.caddy-configuration.app'; + } + + /** + * Get the application root directory. + * + * @return string + */ + protected function root() + { + if ($this->server->stack->under_maintenance && + ! Str::contains($this->domain, 'laravel.build')) { + return '/home/cloud/maintenance/public'; + } + + return '/home/cloud/app/public'; + } + + /** + * Get the TLS configuration block for the script. + * + * @return string + */ + protected function tls() + { + if (Str::contains($this->domain, ':80')) { + return 'tls off'; + } + + if ($this->server->stack->balanced || + $this->server->selfSignsCertificates()) { + return 'tls self_signed'; + } + + return 'tls { + max_certs 1 + }'; + } +} diff --git a/app/Callbacks/CheckActivation.php b/app/Callbacks/CheckActivation.php new file mode 100644 index 00000000..9f6f53ca --- /dev/null +++ b/app/Callbacks/CheckActivation.php @@ -0,0 +1,42 @@ +id = $id; + } + + /** + * Handle the callback. + * + * @param Task $task + * @return void + */ + public function handle(Task $task) + { + if ($deployment = ServerDeployment::find($this->id)) { + $task->successful() + ? $deployment->markAsActivated() + : $deployment->markAsFailed(); + } + } +} diff --git a/app/Callbacks/CheckBuild.php b/app/Callbacks/CheckBuild.php new file mode 100644 index 00000000..8b117bc7 --- /dev/null +++ b/app/Callbacks/CheckBuild.php @@ -0,0 +1,42 @@ +id = $id; + } + + /** + * Handle the callback. + * + * @param Task $task + * @return void + */ + public function handle(Task $task) + { + if ($deployment = ServerDeployment::find($this->id)) { + $task->successful() + ? $deployment->markAsBuilt() + : $deployment->markAsFailed(); + } + } +} diff --git a/app/Callbacks/CheckDatabaseBackup.php b/app/Callbacks/CheckDatabaseBackup.php new file mode 100644 index 00000000..c2213248 --- /dev/null +++ b/app/Callbacks/CheckDatabaseBackup.php @@ -0,0 +1,44 @@ +id = $id; + } + + /** + * Handle the callback. + * + * @param Task $task + * @return void + */ + public function handle(Task $task) + { + if ($backup = DatabaseBackup::find($this->id)) { + $backup->updateSize(); + + $task->successful() + ? $backup->markAsFinished($task->output) + : $backup->markAsFailed($task->exit_code, $task->output); + } + } +} diff --git a/app/Callbacks/CheckDatabaseRestore.php b/app/Callbacks/CheckDatabaseRestore.php new file mode 100644 index 00000000..63e6ad83 --- /dev/null +++ b/app/Callbacks/CheckDatabaseRestore.php @@ -0,0 +1,42 @@ +id = $id; + } + + /** + * Handle the callback. + * + * @param Task $task + * @return void + */ + public function handle(Task $task) + { + if ($restore = DatabaseRestore::find($this->id)) { + $task->successful() + ? $restore->markAsFinished($task->output) + : $restore->markAsFailed($task->exit_code, $task->output); + } + } +} diff --git a/app/Callbacks/CheckServerTask.php b/app/Callbacks/CheckServerTask.php new file mode 100644 index 00000000..15359a55 --- /dev/null +++ b/app/Callbacks/CheckServerTask.php @@ -0,0 +1,44 @@ +id = $id; + } + + /** + * Handle the callback. + * + * @param Task $task + * @return void + */ + public function handle(Task $task) + { + if ($serverTask = ServerTask::find($this->id)) { + if ($serverTask->task->successful()) { + $serverTask->markAsFinished(); + } else { + $serverTask->markAsFailed(); + } + } + } +} diff --git a/app/Callbacks/Dispatch.php b/app/Callbacks/Dispatch.php new file mode 100644 index 00000000..8417d82c --- /dev/null +++ b/app/Callbacks/Dispatch.php @@ -0,0 +1,41 @@ +class = $class; + } + + /** + * Handle the callback. + * + * @param Task $task + * @return void + */ + public function handle(Task $task) + { + if ($task->provisionable) { + $class = $this->class; + + dispatch(new $class($task->provisionable)); + } + } +} diff --git a/app/Callbacks/MarkAsProvisioned.php b/app/Callbacks/MarkAsProvisioned.php new file mode 100644 index 00000000..08ed4d31 --- /dev/null +++ b/app/Callbacks/MarkAsProvisioned.php @@ -0,0 +1,21 @@ +provisionable) { + $task->provisionable->markAsProvisioned(); + } + } +} diff --git a/app/Callbacks/StartBackgroundServices.php b/app/Callbacks/StartBackgroundServices.php new file mode 100644 index 00000000..ab1da3a9 --- /dev/null +++ b/app/Callbacks/StartBackgroundServices.php @@ -0,0 +1,57 @@ +id = $id; + } + + /** + * Handle the callback. + * + * @param Task $task + * @return void + */ + public function handle(Task $task) + { + if (! $deployment = ServerDeployment::find($this->id)) { + return; + } + + $task->successful() && $this->shouldStartBackgroundServices($deployment) + ? $deployment->deployable->startBackgroundServices() + : $deployment->deployable->markDaemonsAsStopped(); + } + + /** + * Determine if daemons and schedulers should wait to start. + * + * @param \App\ServerDeployment $deployment + * @return bool + */ + protected function shouldStartBackgroundServices(ServerDeployment $deployment) + { + return $deployment->deployable->daemonsArePending() + ? ! $deployment->isProduction() + : $deployment->deployable->daemonsAreRunning(); + } +} diff --git a/app/Certificate.php b/app/Certificate.php new file mode 100644 index 00000000..21d80a2d --- /dev/null +++ b/app/Certificate.php @@ -0,0 +1,45 @@ +belongsTo(Project::class, 'project_id'); + } + + /** + * Determine if this certificate is active. + * + * @return bool + */ + public function active() + { + return $this->project->activeCertificates()->contains(function ($certificate) { + return $certificate->id === $this->id; + }); + } +} diff --git a/app/Collaborator.php b/app/Collaborator.php new file mode 100644 index 00000000..3165421a --- /dev/null +++ b/app/Collaborator.php @@ -0,0 +1,17 @@ + 'json', + ]; +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php new file mode 100644 index 00000000..fe69a34a --- /dev/null +++ b/app/Console/Kernel.php @@ -0,0 +1,43 @@ +call(function () { + PruneTasks::dispatch(); + PruneStackTasks::dispatch(); + })->hourly(); + } + + /** + * Register the Closure based commands for the application. + * + * @return void + */ + protected function commands() + { + require base_path('routes/console.php'); + } +} diff --git a/app/Contracts/Alertable.php b/app/Contracts/Alertable.php new file mode 100644 index 00000000..9b94df3c --- /dev/null +++ b/app/Contracts/Alertable.php @@ -0,0 +1,13 @@ +belongsTo(Stack::class, 'stack_id'); + } +} diff --git a/app/Database.php b/app/Database.php new file mode 100644 index 00000000..9d6f41de --- /dev/null +++ b/app/Database.php @@ -0,0 +1,247 @@ + 'json', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array + */ + protected $guarded = []; + + /** + * The attributes that should be hidden for arrays. + * + * @var array + */ + protected $hidden = [ + 'private_key', 'sudo_password', 'password', + ]; + + /** + * Get the project that the database belongs to. + */ + public function project() + { + return $this->belongsTo(Project::class, 'project_id'); + } + + /** + * The database backups associated with the database. + */ + public function backups() + { + return $this->hasMany(DatabaseBackup::class)->latest('id'); + } + + /** + * The database restores associated with the database. + */ + public function restores() + { + return $this->hasMany(DatabaseRestore::class)->latest('id'); + } + + /** + * Get all of the stacks attached to the database. + */ + public function stacks() + { + return $this->belongsToMany( + Stack::class, 'stack_databases', 'database_id', 'stack_id' + ); + } + + /** + * Determine if the given user can SSH into the balancer. + * + * @param \App\User $user + * @return bool + */ + public function canSsh(User $user) + { + return $user->canAccessProject($this->project); + } + + /** + * Sync the network for the database. + * + * @param int $delay + * @return void + */ + public function syncNetwork($delay = 0) + { + SyncNetwork::dispatch($this)->delay($delay); + } + + /** + * Determine if the database's network is synced. + * + * @return bool + */ + public function networkIsSynced() + { + return count(array_diff( + $this->shouldAllowAccessFrom(), + $this->allows_access_from + )) === 0; + } + + /** + * Determine if the database allows access from a given IP address. + * + * @param object|string $address + * @return bool + */ + public function allowsAccessFrom($address) + { + return in_array( + is_object($address) ? $address->address->public_address : $address, + $this->allows_access_from + ); + } + + /** + * Get all of the IP addresses the database should allow access from. + * + * @return array + */ + public function shouldAllowAccessFrom() + { + return $this->stacks->flatMap(function ($stack) { + return $stack->allIpAddresses(); + })->all(); + } + + /** + * Get a network lock instance for the provisionable. + * + * @return \Illuminate\Contracts\Cache\Lock + */ + public function networkLock() + { + return Cache::store('redis')->lock('ufw:'.$this->id, 30); + } + + /** + * Create a new backup of the database. + * + * @param \App\StorageProvider $provider + * @param string $databaseName + * @return \App\DatabaseBackup + */ + public function backup(StorageProvider $provider, $databaseName) + { + return tap($this->backups()->create([ + 'storage_provider_id' => $provider->id, + 'database_name' => $databaseName, + 'backup_path' => DatabaseBackup::newPathFor($this->project), + 'status' => 'pending', + 'output' => '', + ]), function ($backup) { + $this->trimBackups($backup); + + StoreDatabaseBackup::dispatch($backup); + }); + } + + /** + * Trim the backups for a given database. + * + * @param \App\DatabaseBackup $backup + * @return void + */ + protected function trimBackups(DatabaseBackup $backup) + { + $backups = $this->backups()->where( + 'database_name', $backup->database_name + )->get(); + + if (count($backups) > 20) { + $backups->slice(20 - count($backups))->each->delete(); + } + } + + /** + * Get the environment variable name for the database. + * + * @return string + */ + public function variableName() + { + return strtoupper(str_replace('-', '_', $this->name)); + } + + /** + * Dispatch the job to provision the server. + * + * @return void + */ + public function provision() + { + ProvisionDatabase::dispatch($this); + + $this->update(['provisioning_job_dispatched_at' => Carbon::now()]); + } + + /** + * Run the provisioning script on the server. + * + * @return \App\Task|null + */ + public function runProvisioningScript() + { + if ($this->isProvisioning()) { + return; + } + + $this->markAsProvisioning(); + + return $this->runInBackground(new Scripts\ProvisionDatabase($this), [ + 'then' => [MarkAsProvisioned::class], + ]); + } + + /** + * Delete the model from the database. + * + * @return bool|null + * + * @throws \Exception + */ + public function delete() + { + DeleteServerOnProvider::dispatch( + $this->project, $this->providerServerId() + ); + + $this->stacks()->detach(); + $this->address()->delete(); + $this->tasks()->delete(); + + parent::delete(); + } +} diff --git a/app/DatabaseBackup.php b/app/DatabaseBackup.php new file mode 100644 index 00000000..4ef37314 --- /dev/null +++ b/app/DatabaseBackup.php @@ -0,0 +1,204 @@ +belongsTo(Database::class, 'database_id'); + } + + /** + * Get the storage provider used for the backup. + */ + public function storageProvider() + { + return $this->belongsTo(StorageProvider::class, 'storage_provider_id'); + } + + /** + * Get the database restores for this backup. + */ + public function restores() + { + return $this->hasMany(DatabaseRestore::class, 'database_backup_id'); + } + + /** + * Generate a new backup path for the given project. + * + * @param \App\Project $project + * @return string + */ + public static function newPathFor(Project $project) + { + return sprintf( + 'backups/%s/%s.sql.gz', + Str::lower(Str::snake($project->name)), + Carbon::now()->format('Y-m-d-H-i-s') + ); + } + + /** + * Get the configuration script for the storage provider. + * + * @return string + */ + public function configurationScript() + { + return $this->storageProvider->client()->configurationScript(); + } + + /** + * Get the upload script for the storage provider. + * + * @return string + */ + public function uploadScript() + { + return $this->storageProvider->client()->uploadScript($this); + } + + /** + * Get the download script for the storage provider. + * + * @return string + */ + public function downloadScript() + { + return $this->storageProvider->client()->downloadScript($this); + } + + /** + * Mark the database backup as running. + * + * @return void + */ + public function markAsRunning() + { + DatabaseBackupRunning::dispatch(tap($this)->update([ + 'status' => 'running', + ])); + } + + /** + * Update the size of the backup. + * + * @return void + */ + public function updateSize() + { + $this->update([ + 'size' => $this->storageProvider->client()->size($this->backup_path) + ]); + } + + /** + * Mark the database backup as finished. + * + * @param string $output + * @return void + */ + public function markAsFinished($output = '') + { + DatabaseBackupFinished::dispatch(tap($this)->update([ + 'status' => 'finished', + 'exit_code' => 0, + 'output' => $output, + ])); + } + + /** + * Restore the database from the backup. + * + * @return \App\DatabaseRestore + */ + public function restore() + { + return tap($this->restores()->create([ + 'database_id' => $this->database->id, + 'database_name' => $this->database_name, + 'status' => 'pending', + 'output' => '', + ]), function ($restore) { + $this->trimRestores(); + + RestoreDatabaseBackup::dispatch($restore); + }); + } + + /** + * Trim the database restores for the backup. + * + * @return void + */ + protected function trimRestores() + { + $restores = $this->restores()->get(); + + if (count($restores) > 20) { + $restores->slice(20 - count($restores))->each->delete(); + } + } + + /** + * Mark the database backup as failed. + * + * @param int $exitCode + * @param string $output + * @return void + */ + public function markAsFailed($exitCode, $output = '') + { + DatabaseBackupFailed::dispatch(tap($this)->update([ + 'status' => 'failed', + 'exit_code' => $exitCode, + 'output' => $output, + ])); + } + + /** + * Delete the model from the database. + * + * @return bool|null + * + * @throws \Exception + */ + public function delete() + { + DeleteDatabaseBackup::dispatch( + $this->storageProvider, $this->backup_path + ); + + parent::delete(); + } +} diff --git a/app/DatabaseRestore.php b/app/DatabaseRestore.php new file mode 100644 index 00000000..a021f2a0 --- /dev/null +++ b/app/DatabaseRestore.php @@ -0,0 +1,86 @@ +belongsTo(Database::class, 'database_id'); + } + + /** + * Get the database backup the restore belongs to. + */ + public function backup() + { + return $this->belongsTo(DatabaseBackup::class, 'database_backup_id'); + } + + /** + * Mark the database restore as running. + * + * @return void + */ + public function markAsRunning() + { + DatabaseRestoreRunning::dispatch(tap($this)->update([ + 'status' => 'running', + ])); + } + + /** + * Mark the database restore as finished. + * + * @param string $output + * @return void + */ + public function markAsFinished($output = '') + { + DatabaseRestoreFinished::dispatch(tap($this)->update([ + 'status' => 'finished', + 'exit_code' => 0, + 'output' => $output, + ])); + } + + /** + * Mark the database restore as failed. + * + * @param int $exitCode + * @param string $output + * @return void + */ + public function markAsFailed($exitCode, $output = '') + { + DatabaseRestoreFailed::dispatch(tap($this)->update([ + 'status' => 'failed', + 'exit_code' => $exitCode, + 'output' => $output, + ])); + } +} diff --git a/app/Deployment.php b/app/Deployment.php new file mode 100644 index 00000000..0fb3e51e --- /dev/null +++ b/app/Deployment.php @@ -0,0 +1,417 @@ + 'boolean', + 'build_commands' => 'json', + 'activation_commands' => 'json', + 'directories' => 'json', + 'daemons' => 'json', + 'schedule' => 'json', + 'meta' => 'json', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array + */ + protected $guarded = []; + + /** + * Get the project for the deploymnet. + * + * @return \App\Project + */ + public function project() + { + return $this->stack->environment->project; + } + + /** + * Get the source control provider for the deploymnet. + * + * @return \App\SourceProvider + */ + public function sourceProvider() + { + return $this->project()->sourceProvider; + } + + /** + * Get the repository for the deployment. + * + * @return string + */ + public function repository() + { + return $this->project()->repository; + } + + /** + * Determine if the deployment is for a production environment. + * + * @return bool + */ + public function isProduction() + { + return $this->stack->environment->isProduction(); + } + + /** + * Get the stack the deployment belongs to. + */ + public function stack() + { + return $this->belongsTo(Stack::class, 'stack_id'); + } + + /** + * Get the user who initiated the deployment, if applicable. + */ + public function initiator() + { + return $this->belongsTo(User::class, 'initiator_id'); + } + + /** + * Get all of the individual server deployments. + */ + public function serverDeployments() + { + return $this->hasMany(ServerDeployment::class); + } + + /** + * Get the tarball URL for the deployment. + * + * @return string + */ + public function hash() + { + return $this->commit_hash; + } + + /** + * Get the tarball URL for the deployment. + * + * @return string + */ + public function tarballUrl() + { + return $this->sourceProvider()->client()->tarballUrl($this); + } + + /** + * Get the UNIX timestamp of the deployment's creation date. + * + * @return int + */ + public function timestamp() + { + return $this->created_at->getTimestamp(); + } + + /** + * Start monitoring this deployment. + * + * @return void + */ + public function monitor() + { + MonitorDeployment::dispatch($this); + + TimeOutDeploymentIfStillRunning::dispatch($this)->delay( + Carbon::now()->addMinutes(40) + ); + } + + /** + * Determine if the deployment is still pending. + * + * @return bool + */ + public function isPending() + { + return $this->status == 'pending'; + } + + /** + * Determine if all of the server deployments have built. + * + * @return bool + */ + public function isBuilt() + { + return $this->serverDeployments->every->isBuilt(); + } + + /** + * Determine if any of the server deployments are still building. + * + * @return bool + */ + public function isBuilding() + { + return $this->serverDeployments->contains->isBuilding(); + } + + /** + * Build a fresh release of the project. + * + * @return void + */ + public function build() + { + $this->update(['status' => 'building']); + + $this->createServerDeployments()->each->build(); + + DeploymentBuilding::dispatch($this); + } + + /** + * Create the server deployments for this deployment. + * + * @return void + */ + protected function createServerDeployments() + { + return $this->stack->allServers()->map(function ($server) { + return $this->serverDeployments()->create([ + 'deployable_id' => $server->id, + 'deployable_type' => get_class($server), + 'build_commands' => $this->buildCommandsFor($server)->all(), + 'activation_commands' => $this->activationCommandsFor($server)->all(), + 'status' => 'building', + ]); + }); + } + + /** + * Get all of the build commands as a collection of objects. + * + * @return \Illuminate\Support\Collection + */ + public function buildCommands() + { + return collect($this->build_commands)->mapInto(ShellCommand::class); + } + + /** + * Get the build commands for the given server. + * + * @param \App\Server $server + * @return \Illuminate\Support\Collection + */ + protected function buildCommandsFor($server) + { + return $this->buildCommands()->filter->appliesTo($server)->reject->prefixed( + ! $server->isMaster() ? 'once:' : null + )->map->trim()->values(); + } + + /** + * Get all of the activation commands as a collection of objects. + * + * @return \Illuminate\Support\Collection + */ + public function activationCommands() + { + return collect($this->activation_commands)->mapInto(ShellCommand::class); + } + + /** + * Get the activation commands for the given server. + * + * @param \App\Server $server + * @return \Illuminate\Support\Collection + */ + protected function activationCommandsFor($server) + { + return $this->activationCommands()->filter->appliesTo($server)->reject->prefixed( + ! $server->isMaster() ? 'once:' : null + )->map->trim()->values(); + } + + /** + * Determine if all of the server deployments have activated. + * + * @return bool + */ + public function isActivated() + { + return $this->serverDeployments->count() > 0 && + $this->serverDeployments->every->isActivated(); + } + + /** + * Determine if the deployment is activating. + * + * @return bool + */ + public function isActivating() + { + return $this->status == 'activating'; + } + + /** + * Activate the deployed code across all servers. + * + * @return void + */ + public function activate() + { + $this->update([ + 'activated' => true, + 'status' => 'activating', + ]); + + $this->serverDeployments->each->activate(); + + DeploymentActivating::dispatch($this); + } + + /** + * Determine if the deployment is "finished". + * + * @return bool + */ + public function isFinished() + { + return $this->status == 'finished'; + } + + /** + * Mark the deployment as finished. + * + * @return void + */ + public function markAsFinished() + { + $this->update(['status' => 'finished']); + + DeploymentFinished::dispatch($this); + } + + /** + * Determine if the deployment has been marked as timed out. + * + * @return bool + */ + public function isTimedOut() + { + return $this->status == 'timeout'; + } + + /** + * Mark the deployment as timed out. + * + * @return void + */ + public function markAsTimedOut() + { + $this->update(['status' => 'timeout']); + + DeploymentTimedOut::dispatch($this); + } + + /** + * Determine if the deployment has been marked as failed. + * + * @return bool + */ + public function hasFailed() + { + return $this->status == 'failed'; + } + + /** + * Determine if the deployment has any failed server deployments. + * + * @return bool + */ + public function hasFailures() + { + return $this->serverDeployments->filter->hasFailed()->isNotEmpty(); + } + + /** + * Mark the deployment as failed. + * + * @param \Exception|null $exception + * @return void + */ + public function markAsFailed($exception = null) + { + $this->update(['status' => 'failed']); + + DeploymentFailed::dispatch($this, $exception); + } + + /** + * Determine if the deployment has been cancelled. + * + * @return bool + */ + public function wasCancelled() + { + return $this->status == 'cancelled'; + } + + /** + * Cancel the deployment. + * + * @return bool + */ + public function cancel() + { + if ($this->hasEnded() || $this->isActivating()) { + return false; + } + + $this->update([ + 'status' => 'cancelled', + ]); + + DeploymentCancelled::dispatch($this); + + return true; + } + + /** + * Determine if the deployment has completed in some way. + * + * @return bool + */ + public function hasEnded() + { + return $this->isActivated() || + $this->isTimedOut() || + $this->hasFailed() || + $this->wasCancelled(); + } +} diff --git a/app/DeploymentInstructions.php b/app/DeploymentInstructions.php new file mode 100644 index 00000000..c7d71a70 --- /dev/null +++ b/app/DeploymentInstructions.php @@ -0,0 +1,169 @@ +hash = $hash; + $this->build = $build; + $this->activate = $activate; + $this->directories = $directories; + + $this->daemons = $this->filterDaemons($daemons); + $this->schedule = $this->filterSchedule($schedule); + } + + /** + * Create new deployment instructions from a request. + * + * @param \App\Http\Requests\CreateDeploymentRequest $request + * @return static + */ + public static function fromRequest(CreateDeploymentRequest $request) + { + $project = $request->stack->project(); + + $hash = $request->hash ?: $project->sourceProvider->client()->latestHashFor( + $project->repository, $request->branch + ); + + return new static( + $hash, $request->build ?? [], $request->activate ?? [], + $request->directories ?? [], $request->daemons() ?? [], + $request->schedule() ?? [] + ); + } + + /** + * Create new deployment instructions from the given hook and hash. + * + * @param \App\Hook $hook + * @param string|null $hash + * @return static + */ + public static function fromHookCommit(Hook $hook, $hash) + { + $client = $hook->sourceProvider()->client(); + + $manifest = YamlParser::parse($client->manifest( + $hook->stack, $hook->repository(), $hash + )); + + return new static( + $hash, $manifest['build'] ?? [], $manifest['activate'] ?? [], + $manifest['directories'] ?? [], static::daemons($manifest), + static::schedule($manifest) + ); + } + + /** + * Create new deployment instructions from the given hook. + * + * @param \App\Hook $hook + * @return static + */ + public static function forLatestHookCommit(Hook $hook) + { + $client = $hook->sourceProvider()->client(); + + return static::fromHookCommit( + $hook, $client->latestHashFor($hook->repository(), $hook->branch) + ); + } + + /** + * Extract the daemons from the given manifest array. + * + * @param array $manifest + * @return array + */ + public static function daemons(array $manifest) + { + if (isset($manifest['app'])) { + return $manifest['app']['daemons'] ?? []; + } elseif (isset($manifest['worker'])) { + return $manifest['worker']['daemons'] ?? []; + } + + return []; + } + + /** + * Extract the scheduled tasks from the given manifest array. + * + * @param array $manifest + * @return array + */ + public static function schedule(array $manifest) + { + if (isset($manifest['app'])) { + return $manifest['app']['schedule'] ?? []; + } elseif (isset($manifest['worker'])) { + return $manifest['worker']['schedule'] ?? []; + } + + return []; + } +} diff --git a/app/DeterminesAge.php b/app/DeterminesAge.php new file mode 100644 index 00000000..91aa1948 --- /dev/null +++ b/app/DeterminesAge.php @@ -0,0 +1,20 @@ +{$attribute}->lte(Carbon::now()->subMinutes(10)); + } +} diff --git a/app/Environment.php b/app/Environment.php new file mode 100644 index 00000000..3f7547ca --- /dev/null +++ b/app/Environment.php @@ -0,0 +1,134 @@ +belongsTo(Project::class, 'project_id'); + } + + /** + * Get the creator of the environment. + */ + public function creator() + { + return $this->belongsTo(User::class, 'creator_id'); + } + + /** + * Get the promoted stack for the environment. + * + * @return Stack + */ + public function promotedStack() + { + return $this->stacks->first->promoted; + } + + /** + * Get all of the stacks for the environment. + */ + public function stacks() + { + return $this->hasMany(Stack::class); + } + + /** + * Promote the given stack so that it serves production URLs. + * + * @param \App\Stack $stack + * @param array $options + * @return bool + */ + public function promote(Stack $stack, array $options = []) + { + if (! $stack->promotable()) { + return false; + } + + PromoteStack::dispatch($stack, $options); + + return true; + } + + /** + * Mark the given stack as promoted. + * + * @param \App\Stack $stack + * @return void + */ + public function markAsPromoted(Stack $stack) + { + $this->stacks()->update(['promoted' => false]); + + $this->stacks()->where('id', $stack->id)->update(['promoted' => true]); + } + + /** + * Get the lock needed to promote stacks. + * + * @return \Illuminate\Contracts\Cache\Lock + */ + public function promotionLock() + { + return Cache::store('redis')->lock('promote:'.$this->id, 180); + } + + /** + * Determine if this is the production environment. + * + * @return bool + */ + public function isProduction() + { + return $this->name == 'production'; + } + + /** + * Get the provider gateway for the environment. + * + * @return mixed + */ + public function withProvider() + { + return $this->project->withProvider(); + } + + /** + * Delete the environment. + * + * @return void + */ + public function delete() + { + $this->stacks->each->delete(); + + return parent::delete(); + } +} diff --git a/app/Events/AlertCreated.php b/app/Events/AlertCreated.php new file mode 100644 index 00000000..e4d380b9 --- /dev/null +++ b/app/Events/AlertCreated.php @@ -0,0 +1,42 @@ +alert = $alert; + } + + /** + * Get the user IDs affected by this alert. + * + * @return array + */ + public function affectedIds() + { + return collect([$this->alert->project->user])->merge( + $this->alert->project->collaborators + )->pluck('id')->all(); + } +} diff --git a/app/Events/DatabaseBackupFailed.php b/app/Events/DatabaseBackupFailed.php new file mode 100644 index 00000000..5eccd5b7 --- /dev/null +++ b/app/Events/DatabaseBackupFailed.php @@ -0,0 +1,31 @@ +backup = $backup; + } +} diff --git a/app/Events/DatabaseBackupFinished.php b/app/Events/DatabaseBackupFinished.php new file mode 100644 index 00000000..3cb70d96 --- /dev/null +++ b/app/Events/DatabaseBackupFinished.php @@ -0,0 +1,31 @@ +backup = $backup; + } +} diff --git a/app/Events/DatabaseBackupRunning.php b/app/Events/DatabaseBackupRunning.php new file mode 100644 index 00000000..be261c10 --- /dev/null +++ b/app/Events/DatabaseBackupRunning.php @@ -0,0 +1,31 @@ +backup = $backup; + } +} diff --git a/app/Events/DatabaseRestoreFailed.php b/app/Events/DatabaseRestoreFailed.php new file mode 100644 index 00000000..0532dc57 --- /dev/null +++ b/app/Events/DatabaseRestoreFailed.php @@ -0,0 +1,31 @@ +restore = $restore; + } +} diff --git a/app/Events/DatabaseRestoreFinished.php b/app/Events/DatabaseRestoreFinished.php new file mode 100644 index 00000000..bb8e358f --- /dev/null +++ b/app/Events/DatabaseRestoreFinished.php @@ -0,0 +1,31 @@ +restore = $restore; + } +} diff --git a/app/Events/DatabaseRestoreRunning.php b/app/Events/DatabaseRestoreRunning.php new file mode 100644 index 00000000..74a460fa --- /dev/null +++ b/app/Events/DatabaseRestoreRunning.php @@ -0,0 +1,31 @@ +restore = $restore; + } +} diff --git a/app/Events/DeploymentActivating.php b/app/Events/DeploymentActivating.php new file mode 100644 index 00000000..135174e1 --- /dev/null +++ b/app/Events/DeploymentActivating.php @@ -0,0 +1,43 @@ +deployment = $deployment; + } + + /** + * Get the channels the event should broadcast on. + * + * @return Channel|array + */ + public function broadcastOn() + { + return new PrivateChannel('channel-name'); + } +} diff --git a/app/Events/DeploymentBuilding.php b/app/Events/DeploymentBuilding.php new file mode 100644 index 00000000..2793209b --- /dev/null +++ b/app/Events/DeploymentBuilding.php @@ -0,0 +1,43 @@ +deployment = $deployment; + } + + /** + * Get the channels the event should broadcast on. + * + * @return Channel|array + */ + public function broadcastOn() + { + return new PrivateChannel('channel-name'); + } +} diff --git a/app/Events/DeploymentCancelled.php b/app/Events/DeploymentCancelled.php new file mode 100644 index 00000000..ea7092e9 --- /dev/null +++ b/app/Events/DeploymentCancelled.php @@ -0,0 +1,61 @@ +deployment = $deployment; + } + + /** + * Get the stack instance for the object. + * + * @return \App\Stack + */ + public function stack() + { + return $this->deployment->stack; + } + + /** + * Create an alert for the given instance. + * + * @return \App\Alert + */ + public function toAlert() + { + return $this->deployment->project()->alerts()->create([ + 'stack_id' => $this->deployment->stack->id, + 'level' => 'info', + 'type' => 'DeploymentCancelled', + 'exception' => '', + 'meta' => [ + 'deployment_id' => $this->deployment->id + ], + ]); + } +} diff --git a/app/Events/DeploymentFailed.php b/app/Events/DeploymentFailed.php new file mode 100644 index 00000000..40a860e6 --- /dev/null +++ b/app/Events/DeploymentFailed.php @@ -0,0 +1,63 @@ +exception = $exception; + $this->deployment = $deployment; + } + + /** + * Get the stack instance for the object. + * + * @return \App\Stack + */ + public function stack() + { + return $this->deployment->stack; + } + + /** + * Create an alert for the given instance. + * + * @return \App\Alert + */ + public function toAlert() + { + return $this->deployment->project()->alerts()->create([ + 'stack_id' => $this->deployment->stack->id, + 'type' => 'DeploymentFailed', + 'exception' => (string) ($this->exception ?? ''), + 'meta' => [ + 'deployment_id' => $this->deployment->id + ], + ]); + } +} diff --git a/app/Events/DeploymentFinished.php b/app/Events/DeploymentFinished.php new file mode 100644 index 00000000..880573b5 --- /dev/null +++ b/app/Events/DeploymentFinished.php @@ -0,0 +1,77 @@ +deployment = $deployment; + } + + /** + * Get the stack instance for the object. + * + * @return \App\Stack + */ + public function stack() + { + return $this->deployment->stack; + } + + /** + * Create an alert for the given instance. + * + * @return \App\Alert + */ + public function toAlert() + { + return $this->deployment->project()->alerts()->create([ + 'stack_id' => $this->deployment->stack->id, + 'level' => 'success', + 'type' => 'DeploymentFinished', + 'exception' => '', + 'meta' => [ + 'deployment_id' => $this->deployment->id, + 'repository' => $this->deployment->repository(), + 'commit_hash' => $this->deployment->commit_hash, + ], + ]); + } + + /** + * Get the channels the event should broadcast on. + * + * @return Channel|array + */ + public function broadcastOn() + { + return new Channel( + 'stack.'.$this->deployment->stack->id + ); + } +} diff --git a/app/Events/DeploymentTimedOut.php b/app/Events/DeploymentTimedOut.php new file mode 100644 index 00000000..b7887fe0 --- /dev/null +++ b/app/Events/DeploymentTimedOut.php @@ -0,0 +1,60 @@ +deployment = $deployment; + } + + /** + * Get the stack instance for the object. + * + * @return \App\Stack + */ + public function stack() + { + return $this->deployment->stack; + } + + /** + * Create an alert for the given instance. + * + * @return \App\Alert + */ + public function toAlert() + { + return $this->deployment->project()->alerts()->create([ + 'stack_id' => $this->deployment->stack->id, + 'type' => 'DeploymentTimedOut', + 'exception' => '', + 'meta' => [ + 'deployment_id' => $this->deployment->id + ], + ]); + } +} diff --git a/app/Events/ProjectShared.php b/app/Events/ProjectShared.php new file mode 100644 index 00000000..65186d68 --- /dev/null +++ b/app/Events/ProjectShared.php @@ -0,0 +1,39 @@ +user = $user; + $this->project = $project; + } +} diff --git a/app/Events/ProjectUnshared.php b/app/Events/ProjectUnshared.php new file mode 100644 index 00000000..aa86777f --- /dev/null +++ b/app/Events/ProjectUnshared.php @@ -0,0 +1,39 @@ +user = $user; + $this->project = $project; + } +} diff --git a/app/Events/ServerDeploymentActivated.php b/app/Events/ServerDeploymentActivated.php new file mode 100644 index 00000000..9c9905bc --- /dev/null +++ b/app/Events/ServerDeploymentActivated.php @@ -0,0 +1,31 @@ +deployment = $deployment; + } +} diff --git a/app/Events/ServerDeploymentBuilt.php b/app/Events/ServerDeploymentBuilt.php new file mode 100644 index 00000000..768457e2 --- /dev/null +++ b/app/Events/ServerDeploymentBuilt.php @@ -0,0 +1,31 @@ +deployment = $deployment; + } +} diff --git a/app/Events/ServerDeploymentFailed.php b/app/Events/ServerDeploymentFailed.php new file mode 100644 index 00000000..82223443 --- /dev/null +++ b/app/Events/ServerDeploymentFailed.php @@ -0,0 +1,31 @@ +deployment = $deployment; + } +} diff --git a/app/Events/ServerTaskFailed.php b/app/Events/ServerTaskFailed.php new file mode 100644 index 00000000..97de8c5c --- /dev/null +++ b/app/Events/ServerTaskFailed.php @@ -0,0 +1,31 @@ +task = $task; + } +} diff --git a/app/Events/ServerTaskFinished.php b/app/Events/ServerTaskFinished.php new file mode 100644 index 00000000..21244cf3 --- /dev/null +++ b/app/Events/ServerTaskFinished.php @@ -0,0 +1,31 @@ +task = $task; + } +} diff --git a/app/Events/StackDeleting.php b/app/Events/StackDeleting.php new file mode 100644 index 00000000..bbfa0fb9 --- /dev/null +++ b/app/Events/StackDeleting.php @@ -0,0 +1,48 @@ +stack = $stack; + } + + /** + * Create an alert for the given instance. + * + * @return \App\Alert + */ + public function toAlert() + { + return $this->stack->project()->alerts()->create([ + 'stack_id' => $this->stack->id, + 'level' => 'info', + 'type' => 'StackDeleted', + 'exception' => '', + 'meta' => [], + ]); + } +} diff --git a/app/Events/StackProvisioned.php b/app/Events/StackProvisioned.php new file mode 100644 index 00000000..be98b019 --- /dev/null +++ b/app/Events/StackProvisioned.php @@ -0,0 +1,48 @@ +stack = $stack; + } + + /** + * Create an alert for the given instance. + * + * @return \App\Alert + */ + public function toAlert() + { + return $this->stack->project()->alerts()->create([ + 'stack_id' => $this->stack->id, + 'level' => 'success', + 'type' => 'StackProvisioned', + 'exception' => '', + 'meta' => [], + ]); + } +} diff --git a/app/Events/StackProvisioning.php b/app/Events/StackProvisioning.php new file mode 100644 index 00000000..9c50c541 --- /dev/null +++ b/app/Events/StackProvisioning.php @@ -0,0 +1,43 @@ +stack = $stack; + } + + /** + * Get the channels the event should broadcast on. + * + * @return Channel|array + */ + public function broadcastOn() + { + return new PrivateChannel('channel-name'); + } +} diff --git a/app/Events/StackTaskFailed.php b/app/Events/StackTaskFailed.php new file mode 100644 index 00000000..7e1f1a42 --- /dev/null +++ b/app/Events/StackTaskFailed.php @@ -0,0 +1,31 @@ +task = $task; + } +} diff --git a/app/Events/StackTaskFinished.php b/app/Events/StackTaskFinished.php new file mode 100644 index 00000000..764b1da6 --- /dev/null +++ b/app/Events/StackTaskFinished.php @@ -0,0 +1,31 @@ +task = $task; + } +} diff --git a/app/Events/StackTaskRunning.php b/app/Events/StackTaskRunning.php new file mode 100644 index 00000000..ed44f8f1 --- /dev/null +++ b/app/Events/StackTaskRunning.php @@ -0,0 +1,31 @@ +task = $task; + } +} diff --git a/app/Exceptions/AlreadyDeployingException.php b/app/Exceptions/AlreadyDeployingException.php new file mode 100644 index 00000000..8936a12f --- /dev/null +++ b/app/Exceptions/AlreadyDeployingException.php @@ -0,0 +1,10 @@ +expectsJson()) { + return response()->json(['error' => 'Unauthenticated.'], 401); + } + + return redirect()->guest(route('login')); + } +} diff --git a/app/Exceptions/ManifestNotFoundException.php b/app/Exceptions/ManifestNotFoundException.php new file mode 100644 index 00000000..33f2b819 --- /dev/null +++ b/app/Exceptions/ManifestNotFoundException.php @@ -0,0 +1,55 @@ +stack = $stack; + $this->branch = $branch; + $this->repository = $repository; + } + + /** + * Render the exception. + * + * @return Response + */ + public function render() + { + return response('Cloud manifest not found.', 404); + } +} diff --git a/app/Exceptions/ProvisioningTimeout.php b/app/Exceptions/ProvisioningTimeout.php new file mode 100644 index 00000000..5c1b878e --- /dev/null +++ b/app/Exceptions/ProvisioningTimeout.php @@ -0,0 +1,24 @@ +projectId() ?? 'Deleted'; + + $type = get_class($provisionable); + + return new static("Timed out while provisioning [{$type}] server for project [{$project}]"); + } +} diff --git a/app/Exceptions/StackProvisioningTimeout.php b/app/Exceptions/StackProvisioningTimeout.php new file mode 100644 index 00000000..37111b47 --- /dev/null +++ b/app/Exceptions/StackProvisioningTimeout.php @@ -0,0 +1,29 @@ +stack = $stack; + } +} diff --git a/app/FiltersConfigurationArrays.php b/app/FiltersConfigurationArrays.php new file mode 100644 index 00000000..20bfc724 --- /dev/null +++ b/app/FiltersConfigurationArrays.php @@ -0,0 +1,57 @@ +mapWithKeys(function ($daemon, $name) { + return [$name => Arr::only($daemon, $this->daemonAttributes)]; + })->all(); + } + + /** + * Filter the given schedule definition. + * + * @param array $schedule + * @return array + */ + protected function filterSchedule(array $schedule) + { + return collect($schedule)->mapWithKeys(function ($schedule, $name) { + return [$name => Arr::only($schedule, $this->scheduleAttributes)]; + })->all(); + } +} diff --git a/app/Haiku.php b/app/Haiku.php new file mode 100644 index 00000000..270bea3e --- /dev/null +++ b/app/Haiku.php @@ -0,0 +1,214 @@ + 'json', + 'published' => 'boolean', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array + */ + protected $guarded = []; + + /** + * Get the project for the hook. + * + * @return \App\Project + */ + public function project() + { + return $this->stack->project(); + } + + /** + * Get the source control provider for the hook. + * + * @return \App\SourceProvider + */ + public function sourceProvider() + { + return $this->stack->project()->sourceProvider; + } + + /** + * Get the repository for the hook. + * + * @return string + */ + public function repository() + { + return $this->project()->repository; + } + + /** + * Get the stack the hook belongs to. + */ + public function stack() + { + return $this->belongsTo(Stack::class, 'stack_id'); + } + + /** + * Publish the hook to the source control provider. + * + * @return void + */ + public function publish() + { + $this->sourceProvider()->client()->publishHook($this); + } + + /** + * Remove the hook from the source control provider. + * + * @return void + */ + public function unpublish() + { + if ($this->published) { + $this->sourceProvider()->client()->unpublishHook($this); + } + } + + /** + * Determine if the given hook payload is a test. + * + * @param \App\Hook $hook + * @param array $payload + * @return bool + */ + public function isTest(array $payload) + { + return $this->sourceProvider()->client()->isTestHookPayload( + $this, $payload + ); + } + + /** + * Determine if this hook responds to the given source provider event payload. + * + * @param arary $payload + * @return bool + */ + public function receives(array $payload) + { + return ! $this->published || $this->sourceProvider()->client()->receivesHookPayload( + $this, $payload + ); + } + + /** + * Get the URL to be used for hook deployments. + * + * @return string + */ + public function url() + { + return url(config('app.url')."/api/hook-deployment/{$this->id}/{$this->token}"); + } + + /** + * Delete the model from the database. + * + * @return bool|null + * + * @throws \Exception + */ + public function delete() + { + $this->unpublish(); + + return parent::delete(); + } + + /** + * Convert the model instance to an array. + * + * @return array + */ + public function toArray() + { + return array_merge(parent::toArray(), [ + 'url' => $this->url(), + ]); + } +} diff --git a/app/Http/Controllers/API/BalancerController.php b/app/Http/Controllers/API/BalancerController.php new file mode 100644 index 00000000..e0c3180b --- /dev/null +++ b/app/Http/Controllers/API/BalancerController.php @@ -0,0 +1,64 @@ +authorize('view', $request->project); + + return $request->project->balancers()->with('address')->get(); + } + + /** + * Handle the incoming request. + * + * @param \App\Http\Requests\CreateBalancerRequest $request + * @return mixed + */ + public function store(CreateBalancerRequest $request) + { + $this->authorize('view', $request->project); + + $balancer = $request->project->provisionBalancer( + $request->name, $request->size, $request->tls === 'self-signed' + ); + + return response()->json($balancer, 201); + } + + /** + * Delete the given balancer. + * + * @param \App\Balancer $balancer + * @return Response + */ + public function destroy(Balancer $balancer) + { + $this->authorize('delete', $balancer); + + $balancer->load('project.environments.stacks.webServers'); + + if (count($balancer->project->balancers) === 1 && + $balancer->project->allStacks()->contains->balanced) { + throw ValidationException::withMessages(['balancer' => [ + 'You may not delete the last load balancer if it is currently in use.' + ]]); + } + + $balancer->delete(); + } +} diff --git a/app/Http/Controllers/API/CallbackController.php b/app/Http/Controllers/API/CallbackController.php new file mode 100644 index 00000000..8a79133f --- /dev/null +++ b/app/Http/Controllers/API/CallbackController.php @@ -0,0 +1,34 @@ +isRunning(), 404); + + FinishTask::dispatch( + $task, (int) $request->query('exit_code') + ); + } +} diff --git a/app/Http/Controllers/API/CancelsDeployments.php b/app/Http/Controllers/API/CancelsDeployments.php new file mode 100644 index 00000000..242211df --- /dev/null +++ b/app/Http/Controllers/API/CancelsDeployments.php @@ -0,0 +1,25 @@ +stack->resetDeploymentStatus(); + + if (! $deployment->cancel()) { + return response()->json([ + 'deployment' => ['We were unable to cancel this deployment.'], + ], 400); + } + } +} diff --git a/app/Http/Controllers/API/CollaboratorController.php b/app/Http/Controllers/API/CollaboratorController.php new file mode 100644 index 00000000..3481b107 --- /dev/null +++ b/app/Http/Controllers/API/CollaboratorController.php @@ -0,0 +1,45 @@ +user()->projects()->with('collaborators')->get(); + + $collaborators = collect(); + + foreach ($projects as $project) { + $collaborators = $collaborators->merge($project->collaborators); + } + + return $collaborators->unique(function ($collaborator) { + return $collaborator->id; + }); + } + + /** + * Remove a user from all of the user's owned projects. + * + * @param Request $request + * @return Response + */ + public function destroy(Request $request) + { + $user = User::where('email', $request->email)->firstOrFail(); + + foreach ($request->user()->projects as $project) { + $project->stopSharingWith($user); + } + } +} diff --git a/app/Http/Controllers/API/DaemonController.php b/app/Http/Controllers/API/DaemonController.php new file mode 100644 index 00000000..9eb2816b --- /dev/null +++ b/app/Http/Controllers/API/DaemonController.php @@ -0,0 +1,49 @@ +authorize('view', $stack->project()); + + $request->validate([ + 'action' => 'required|string|in:start,restart,pause,continue,unpause' + ]); + + if (! $deployment = $stack->lastDeployment()) { + throw ValidationException::withMessages([ + 'stack' => ['This stack does not have any deployments.'], + ]); + } + + switch ($request->action) { + case 'start': + case 'restart': + $deployment->serverDeployments->each->restartDaemons(); + break; + + case 'pause': + $deployment->serverDeployments->each->pauseDaemons(); + break; + + case 'continue': + case 'unpause': + $deployment->serverDeployments->each->unpauseDaemons(); + break; + } + } +} diff --git a/app/Http/Controllers/API/DatabaseBackupController.php b/app/Http/Controllers/API/DatabaseBackupController.php new file mode 100644 index 00000000..b162917c --- /dev/null +++ b/app/Http/Controllers/API/DatabaseBackupController.php @@ -0,0 +1,69 @@ +authorize('view', $database->project); + + return $database->backups()->with('storageProvider') + ->when($request->database_name, function ($query, $name) { + $query->where('database_name', $name); + })->get()->groupBy('database_name'); + } + + /** + * Create a new backup for the database. + * + * @param \App\Http\Requests\CreateDatabaseBackupRequest $request + * @param \App\Database $database + * @return Response + */ + public function store(CreateDatabaseBackupRequest $request, Database $database) + { + $this->authorize('view', $database->project); + + if (! $database->isProvisioned()) { + throw ValidationException::withMessages([ + 'database' => ['This database has not finished provisioning.'], + ]); + } + + return response()->json( + $database->backup( + $request->storageProvider(), + $request->database_name + ), 201 + ); + } + + /** + * Destroy the given database backup. + * + * @param \App\DatabaseBackup $backup + * @return Response + */ + public function destroy(DatabaseBackup $backup) + { + $this->authorize('delete', $backup); + + $backup->delete(); + } +} diff --git a/app/Http/Controllers/API/DatabaseController.php b/app/Http/Controllers/API/DatabaseController.php new file mode 100644 index 00000000..28e04c54 --- /dev/null +++ b/app/Http/Controllers/API/DatabaseController.php @@ -0,0 +1,55 @@ +authorize('view', $project); + + return $project->databases()->with('address')->get(); + } + + /** + * Handle the incoming request. + * + * @param CreateDatabaseRequest $request + * @return mixed + */ + public function store(CreateDatabaseRequest $request) + { + $this->authorize('view', $request->project); + + $database = $request->project->provisionDatabase( + $request->name, $request->size + ); + + return response()->json($database, 201); + } + + /** + * Delete the given database. + * + * @param Database $database + * @return Response + */ + public function destroy(Database $database) + { + $this->authorize('delete', $database); + + $database->delete(); + } +} diff --git a/app/Http/Controllers/API/DatabaseRestoreController.php b/app/Http/Controllers/API/DatabaseRestoreController.php new file mode 100644 index 00000000..e71e9c2c --- /dev/null +++ b/app/Http/Controllers/API/DatabaseRestoreController.php @@ -0,0 +1,59 @@ +authorize('view', $database->project); + + $restores = $database->restores->load('backup'); + + if ($request->database_name) { + $restores = $restores->filter(function ($restore) use ($request) { + return $restore->backup->database_name == $request->database_name; + }); + } + + return $restores->groupBy(function ($restore) { + return $restore->backup->database_name; + }); + } + + /** + * Restore the database from the given backup. + * + * @param \Illuminate\Http\Request $request + * @param \App\DatabaseBackup $backup + * @return Response + */ + public function store(Request $request, DatabaseBackup $backup) + { + $this->authorize('create', [DatabaseRestore::class, $backup->database]); + + if (! $backup->database->isProvisioned()) { + throw ValidationException::withMessages([ + 'database' => ['This database has not finished provisioning.'], + ]); + } + + return response()->json( + $backup->restore(), 201 + ); + } +} diff --git a/app/Http/Controllers/API/DatabaseTransferController.php b/app/Http/Controllers/API/DatabaseTransferController.php new file mode 100644 index 00000000..5b4fd9d7 --- /dev/null +++ b/app/Http/Controllers/API/DatabaseTransferController.php @@ -0,0 +1,39 @@ +authorize('transfer', $database); + + $request->validate([ + 'project_id' => [ + 'required' , + 'integer', + Rule::exists('projects', 'id')->where(function ($query) use ($request) { + $query->where('user_id', $request->user()->id); + }) + ], + ]); + + $database->stacks()->detach(); + + tap($database)->update([ + 'project_id' => $request->project_id, + ])->syncNetwork(); + } +} diff --git a/app/Http/Controllers/API/DeploymentController.php b/app/Http/Controllers/API/DeploymentController.php new file mode 100644 index 00000000..8401566f --- /dev/null +++ b/app/Http/Controllers/API/DeploymentController.php @@ -0,0 +1,92 @@ +authorize('view', $stack); + + return $stack->deployments()->take(10)->get(); + } + + /** + * Get the deployment with the given ID. + * + * @param \App\Deployment $deployment + * @return Response + */ + public function show(Deployment $deployment) + { + $this->authorize('view', $deployment->stack); + + return $deployment->load([ + 'serverDeployments.deployable', + 'serverDeployments.buildTask', + 'serverDeployments.activationTask' + ]); + } + + /** + * Create a new deployment for the stack. + * + * @param \App\Http\Requests\CreateDeploymentRequest $request + * @return Response + */ + public function store(CreateDeploymentRequest $request) + { + $this->authorize('view', $request->stack); + + if (! $request->stack->isProvisioned()) { + throw ValidationException::withMessages([ + 'stack' => ['This stack has not finished provisioning.'], + ]); + } + + $instructions = DeploymentInstructions::fromRequest($request); + + try { + $deployment = $request->stack->deployUsing($instructions); + + $deployment->update([ + 'initiator_id' => $request->user()->id ?? null, + ]); + + return response($deployment, 201); + } catch (AlreadyDeployingException $e) { + return response('', 409); + } + } + + /** + * Cancel the given deployment. + * + * @param \App\Deployment $deployment + * @return Response + */ + public function destroy(Deployment $deployment) + { + $this->authorize('view', $deployment->stack); + + return $this->cancel($deployment); + } +} diff --git a/app/Http/Controllers/API/EnvironmentController.php b/app/Http/Controllers/API/EnvironmentController.php new file mode 100644 index 00000000..450e96db --- /dev/null +++ b/app/Http/Controllers/API/EnvironmentController.php @@ -0,0 +1,102 @@ +authorize('view', $request->project); + + return $request->project->environments; + } + + /** + * Retrieve the given environment. + * + * @param Request $request + * @param \App\Environment $environment + * @return Response + */ + public function show(Request $request, Environment $environment) + { + $this->authorize('view', $environment->project); + + return $environment->makeVisible(['variables']); + } + + /** + * Create a new environment. + * + * @param Request $request + * @return Response + */ + public function store(Request $request) + { + $this->authorize('view', $request->project); + + $request->validate([ + 'name' => 'required|string|max:255|unique:environments,name,NULL,id,project_id,'.$request->project->id, + 'variables' => 'string|max:50000' + ]); + + return response()->json($request->project->environments()->create([ + 'creator_id' => $request->user()->id, + 'name' => $request->name, + 'encryption_key' => 'base64:'.base64_encode(Encrypter::generateKey(config('app.cipher'))), + 'variables' => $request->variables ?? '', + ]), 201); + } + + /** + * Create a new environment. + * + * @param Request $request + * @return Response + */ + public function update(Request $request) + { + $this->authorize('view', $request->environment->project); + + $request->validate([ + 'variables' => 'nullable|string|max:50000' + ]); + + return tap($request->environment)->update([ + 'variables' => $request->variables ?? '', + ]); + } + + /** + * Delete an environment. + * + * @param Request $request + * @param \App\Environment $environment + * @return Response + */ + public function destroy(Request $request, Environment $environment) + { + $this->authorize('delete', $environment); + + if (! $environment->stacks->every->isProvisioned() || + $environment->stacks->contains->isDeploying()) { + throw ValidationException::withMessages([ + 'stack' => ['This environment has stacks that are provisioning or deploying.'], + ]); + } + + $environment->delete(); + } +} diff --git a/app/Http/Controllers/API/EnvironmentHookController.php b/app/Http/Controllers/API/EnvironmentHookController.php new file mode 100644 index 00000000..59c2907e --- /dev/null +++ b/app/Http/Controllers/API/EnvironmentHookController.php @@ -0,0 +1,29 @@ +authorize('view', $environment->project); + + return $environment->stacks->load('hooks.stack') + ->flatMap + ->hooks + ->sortBy('name') + ->sortBy('stack.name') + ->values(); + } +} diff --git a/app/Http/Controllers/API/HookController.php b/app/Http/Controllers/API/HookController.php new file mode 100644 index 00000000..7837ec5c --- /dev/null +++ b/app/Http/Controllers/API/HookController.php @@ -0,0 +1,75 @@ +authorize('view', $stack->project()); + + return $stack->hooks->load('stack'); + } + + /** + * Create a new hook for the stack. + * + * @param \App\Http\Requests\CreateHookRequest $request + * @param \App\Stack $stack + * @return Response + */ + public function store(CreateHookRequest $request, Stack $stack) + { + $this->authorize('view', $stack->project()); + + if (! $stack->isProvisioned()) { + throw ValidationException::withMessages([ + 'stack' => ['This stack has not finished provisioning.'], + ]); + } + + $hook = DB::transaction(function () use ($request, $stack) { + return tap($stack->hooks()->create([ + 'name' => $request->name, + 'token' => Str::random(40), + 'branch' => $request->branch, + 'meta' => [], + ]), function ($hook) use ($request) { + if ($request->publish) { + $hook->publish(); + } + }); + }); + + return response()->json($hook, 201); + } + + /** + * Delete the given hook. + * + * @param \App\Hook $hook + * @return Response + */ + public function destroy(Hook $hook) + { + $this->authorize('view', $hook->project()); + + $hook->delete(); + } +} diff --git a/app/Http/Controllers/API/HookDeploymentController.php b/app/Http/Controllers/API/HookDeploymentController.php new file mode 100644 index 00000000..c0548843 --- /dev/null +++ b/app/Http/Controllers/API/HookDeploymentController.php @@ -0,0 +1,46 @@ +token !== $token, 403); + + if ($hook->isTest($request->all())) { + return; + } + + if (! $request->receivable() || ! $hook->receives($request->all())) { + return response('', 204); + } + + try { + $instructions = $request->instructions(); + + return response()->json($hook->stack->deployHash( + $instructions->hash, + $instructions->build, $instructions->activate, + $instructions->directories, $instructions->daemons + ), 201); + } catch (AlreadyDeployingException $e) { + $hook->stack->storePendingDeployment($hook, $request->hash()); + + return response('', 202); + } + } +} diff --git a/app/Http/Controllers/API/KeyController.php b/app/Http/Controllers/API/KeyController.php new file mode 100644 index 00000000..d5c87a12 --- /dev/null +++ b/app/Http/Controllers/API/KeyController.php @@ -0,0 +1,52 @@ +addressable($request); + + $this->authorize('view', $addressable->project); + + $task = $addressable->run(new AddKeyToServer( + 'cloud-user-'.$request->user()->id, $request->user()->public_key + )); + + if (! $task->successful()) { + return response('', 504); + } + + RemoveKeyFromServer::dispatch( + $request->user(), $addressable + )->delay(30); + + return ['key' => $request->user()->private_key]; + } + + /** + * Get the addressable instance for the request. + * + * @param Request $request + * @return mixed + */ + protected function addressable(Request $request) + { + return IpAddress::where( + 'public_address', $request->ip_address + )->firstOrFail()->addressable; + } +} diff --git a/app/Http/Controllers/API/LastDeploymentController.php b/app/Http/Controllers/API/LastDeploymentController.php new file mode 100644 index 00000000..3f76a84f --- /dev/null +++ b/app/Http/Controllers/API/LastDeploymentController.php @@ -0,0 +1,31 @@ +authorize('view', $stack); + + if ($deployment = $stack->lastDeployment()) { + return $this->cancel($deployment); + } + + return response()->json([ + 'deployment' => ['No deployments exist for this stack.'], + ], 400); + } +} diff --git a/app/Http/Controllers/API/LoginController.php b/app/Http/Controllers/API/LoginController.php new file mode 100644 index 00000000..62733534 --- /dev/null +++ b/app/Http/Controllers/API/LoginController.php @@ -0,0 +1,28 @@ +firstOrFail(); + + if (! Hash::check(request('password'), $user->password)) { + abort(422); + } + + $user->revokeTokens(request('host')); + + return ['access_token' => $user->createToken(request('host'), ['*'])->accessToken]; + } +} diff --git a/app/Http/Controllers/API/MaintenancedStackController.php b/app/Http/Controllers/API/MaintenancedStackController.php new file mode 100644 index 00000000..bd7f567f --- /dev/null +++ b/app/Http/Controllers/API/MaintenancedStackController.php @@ -0,0 +1,47 @@ +authorize( + 'view', $stack = Stack::findOrFail($request->stack) + ); + + $stack->update([ + 'under_maintenance' => true, + ]); + + SyncServers::dispatch($stack); + } + + /** + * Remove the given stack from maintenance mode. + * + * @param \App\Stack $stack + * @return Response + */ + public function destroy(Stack $stack) + { + $this->authorize('view', $stack); + + $stack->update([ + 'under_maintenance' => false, + ]); + + SyncServers::dispatch($stack); + } +} diff --git a/app/Http/Controllers/API/OwnedProjectsController.php b/app/Http/Controllers/API/OwnedProjectsController.php new file mode 100644 index 00000000..8a99c417 --- /dev/null +++ b/app/Http/Controllers/API/OwnedProjectsController.php @@ -0,0 +1,20 @@ +user()->projects->sortBy('name')->reject->archived; + } +} diff --git a/app/Http/Controllers/API/ProjectCollaboratorController.php b/app/Http/Controllers/API/ProjectCollaboratorController.php new file mode 100644 index 00000000..e4788369 --- /dev/null +++ b/app/Http/Controllers/API/ProjectCollaboratorController.php @@ -0,0 +1,74 @@ +authorize('view', $request->project); + + return $request->project->collaborators; + } + + /** + * Add a collaborator to the project. + * + * @return \Illuminate\Http\Response + */ + public function store(Request $request) + { + $this->authorize('updateCollaborators', $request->project); + + $this->validate($request, [ + 'email' => 'required|max:255', + ]); + + if (is_null($user = User::where('email', $request->email)->first())) { + throw ValidationException::withMessages([ + 'user' => ['Unable to find a user with that email address.'], + ]); + } + + $request->project->shareWith($user); + } + + /** + * Remove a collaborator to the project. + * + * @return \Illuminate\Http\Response + */ + public function destroy(Request $request) + { + $this->authorize('updateCollaborators', $request->project); + + $this->validate($request, [ + 'email' => 'required|max:255|in:'.implode(',', $this->emails($request)), + ]); + + $request->project->stopSharingWith( + User::where('email', $request->email)->firstOrFail() + ); + } + + /** + * Get the emails of all of the current collaborators. + * + * @param Request $request + * @return array + */ + protected function emails(Request $request) + { + return $request->project->collaborators->pluck('email')->all(); + } +} diff --git a/app/Http/Controllers/API/ProjectController.php b/app/Http/Controllers/API/ProjectController.php new file mode 100644 index 00000000..a9232547 --- /dev/null +++ b/app/Http/Controllers/API/ProjectController.php @@ -0,0 +1,86 @@ +user()->projects->merge( + $request->user()->teamProjects + )->sortBy('name')->reject->archived; + } + + /** + * Get the project with the given ID. + * + * @param \App\Project $project + * @return Response + */ + public function show(Project $project) + { + $this->authorize('view', $project); + + return $project; + } + + /** + * Handle the incoming request. + * + * @param CreateProjectRequest $request + * @return mixed + */ + public function store(CreateProjectRequest $request) + { + $project = $request->user()->projects()->create([ + 'name' => $request->name, + 'server_provider_id' => $request->server_provider_id, + 'region' => $request->region, + 'source_provider_id' => $request->source_provider_id, + 'repository' => $request->repository, + ]); + + if ($request->database) { + $project->provisionDatabase( + $request->database, $request->database_size + ); + } + + return response()->json($project, 201); + } + + /** + * Destroy the given project. + * + * @param Request $request + * @param \App\Project $project + * @return Response + */ + public function destroy(Request $request, Project $project) + { + $this->authorize('delete', $project); + + if (! $project->allStacks()->every->isProvisioned() || + $project->allStacks()->contains->isDeploying()) { + throw ValidationException::withMessages([ + 'stack' => ['This project has stacks that are provisioning or deploying.'], + ]); + } + + $project->purge(); + + $project->archive(); + } +} diff --git a/app/Http/Controllers/API/ProjectSizeController.php b/app/Http/Controllers/API/ProjectSizeController.php new file mode 100644 index 00000000..1223d91e --- /dev/null +++ b/app/Http/Controllers/API/ProjectSizeController.php @@ -0,0 +1,22 @@ +serverProvider->sizes(); + } +} diff --git a/app/Http/Controllers/API/PromotedStackController.php b/app/Http/Controllers/API/PromotedStackController.php new file mode 100644 index 00000000..42ad7d9a --- /dev/null +++ b/app/Http/Controllers/API/PromotedStackController.php @@ -0,0 +1,62 @@ +authorize('view', $environment->project); + + if (! $environment->promotedStack()) { + abort(404); + } + + return $environment->promotedStack(); + } + + /** + * Set the promoted stack for the environment. + * + * @param Request $request + * @param \App\Environment $environment + * @return mixed + */ + public function update(Request $request, Environment $environment) + { + $request->validate([ + 'stack' => [ + 'required', + new StackIsPromotable($stack = Stack::findOrFail($request->stack)) + ], + 'hooks' => 'nullable|boolean', + 'wait' => 'nullable|boolean', + ]); + + $this->authorize('view', $stack); + + if (! $environment->promotionLock()->get()) { + return response()->json([ + 'environment' => ['This environment is already promoting another stack.'], + ], 409); + } + + $environment->promote($stack, [ + 'hooks' => (bool) $request->hooks, + 'wait' => (bool) $request->wait, + ]); + } +} diff --git a/app/Http/Controllers/API/SchedulerController.php b/app/Http/Controllers/API/SchedulerController.php new file mode 100644 index 00000000..168a74f9 --- /dev/null +++ b/app/Http/Controllers/API/SchedulerController.php @@ -0,0 +1,53 @@ +authorize('view', $stack); + + if (! $deployment = $stack->lastDeployment()) { + throw ValidationException::withMessages([ + 'stack' => ['This stack does not have any deployments.'], + ]); + } + + $deployment->serverDeployments->each->startScheduler(); + + return response('', 201); + } + + /** + * Remove the scheduler for the stack. + * + * @param Request $request + * @param \App\Stack $stack + * @return mixed + */ + public function destroy(Request $request, Stack $stack) + { + $this->authorize('view', $stack); + + if (! $deployment = $stack->lastDeployment()) { + throw ValidationException::withMessages([ + 'stack' => ['This stack does not have any deployments.'], + ]); + } + + $deployment->serverDeployments->each->stopScheduler(); + } +} diff --git a/app/Http/Controllers/API/ServerConfigurationController.php b/app/Http/Controllers/API/ServerConfigurationController.php new file mode 100644 index 00000000..6f2752c1 --- /dev/null +++ b/app/Http/Controllers/API/ServerConfigurationController.php @@ -0,0 +1,22 @@ +authorize('view', $stack); + + $stack->syncServers(); + } +} diff --git a/app/Http/Controllers/API/ServerProviderController.php b/app/Http/Controllers/API/ServerProviderController.php new file mode 100644 index 00000000..80e8f1ba --- /dev/null +++ b/app/Http/Controllers/API/ServerProviderController.php @@ -0,0 +1,52 @@ +user()->serverProviders; + } + + /** + * Handle the incoming request. + * + * @param Request $request + * @return mixed + */ + public function store(Request $request) + { + $this->validate($request, [ + 'name' => 'required|max:255|unique:server_providers,name,NULL,id,user_id,'.$request->user()->id, + 'type' => 'required|in:DigitalOcean', + 'meta' => 'required|array', + ]); + + $provider = $request->user()->serverProviders()->create([ + 'name' => $request->name, + 'type' => $request->type, + 'meta' => $request->meta, + ]); + + if (! $provider->client()->valid()) { + $provider->delete(); + + throw ValidationException::withMessages([ + 'meta' => ['The given credentials are invalid.'], + ]); + } + + return response()->json($provider, 201); + } +} diff --git a/app/Http/Controllers/API/ServerProviderRegionController.php b/app/Http/Controllers/API/ServerProviderRegionController.php new file mode 100644 index 00000000..026be513 --- /dev/null +++ b/app/Http/Controllers/API/ServerProviderRegionController.php @@ -0,0 +1,22 @@ +regions(); + } +} diff --git a/app/Http/Controllers/API/ServerProviderSizeController.php b/app/Http/Controllers/API/ServerProviderSizeController.php new file mode 100644 index 00000000..0bafaec2 --- /dev/null +++ b/app/Http/Controllers/API/ServerProviderSizeController.php @@ -0,0 +1,22 @@ +sizes(); + } +} diff --git a/app/Http/Controllers/API/SourceProviderController.php b/app/Http/Controllers/API/SourceProviderController.php new file mode 100644 index 00000000..acc6e5a3 --- /dev/null +++ b/app/Http/Controllers/API/SourceProviderController.php @@ -0,0 +1,73 @@ +user()->sourceProviders; + } + + /** + * Handle the incoming request. + * + * @param Request $request + * @return mixed + */ + public function store(Request $request) + { + $this->validate($request, [ + 'name' => 'required|max:255|unique:source_providers,name,NULL,id,user_id,'.$request->user()->id, + 'type' => 'required|in:GitHub', + 'meta' => 'required|array', + ]); + + $source = $request->user()->sourceProviders()->create([ + 'name' => $request->name, + 'type' => $request->type, + 'meta' => $request->meta, + ]); + + if (! $source->client()->valid()) { + $source->delete(); + + throw ValidationException::withMessages([ + 'meta' => ['The given credentials are invalid.'], + ]); + } + + return response()->json($source, 201); + } + + /** + * Delete the given source control provider. + * + * @param \Illuminate\Http\Request $request + * @param \App\SourceProvider $provider + * @return Response + */ + public function destroy(Request $request, SourceProvider $provider) + { + abort_if(! $request->user()->sourceProviders->contains($provider), 403); + + if (count($provider->projects) > 0) { + throw ValidationException::withMessages(['balancer' => [ + 'This source control provider is being used by active projects.' + ]]); + } + + $provider->delete(); + } +} diff --git a/app/Http/Controllers/API/SshBalancerController.php b/app/Http/Controllers/API/SshBalancerController.php new file mode 100644 index 00000000..18ecd10c --- /dev/null +++ b/app/Http/Controllers/API/SshBalancerController.php @@ -0,0 +1,26 @@ +authorize('view', $request->project); + + return $request->project->balancers() + ->with('address') + ->get() + ->filter + ->canSsh($request->user()); + } +} diff --git a/app/Http/Controllers/API/SshDatabaseController.php b/app/Http/Controllers/API/SshDatabaseController.php new file mode 100644 index 00000000..5951f2e3 --- /dev/null +++ b/app/Http/Controllers/API/SshDatabaseController.php @@ -0,0 +1,28 @@ +authorize('view', $project); + + return $project->databases() + ->with('address') + ->get() + ->filter + ->canSsh($request->user()); + } +} diff --git a/app/Http/Controllers/API/StackController.php b/app/Http/Controllers/API/StackController.php new file mode 100644 index 00000000..5ff12a7f --- /dev/null +++ b/app/Http/Controllers/API/StackController.php @@ -0,0 +1,63 @@ +authorize('view', $request->project); + + return $request->project->environments()->with('stacks') + ->when($request->environment, function ($query) use ($request) { + $query->where('id', $request->environment); + })->get()->flatMap->stacks->sortBy('name')->values(); + } + + /** + * Provision a stack. + * + * @param ProvisionStackRequest $request + * @return mixed + */ + public function store(ProvisionStackRequest $request) + { + $this->authorize('view', $request->project()); + + return response()->json(DB::transaction(function () use ($request) { + return Stack::createForEnvironment($request->environment(), $request); + })->provision(), 201); + } + + /** + * Delete the given stack. + * + * @param \App\Stack $stack + * @return Response + */ + public function destroy(Stack $stack) + { + $this->authorize('delete', $stack); + + if ($stack->isDeploying()) { + throw ValidationException::withMessages([ + 'stack' => ['A stack may not be deleted while it is deploying.'], + ]); + } + + $stack->delete(); + } +} diff --git a/app/Http/Controllers/API/StackDatabaseController.php b/app/Http/Controllers/API/StackDatabaseController.php new file mode 100644 index 00000000..d93c6b66 --- /dev/null +++ b/app/Http/Controllers/API/StackDatabaseController.php @@ -0,0 +1,39 @@ +authorize('view', $request->stack->project()); + + $request->validate([ + 'databases' => 'array', + 'databases.*' => 'string', + ]); + + $databases = $request->stack->project()->databases()->with('stacks')->get(); + + $databases->reject(function ($database) use ($request) { + return ((in_array($database->name, $request->databases) && + $database->stacks->contains($request->stack)) || + (! in_array($database->name, $request->databases) && + ! $database->stacks->contains($request->stack))); + })->each(function ($database) use ($request) { + $database->stacks()->toggle($request->stack); + + $database->syncNetwork(); + }); + } +} diff --git a/app/Http/Controllers/API/StackServerController.php b/app/Http/Controllers/API/StackServerController.php new file mode 100644 index 00000000..93672eaf --- /dev/null +++ b/app/Http/Controllers/API/StackServerController.php @@ -0,0 +1,30 @@ +authorize('view', $request->stack->project()); + + $stack = $request->stack; + + $stack->load('appServers.address', 'webServers.address', 'workerServers.address'); + + return [ + 'app' => $stack->appServers->all(), + 'web' => $stack->webServers->all(), + 'worker' => $stack->workerServers->all(), + ]; + } +} diff --git a/app/Http/Controllers/API/StackSshServerController.php b/app/Http/Controllers/API/StackSshServerController.php new file mode 100644 index 00000000..88693bd5 --- /dev/null +++ b/app/Http/Controllers/API/StackSshServerController.php @@ -0,0 +1,30 @@ +authorize('view', $request->stack->project()); + + $stack = $request->stack; + + $stack->load('appServers.address', 'webServers.address', 'workerServers.address'); + + return [ + 'app' => $stack->appServers->filter->canSsh($request->user())->all(), + 'web' => $stack->webServers->filter->canSsh($request->user())->all(), + 'worker' => $stack->workerServers->filter->canSsh($request->user())->all(), + ]; + } +} diff --git a/app/Http/Controllers/API/StackTaskController.php b/app/Http/Controllers/API/StackTaskController.php new file mode 100644 index 00000000..bd8de419 --- /dev/null +++ b/app/Http/Controllers/API/StackTaskController.php @@ -0,0 +1,33 @@ +validate([ + 'name' => 'required|string|max:255', + 'user' => 'required|string|in:root,cloud', + 'commands' => 'required|array|min:1', + 'commands.*' => 'string', + ]); + + $request->user === 'root' + ? $this->authorize('delete', $request->stack) + : $this->authorize('view', $request->stack); + + return response()->json($request->stack->dispatchTask( + $request->name, $request->user, $request->commands + ), 201); + } +} diff --git a/app/Http/Controllers/API/StorageProviderController.php b/app/Http/Controllers/API/StorageProviderController.php new file mode 100644 index 00000000..d26c2644 --- /dev/null +++ b/app/Http/Controllers/API/StorageProviderController.php @@ -0,0 +1,67 @@ +user()->storageProviders; + } + + /** + * Handle the incoming request. + * + * @param Request $request + * @return mixed + */ + public function store(Request $request) + { + $this->validate($request, [ + 'name' => 'required|max:255|unique:storage_providers,name,NULL,id,user_id,'.$request->user()->id, + 'type' => 'required|in:S3', + 'meta' => 'required|array', + ]); + + $provider = $request->user()->storageProviders()->create([ + 'name' => $request->name, + 'type' => $request->type, + 'meta' => $request->meta, + ]); + + if (! $provider->client()->valid()) { + $provider->delete(); + + throw ValidationException::withMessages([ + 'meta' => ['The given credentials are invalid.'], + ]); + } + + return response()->json($provider, 201); + } + + /** + * Delete the given storage provider. + * + * @param \Illuminate\Http\Request $request + * @param \App\StorageProvider $provider + * @return Response + */ + public function destroy(Request $request, StorageProvider $provider) + { + abort_if(! $request->user()->storageProviders->contains($provider), 403); + + $provider->delete(); + } +} diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php new file mode 100644 index 00000000..6a247fef --- /dev/null +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -0,0 +1,32 @@ +middleware('guest'); + } +} diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php new file mode 100644 index 00000000..75949531 --- /dev/null +++ b/app/Http/Controllers/Auth/LoginController.php @@ -0,0 +1,39 @@ +middleware('guest', ['except' => 'logout']); + } +} diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php new file mode 100644 index 00000000..1fadf761 --- /dev/null +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -0,0 +1,74 @@ +middleware('guest'); + } + + /** + * Get a validator for an incoming registration request. + * + * @param array $data + * @return \Illuminate\Contracts\Validation\Validator + */ + protected function validator(array $data) + { + return Validator::make($data, [ + 'name' => 'required|max:255', + 'email' => 'required|email|max:255|unique:users', + 'password' => 'required|min:6|confirmed', + ]); + } + + /** + * Create a new user instance after a valid registration. + * + * @param array $data + * @return User + */ + protected function create(array $data) + { + return User::create([ + 'name' => $data['name'], + 'email' => $data['email'], + 'password' => bcrypt($data['password']), + 'keypair' => SecureShellKey::forNewUser($data['password']), + 'worker_keypair' => SecureShellKey::forNewUser(), + ]); + } +} diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php new file mode 100644 index 00000000..cf726eec --- /dev/null +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -0,0 +1,39 @@ +middleware('guest'); + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php new file mode 100644 index 00000000..03e02a23 --- /dev/null +++ b/app/Http/Controllers/Controller.php @@ -0,0 +1,13 @@ +middleware('auth'); + } + + /** + * Show the projects dashboard. + * + * @return \Illuminate\Http\Response + */ + public function index(Request $request) + { + return view('projects.index', [ + 'projects' => $request->user()->projects + ]); + } +} diff --git a/app/Http/Controllers/ScheduleController.php b/app/Http/Controllers/ScheduleController.php new file mode 100644 index 00000000..f300666a --- /dev/null +++ b/app/Http/Controllers/ScheduleController.php @@ -0,0 +1,18 @@ + [ + \App\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + // \Illuminate\Session\Middleware\AuthenticateSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\VerifyCsrfToken::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class, + ], + + 'api' => [ + 'throttle:180,1', + 'bindings', + ], + ]; + + /** + * The application's route middleware. + * + * These middleware may be assigned to groups or used individually. + * + * @var array + */ + protected $routeMiddleware = [ + 'auth' => \Illuminate\Auth\Middleware\Authenticate::class, + 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, + 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, + 'can' => \Illuminate\Auth\Middleware\Authorize::class, + 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, + 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, + ]; +} diff --git a/app/Http/Middleware/EncryptCookies.php b/app/Http/Middleware/EncryptCookies.php new file mode 100644 index 00000000..3aa15f8d --- /dev/null +++ b/app/Http/Middleware/EncryptCookies.php @@ -0,0 +1,17 @@ +check()) { + return redirect('/home'); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/TrimStrings.php b/app/Http/Middleware/TrimStrings.php new file mode 100644 index 00000000..943e9a4d --- /dev/null +++ b/app/Http/Middleware/TrimStrings.php @@ -0,0 +1,18 @@ +validateRegionAndSize(validator($this->all(), [ + 'name' => 'required|string|alpha_dash|max:255|unique:balancers,name,NULL,id,project_id,'.$this->project->id, + 'size' => 'required|string', + 'tls' => 'nullable|string|in:self-signed', + ])); + } + + /** + * Validate the size of the server. + * + * @param \Illuminate\Validator\Validator $validator + * @return \Illuminate\Validator\Validator + */ + protected function validateRegionAndSize($validator) + { + return $validator->after(function ($validator) { + if (! $this->project->serverProvider->validSize($this->size)) { + $validator->errors()->add('size', 'The provided size is invalid.'); + } + }); + } +} diff --git a/app/Http/Requests/CreateDatabaseBackupRequest.php b/app/Http/Requests/CreateDatabaseBackupRequest.php new file mode 100644 index 00000000..37b0e0c3 --- /dev/null +++ b/app/Http/Requests/CreateDatabaseBackupRequest.php @@ -0,0 +1,45 @@ +storage_provider_id); + } + + /** + * Determine if the user is authorized to make this request. + * + * @return bool + */ + public function authorize() + { + return true; + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + 'storage_provider_id' => ['required', Rule::exists('storage_providers', 'id')->where(function ($query) { + $query->where('user_id', $this->user()->id); + })], + 'database_name' => 'required|string|max:255', + ]; + } +} diff --git a/app/Http/Requests/CreateDatabaseRequest.php b/app/Http/Requests/CreateDatabaseRequest.php new file mode 100644 index 00000000..4e95bbf9 --- /dev/null +++ b/app/Http/Requests/CreateDatabaseRequest.php @@ -0,0 +1,46 @@ +validateRegionAndSize(validator($this->all(), [ + 'name' => 'required|string|alpha_dash|max:255|unique:databases,name,NULL,id,project_id,'.$this->project->id, + 'size' => 'required|string', + ])); + } + + /** + * Validate the size of the server. + * + * @param \Illuminate\Validator\Validator $validator + * @return \Illuminate\Validator\Validator + */ + protected function validateRegionAndSize($validator) + { + return $validator->after(function ($validator) { + if (! $this->project->serverProvider->validSize($this->size)) { + $validator->errors()->add('size', 'The provided size is invalid.'); + } + }); + } +} diff --git a/app/Http/Requests/CreateDeploymentRequest.php b/app/Http/Requests/CreateDeploymentRequest.php new file mode 100644 index 00000000..924d5047 --- /dev/null +++ b/app/Http/Requests/CreateDeploymentRequest.php @@ -0,0 +1,74 @@ +all(), [ + 'branch' => ['required_without:hash', 'string', 'max:255', new ValidBranch( + $this->stack->project()->sourceProvider, $this->stack->project()->repository + )], + 'hash' => ['required_without:branch', 'string', 'max:40', new ValidCommit( + $this->stack->project()->sourceProvider, $this->stack->project()->repository + )], + 'build' => 'array', + 'build.*' => 'string', + 'activate' => 'array', + 'activate.*' => 'string', + 'daemons' => 'array', + 'daemons.*.command' => 'required_with:daemons|string', + 'daemons.*.directory' => 'string', + 'daemons.*.processes' => 'integer|min:1', + 'daemons.*.wait' => 'integer|min:1', + 'schedule.*' => 'array', + 'schedule.*.command' => 'required_with:schedule|string|max:1000', + 'schedule.*.frequency' => 'required_with:schedule|string|max:50', + 'schedule.*.user' => 'string|max:50', + ]); + } + + /** + * Extract the daemons from the request. + * + * @return array + */ + public function daemons() + { + return empty($this->daemons) ? [] : $this->filterDaemons($this->daemons); + } + + /** + * Extract the scheduled tasks from the request. + * + * @return array + */ + public function schedule() + { + return empty($this->schedule) ? [] : $this->filterSchedule($this->schedule); + } +} diff --git a/app/Http/Requests/CreateHookDeploymentRequest.php b/app/Http/Requests/CreateHookDeploymentRequest.php new file mode 100644 index 00000000..42f1c959 --- /dev/null +++ b/app/Http/Requests/CreateHookDeploymentRequest.php @@ -0,0 +1,118 @@ +fromCodeship()) { + return $this->input('build')['status'] == 'success'; + } + + return true; + } + + /** + * Get the commit hash being deployed. + * + * @return string + */ + public function hash() + { + return once(function () { + switch (true) { + case $this->fromCodeship(): + return $this->hashFromCodeship(); + + default: + return $this->hashFromSourceProvider(); + } + }); + } + + /** + * Geth the commit hash from the Codeship payload. + * + * @return string + */ + protected function hashFromCodeship() + { + return $this->input('build')['commit_id'] ?? null; + } + + /** + * Geth the commit hash from the source control provider. + * + * @return string + */ + protected function hashFromSourceProvider() + { + return $this->sourceProviderClient()->extractCommitFromHookPayload($this->all()); + } + + /** + * Get the deployment instructions for the commit. + * + * @return \App\DeploymentInstructions + */ + public function instructions() + { + return once(function () { + return $this->hash() + ? DeploymentInstructions::fromHookCommit($this->hook, $this->hash()) + : DeploymentInstructions::forLatestHookCommit($this->hook); + }); + } + + /** + * Determine if the request is from Codeship. + * + * @return bool + */ + protected function fromCodeship() + { + return $this->userAgent() == 'Codeship Webhook'; + } + + /** + * Get the source control provider client instance. + * + * @return \App\Contracts\SourceProviderClient + */ + protected function sourceProviderClient() + { + return $this->hook->sourceProvider()->client(); + } + + /** + * Determine if the user is authorized to make this request. + * + * @return bool + */ + public function authorize() + { + return true; + } + + /** + * Get the validator for the request. + * + * @return \Illuminate\Validation\Validator + */ + public function validator() + { + return validator($this->all(), []); + } +} diff --git a/app/Http/Requests/CreateHookRequest.php b/app/Http/Requests/CreateHookRequest.php new file mode 100644 index 00000000..13b65b04 --- /dev/null +++ b/app/Http/Requests/CreateHookRequest.php @@ -0,0 +1,43 @@ +all(), [ + 'name' => 'required|string|max:255', + 'branch' => [ + 'required', + 'string', + 'max:255', + new ValidBranch( + $this->stack->project()->sourceProvider, $this->stack->project()->repository + ) + ], + 'publish' => 'required|boolean', + ]); + } +} diff --git a/app/Http/Requests/CreateProjectRequest.php b/app/Http/Requests/CreateProjectRequest.php new file mode 100644 index 00000000..1511e847 --- /dev/null +++ b/app/Http/Requests/CreateProjectRequest.php @@ -0,0 +1,70 @@ +all(), [ + 'name' => 'required|max:255', + 'server_provider_id' => ['required', Rule::exists('server_providers', 'id')->where(function ($query) { + $query->where('user_id', $this->user()->id); + })], + 'region' => 'required|string', + 'source_provider_id' => ['required', Rule::exists('source_providers', 'id')->where(function ($query) { + $query->where('user_id', $this->user()->id); + })], + 'repository' => [ + 'required', + 'string', + new ValidRepository(SourceProvider::find($this->source_provider_id)) + ], + 'database' => 'string|alpha_dash|max:255', + 'database_size' => 'string', + ]); + + return $this->validateRegionAndSize($validator); + } + + /** + * Validate the region and size for the provider. + * + * @param \Illuminate\Validator\Validator $validator + * @return \Illuminate\Validator\Validator + */ + protected function validateRegionAndSize($validator) + { + return $validator->after(function ($validator) { + $provider = $this->user()->serverProviders()->find($this->server_provider_id); + + if (! $provider->validRegion($this->region)) { + $validator->errors()->add('region', 'Invalid region for provider.'); + } + + if ($this->database && ! $provider->validSize($this->database_size)) { + $validator->errors()->add('database_size', 'Invalid size for database.'); + } + }); + } +} diff --git a/app/Http/Requests/ProvisionStackRequest.php b/app/Http/Requests/ProvisionStackRequest.php new file mode 100644 index 00000000..3fd3a91a --- /dev/null +++ b/app/Http/Requests/ProvisionStackRequest.php @@ -0,0 +1,290 @@ +user(); + } + + /** + * Get the project associated with the request. + * + * @return \App\Project + */ + public function project() + { + return $this->environment->project; + } + + /** + * Get the environment for the request. + * + * @return \App\Environment + */ + public function environment() + { + return $this->environment; + } + + /** + * Extract the daemons from the request. + * + * @return array + */ + public function daemons() + { + $daemons = []; + + if ($this->app && is_array($this->app)) { + $daemons = $this->app['daemons'] ?? []; + } elseif ($this->worker && is_array($this->worker)) { + $daemons = $this->worker['daemons'] ?? []; + } + + return empty($daemons) ? [] : $this->filterDaemons($daemons); + } + + /** + * Extract the scheduled tasks from the request. + * + * @return array + */ + public function schedule() + { + $schedule = []; + + if ($this->app && is_array($this->app)) { + $schedule = $this->app['schedule'] ?? []; + } elseif ($this->worker && is_array($this->worker)) { + $schedule = $this->worker['schedule'] ?? []; + } + + return empty($schedule) ? [] : $this->filterSchedule($schedule); + } + + /** + * Extract the scripts from the request. + * + * @return array + */ + public function scripts() + { + return [ + 'app' => $this['app']['scripts'] ?? [], + 'web' => $this['web']['scripts'] ?? [], + 'worker' => $this['worker']['scripts'] ?? [], + ]; + } + + /** + * Determine if the user is authorized to make this request. + * + * @return bool + */ + public function authorize() + { + return true; + } + + /** + * Get the validator for the request. + * + * @return \Illuminate\Validation\Validator + */ + public function validator() + { + return validator($this->all(), array_merge([ + 'name' => 'required|string|max:255|alpha_dash', + ], + $this->databaseRules(), + $this->sourceControlRules(), + $this->appServerRules(), + $this->webServerRules(), + $this->workerServerRules(), + $this->deploymentRules(), + $this->metaRules() + ))->after(function ($validator) { + if (! $this->hasAppServers() && ! $this->hasWebServers()) { + $validator->errors()->add('web', 'At least one web server must be defined.'); + } + }); + } + + /** + * Determine if the request has app servers. + * + * @return bool + */ + protected function hasAppServers() + { + return is_array($this->app) and count($this->app) > 0; + } + + /** + * Determine if the request has web servers. + * + * @return bool + */ + protected function hasWebServers() + { + return is_array($this->web) and count($this->web) > 0; + } + + /** + * Get the application server validation rules. + * + * @return array + */ + protected function databaseRules() + { + return [ + 'databases' => 'array|max:20', + 'databases.*' => [ + 'string', + new ValidDatabaseName($this->project()), + new DatabaseIsProvisioned($this->project()), + ], + ]; + } + + /** + * Get the source control validation rules. + * + * @return array + */ + protected function sourceControlRules() + { + return [ + 'branch' => 'required|string', + ]; + } + + /** + * Get the application server validation rules. + * + * @return array + */ + protected function appServerRules() + { + return [ + 'app' => ['array', new ValidAppServerStack($this)], + 'app.scale' => 'integer|between:1,1', + 'app.size' => ['required_with:app', 'string', new ValidSize($this->project())], + 'app.tls' => 'string|in:self-signed', + 'app.serves' => [ + 'array', + 'min:1', + new ValidServeList($this->project(), $this->environment()->name) + ], + 'app.serves.*' => 'string', + 'app.daemons' => 'array', + 'app.daemons.*.command' => 'required_with:app.daemons|string', + 'app.daemons.*.directory' => 'string', + 'app.daemons.*.processes' => 'integer|min:1', + 'app.daemons.*.wait' => 'integer|min:1', + 'app.schedule.*' => 'array', + 'app.schedule.*.command' => 'required_with:app.schedule|string|max:1000', + 'app.schedule.*.frequency' => 'required_with:app.schedule|string|max:50', + 'app.schedule.*.user' => 'string|max:50', + 'app.scripts' => 'array', + 'app.scripts.*' => 'string', + ]; + } + + /** + * Get the web server validation rules. + * + * @return array + */ + protected function webServerRules() + { + return [ + 'web' => 'array', + 'web.scale' => 'integer', + 'web.size' => ['required_with:web', 'string', new ValidSize($this->project())], + 'web.tls' => 'string|in:self-signed', + 'web.serves' => [ + 'array', + 'min:1', + new ValidServeList($this->project(), $this->environment()->name) + ], + 'web.serves.*' => 'string', + 'web.scripts' => 'array', + 'web.scripts.*' => 'string', + ]; + } + + /** + * Get the web server validation rules. + * + * @return array + */ + protected function workerServerRules() + { + return [ + 'worker' => 'array', + 'worker.scale' => 'integer', + 'worker.size' => ['required_with:worker', 'string', new ValidSize($this->project())], + 'worker.daemons' => 'array', + 'worker.daemons.*.command' => 'required_with:worker.daemons|string', + 'worker.daemons.*.directory' => 'string', + 'worker.daemons.*.processes' => 'integer|min:1', + 'worker.daemons.*.wait' => 'integer|min:1', + 'worker.schedule.*' => 'array', + 'worker.schedule.*.command' => 'required_with:worker.schedule|string|max:1000', + 'worker.schedule.*.frequency' => 'required_with:worker.schedule|string|max:50', + 'worker.schedule.*.user' => 'string|max:50', + 'worker.scripts' => 'array', + 'worker.scripts.*' => 'string', + ]; + } + + /** + * Get the deployment validation rules. + * + * @return array + */ + protected function deploymentRules() + { + return [ + 'build' => 'array', + 'build.*' => 'string', + 'activate' => 'array', + 'activate.*' => 'string', + 'directories' => 'array', + 'directories.*' => 'string', + ]; + } + + /** + * Get the meta validation rules. + * + * @return array + */ + protected function metaRules() + { + return [ + 'meta' => 'array', + ]; + } +} diff --git a/app/HttpServer.php b/app/HttpServer.php new file mode 100644 index 00000000..0ff30bd1 --- /dev/null +++ b/app/HttpServer.php @@ -0,0 +1,77 @@ +meta['serves'] ?? [])->flatMap(function ($domain) { + return array_unique([$domain, $this->stack->nonCanonicalDomain($domain)]); + })->merge(collect([$this->stack->url.'.laravel.build']))->all(); + } + + /** + * Get the array of domains / ports the server should respond to. + * + * @return array + */ + public function shouldRespondToWithPorts() + { + return collect($this->shouldRespondTo())->flatMap(function ($domain) { + return [$domain.':80', $domain.':443']; + })->unique()->values()->all(); + } + + /** + * Get all of the server's vanity domain's with ports. + * + * @return array + */ + public function actualDomainsWithPorts() + { + return collect($this->shouldRespondToWithPorts())->reject(function ($domain) { + return Str::contains($domain, 'laravel.build'); + })->values()->all(); + } + + /** + * Get all of the server's vanity domain's with ports. + * + * @return array + */ + public function vanityDomainsWithPorts() + { + return collect($this->shouldRespondToWithPorts())->filter(function ($domain) { + return Str::contains($domain, 'laravel.build'); + })->unique()->values()->all(); + } + + /** + * Refresh the server's configuration. + * + * @return void + */ + public function sync() + { + SyncServer::dispatch($this); + } + + /** + * Determine if the server should self-sign TLS certificates. + * + * @return bool + */ + public function selfSignsCertificates() + { + return ($this->meta['tls'] ?? null) === 'self-signed'; + } +} diff --git a/app/InteractsWithSsh.php b/app/InteractsWithSsh.php new file mode 100644 index 00000000..2d5f5a11 --- /dev/null +++ b/app/InteractsWithSsh.php @@ -0,0 +1,219 @@ +markAsRunning(); + + $this->ensureWorkingDirectoryExists(); + + try { + $this->upload(); + } catch (ProcessTimedOutException $e) { + return $this->markAsTimedOut(); + } + + return $this->updateForResponse($this->runInline(sprintf( + 'bash %s 2>&1 | tee %s', + $this->scriptFile(), + $this->outputFile() + ), $this->options['timeout'] ?? 60)); + } + + /** + * Update the model for the given SSH response. + * + * @param object $response + * @return $this + */ + protected function updateForResponse($response) + { + return tap($this)->update([ + 'status' => $response->timedOut ? 'timeout' : 'finished', + 'exit_code' => $response->exitCode, + 'output' => $response->output, + ]); + } + + /** + * Run the given script in the background on a remote server. + * + * @return $this + */ + public function runInBackground() + { + $this->markAsRunning(); + + $this->addCallbackToScript(); + + $this->ensureWorkingDirectoryExists(); + + try { + $this->upload(); + } catch (ProcessTimedOutException $e) { + return $this->markAsTimedOut(); + } + + ShellProcessRunner::run($this->toProcess(sprintf( + '\'nohup bash %s >> %s 2>&1 &\'', + $this->scriptFile(), + $this->outputFile() + ), 10)); + + return $this; + } + + /** + * Add a callback to the script. + * + * @return void + */ + protected function addCallbackToScript() + { + $this->update([ + 'script' => view('scripts.tools.callback', [ + 'task' => $this, + 'path' => str_replace('.sh', '-script.sh', $this->scriptFile()), + 'token' => str_random(20), + ])->render(), + ]); + } + + /** + * Create the remote working directory for the task. + * + * @return void + */ + protected function ensureWorkingDirectoryExists() + { + $this->runInline('mkdir -p '.$this->path(), 10); + } + + /** + * Upload the given script to the server. + * + * @return bool + */ + protected function upload() + { + $process = (new Process(SecureShellCommand::forUpload( + $this->provisionable->ipAddress(), + $this->provisionable->port(), + $this->provisionable->ownerKeyPath(), + $this->user, + $localScript = $this->writeScript(), + $this->scriptFile() + ), base_path()))->setTimeout(15); + + $response = ShellProcessRunner::run($process); + + @unlink($localScript); + + return $response->exitCode === 0; + } + + /** + * Write the script to storage in preparation for upload. + * + * @return string + */ + protected function writeScript() + { + $hash = md5(str_random(20).$this->script); + + return tap(storage_path('app/scripts').'/'.$hash, function ($path) { + file_put_contents($path, $this->script); + }); + } + + /** + * Download the output of the task from the remote server. + * + * @param string|null $path + * @return string + */ + public function retrieveOutput($path = null) + { + return $this->runInline('tail --bytes=2000000 '.($path ?? $this->outputFile()), 10)->output; + } + + /** + * Run a given script inline on the remote server. + * + * @param string $script + * @param int $timeout + * @return ShellResponse + */ + protected function runInline($script, $timeout = 60) + { + $token = str_random(20); + + return ShellProcessRunner::run($this->toProcess('\'bash -s \' << \''.$token.'\' +'.$script.' +'.$token, $timeout)); + } + + /** + * Get the remote working directory path for the task. + * + * @return string + */ + protected function path() + { + return $this->user === 'root' + ? '/root/.cloud' + : '/home/cloud/.cloud'; + } + + /** + * Get the remote path to the script. + * + * @return string + */ + protected function scriptFile() + { + return $this->path().'/'.$this->id.'.sh'; + } + + /** + * Get the remote path to the output. + * + * @return string + */ + protected function outputFile() + { + return $this->path().'/'.$this->id.'.out'; + } + + /** + * Create a Process instance for the given script. + * + * @param string $script + * @param int $timeout + * @return Process + */ + protected function toProcess($script, $timeout) + { + return (new Process( + SecureShellCommand::forScript( + $this->provisionable->ipAddress(), + $this->provisionable->port(), + $this->provisionable->ownerKeyPath(), + $this->user, + $script + ) + ))->setTimeout($timeout); + } +} diff --git a/app/IpAddress.php b/app/IpAddress.php new file mode 100644 index 00000000..42c6a776 --- /dev/null +++ b/app/IpAddress.php @@ -0,0 +1,30 @@ +morphTo(); + } +} diff --git a/app/Jobs/Activate.php b/app/Jobs/Activate.php new file mode 100644 index 00000000..85a913d4 --- /dev/null +++ b/app/Jobs/Activate.php @@ -0,0 +1,82 @@ +deployment = $deployment; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->deployment->update([ + 'activation_task_id' => $this->activate()->id, + ]); + } + + /** + * Run the activation script on the server. + * + * @return \App\Task + */ + protected function activate() + { + $deployable = $this->deployment->deployable; + + return $deployable->runInBackground(new ActivateScript($this->deployment), [ + 'then' => [ + new CheckActivation($this->deployment->id), + new StartBackgroundServices($this->deployment->id), + ], + ]); + } + + /** + * Handle a job failure. + * + * @param \Exception $exception + * @return void + */ + public function failed(Exception $exception) + { + $this->deployment->project()->alerts()->create([ + 'stack_id' => $this->deployment->stack()->id, + 'type' => 'ActivationFailed', + 'exception' => (string) $exception, + 'meta' => [], + ]); + } +} diff --git a/app/Jobs/AddDnsRecord.php b/app/Jobs/AddDnsRecord.php new file mode 100644 index 00000000..09bd8adc --- /dev/null +++ b/app/Jobs/AddDnsRecord.php @@ -0,0 +1,45 @@ +stack = $stack; + } + + /** + * Execute the job. + * + * @param \App\Contracts\DnsProvider $dns + * @return void + */ + public function handle(DnsProvider $dns) + { + $dns->addRecord($this->stack); + } +} diff --git a/app/Jobs/Build.php b/app/Jobs/Build.php new file mode 100644 index 00000000..56bea8ec --- /dev/null +++ b/app/Jobs/Build.php @@ -0,0 +1,80 @@ +deployment = $deployment; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->deployment->update([ + 'build_task_id' => $this->build()->id, + ]); + } + + /** + * Run the build script on the server. + * + * @return \App\Task + */ + protected function build() + { + $deployable = $this->deployment->deployable; + + return $deployable->runInBackground(new BuildScript($this->deployment), [ + 'then' => [ + new CheckBuild($this->deployment->id), + ], + ]); + } + + /** + * Handle a job failure. + * + * @param \Exception $exception + * @return void + */ + public function failed(Exception $exception) + { + $this->deployment->deployment->project()->alerts()->create([ + 'stack_id' => $this->deployment->stack()->id, + 'type' => 'BuildFailed', + 'exception' => (string) $exception, + 'meta' => [], + ]); + } +} diff --git a/app/Jobs/CreateLoadBalancerIfNecessary.php b/app/Jobs/CreateLoadBalancerIfNecessary.php new file mode 100644 index 00000000..06c6273f --- /dev/null +++ b/app/Jobs/CreateLoadBalancerIfNecessary.php @@ -0,0 +1,55 @@ +stack = $stack; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $stack = $this->stack; + + if ($stack->balancers()->isEmpty()) { + if ($stack->appServer || count($stack->webServers) === 1) { + return; + } + + $stack->environment->project->provisionBalancer( + 'balancer', $stack->recommendedBalancerSize() + ); + } + + $stack->update(['balanced' => true]); + } +} diff --git a/app/Jobs/DeleteDatabaseBackup.php b/app/Jobs/DeleteDatabaseBackup.php new file mode 100644 index 00000000..0d27df43 --- /dev/null +++ b/app/Jobs/DeleteDatabaseBackup.php @@ -0,0 +1,54 @@ +provider = $provider; + $this->backupPath = $backupPath; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->provider->client()->delete( + $this->backupPath + ); + } +} diff --git a/app/Jobs/DeleteDnsRecord.php b/app/Jobs/DeleteDnsRecord.php new file mode 100644 index 00000000..85f8d3b6 --- /dev/null +++ b/app/Jobs/DeleteDnsRecord.php @@ -0,0 +1,53 @@ +name = $name; + $this->ipAddress = $ipAddress; + } + + /** + * Execute the job. + * + * @param \App\Contracts\DnsProvider $dns + * @return void + */ + public function handle(DnsProvider $dns) + { + $dns->deleteRecordByName($this->name, $this->ipAddress); + } +} diff --git a/app/Jobs/DeleteServerOnProvider.php b/app/Jobs/DeleteServerOnProvider.php new file mode 100644 index 00000000..c6480a0b --- /dev/null +++ b/app/Jobs/DeleteServerOnProvider.php @@ -0,0 +1,70 @@ +project = $project; + $this->providerServerId = $providerServerId; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->project->withProvider()->deleteServerById( + $this->providerServerId + ); + } + + /** + * Handle a job failure. + * + * @param \Exception $exception + * @return void + */ + public function failed(Exception $exception) + { + $this->project->alerts()->create([ + 'type' => 'ServerDeletionFailed', + 'exception' => (string) $exception, + 'meta' => [], + ]); + } +} diff --git a/app/Jobs/FinishTask.php b/app/Jobs/FinishTask.php new file mode 100644 index 00000000..319317cd --- /dev/null +++ b/app/Jobs/FinishTask.php @@ -0,0 +1,52 @@ +task = $task; + $this->exitCode = $exitCode; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->task->finish($this->exitCode); + } +} diff --git a/app/Jobs/HandlesStackProvisioningFailures.php b/app/Jobs/HandlesStackProvisioningFailures.php new file mode 100644 index 00000000..ebf7c70b --- /dev/null +++ b/app/Jobs/HandlesStackProvisioningFailures.php @@ -0,0 +1,29 @@ +stack->delete(); + } catch (Exception $e) { + report($e); + } + + $this->stack->environment->project->alerts()->create([ + 'type' => 'StackProvisioningFailed', + 'exception' => (string) $exception, + 'meta' => [], + ]); + } +} diff --git a/app/Jobs/InstallRepository.php b/app/Jobs/InstallRepository.php new file mode 100644 index 00000000..d24231eb --- /dev/null +++ b/app/Jobs/InstallRepository.php @@ -0,0 +1,50 @@ +stack = $stack; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->stack->deployBranch( + $this->stack->meta['initial_branch'], + $this->stack->meta['initial_build_commands'], + $this->stack->meta['initial_activation_commands'], + $this->stack->meta['initial_directories'], + $this->stack->meta['initial_daemons'] ?? [], + $this->stack->meta['initial_schedule'] ?? [] + ); + } +} diff --git a/app/Jobs/ManipulatesDaemons.php b/app/Jobs/ManipulatesDaemons.php new file mode 100644 index 00000000..6aec7bd0 --- /dev/null +++ b/app/Jobs/ManipulatesDaemons.php @@ -0,0 +1,56 @@ +deployment = $deployment; + } + + /** + * Get the script instance for the job. + * + * @return \App\Scripts\Script + */ + abstract public function script(); + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + if (empty($this->deployment->daemons())) { + return $this->delete(); + } + + if ($this->deployment->deployable->isWorker()) { + $this->deployment->deployable->runInBackground($this->script()); + } + } +} diff --git a/app/Jobs/MarkStackAsProvisioned.php b/app/Jobs/MarkStackAsProvisioned.php new file mode 100644 index 00000000..905562f9 --- /dev/null +++ b/app/Jobs/MarkStackAsProvisioned.php @@ -0,0 +1,49 @@ +stack = $stack; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->stack->markAsProvisioned(); + + Mail::to($this->stack->environment->project->user)->send( + new StackProvisioned($this->stack) + ); + } +} diff --git a/app/Jobs/MonitorDeployment.php b/app/Jobs/MonitorDeployment.php new file mode 100644 index 00000000..3623fbc7 --- /dev/null +++ b/app/Jobs/MonitorDeployment.php @@ -0,0 +1,98 @@ +deployment = $deployment; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + // If the deployment has been activated, then we can mark this as finished and + // delete the job. If it has failures, we will need to mark this deployment + // as failed and delete the job so the job monitoring will cease running. + if ($this->deployment->isActivated()) { + $this->deployment->markAsFinished(); + + return $this->delete(); + } + + if ($this->deployment->wasCancelled() || + $this->deployment->isTimedOut()) { + return $this->delete(); + } + + // if the deployment has failures, we will mark this as failed and delete the + // job so it no longer monitors this deployment. We'll also delete it when + // this deployment has been running for too long and has just timed out. + if ($this->deployment->hasFailures()) { + $this->deployment->markAsFailed(); + + return $this->delete(); + } + + if ($this->deployment->olderThan(20)) { + $this->deployment->markAsTimedOut(); + + return $this->delete(); + } + + // If this deploymnet has completed building we will fire off the activation + // job and release this job to keep monitoring this deployment's progress + // as activation continues. Then the deloyment will be really finished. + if ($this->deployment->isBuilt()) { + $this->deployment->activate(); + } + + $this->release(5); + } + + /** + * Handle a job failure. + * + * @param \Exception $exception + * @return void + */ + public function failed(Exception $exception) + { + $this->deployment->markAsFailed($exception); + } +} diff --git a/app/Jobs/PauseDaemons.php b/app/Jobs/PauseDaemons.php new file mode 100644 index 00000000..e1469b7e --- /dev/null +++ b/app/Jobs/PauseDaemons.php @@ -0,0 +1,19 @@ +deployment); + } +} diff --git a/app/Jobs/PromoteStack.php b/app/Jobs/PromoteStack.php new file mode 100644 index 00000000..d41582d7 --- /dev/null +++ b/app/Jobs/PromoteStack.php @@ -0,0 +1,94 @@ +stack = $stack; + + $this->options = array_merge([ + 'hooks' => true, + 'wait' => false, + ], $options); + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + [$stack, $previous] = [ + $this->stack, $this->stack->environment->promotedStack(), + ]; + + $stack->environment->markAsPromoted($stack); + + // Once the new stack has been promoted, we need to sync the balancers so that + // the production URLs will be served by the new stack. We will dispatch it + // synchronously so that it totally finishes within this task's lifetime. + Bus::dispatchNow( + new SyncBalancers($stack->project()) + ); + + // If this stack is in the production environment, we'll enable its background + // services such as its queue workers and schedulers. This will allow it to + // begin fully functioning as a new production stack for the application. + if ($stack->environment->isProduction() && + ! $this->options['wait']) { + $stack->allServers()->each->startBackgroundServices(); + } + + // If there was a previously promoted stack we will disable all the background + // services on that stack. This will prevent it from processing queued jobs + // or running schedulers, letting this newly promoted stack to take over. + if ($stack->environment->isProduction() && $previous) { + $previous->allServers()->each->stopBackgroundServices(); + } + + // If there was a previously promoted stack and the deployment hooks should be + // transferred to the newly promoted stack, we will update the stack ID for + // the previously promoted stack's hooks so they point to this new stack. + if ($previous && $this->options['hooks']) { + $previous->hooks()->update([ + 'stack_id' => $stack->id, + ]); + } + + $stack->environment->promotionLock()->release(); + } +} diff --git a/app/Jobs/ProvisionAppServer.php b/app/Jobs/ProvisionAppServer.php new file mode 100644 index 00000000..5300388c --- /dev/null +++ b/app/Jobs/ProvisionAppServer.php @@ -0,0 +1,19 @@ +provisionable = $provisionable; + } +} diff --git a/app/Jobs/ProvisionBalancer.php b/app/Jobs/ProvisionBalancer.php new file mode 100644 index 00000000..70c5beaa --- /dev/null +++ b/app/Jobs/ProvisionBalancer.php @@ -0,0 +1,33 @@ +provisionable = $provisionable; + } + + /** + * Perform any tasks after the server is provisioned. + * + * @return void + */ + protected function provisioned() + { + Mail::to($this->provisionable->project->user)->queue( + new BalancerProvisioned($this->provisionable) + ); + } +} diff --git a/app/Jobs/ProvisionDatabase.php b/app/Jobs/ProvisionDatabase.php new file mode 100644 index 00000000..1d6ca99c --- /dev/null +++ b/app/Jobs/ProvisionDatabase.php @@ -0,0 +1,33 @@ +provisionable = $provisionable; + } + + /** + * Perform any tasks after the server is provisioned. + * + * @return void + */ + protected function provisioned() + { + Mail::to($this->provisionable->project->user)->queue( + new DatabaseProvisioned($this->provisionable) + ); + } +} diff --git a/app/Jobs/ProvisionServers.php b/app/Jobs/ProvisionServers.php new file mode 100644 index 00000000..9e993e25 --- /dev/null +++ b/app/Jobs/ProvisionServers.php @@ -0,0 +1,72 @@ +stack = $stack; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->stack->update([ + 'initial_server_count' => count($this->stack->allServers()), + ]); + + foreach ($this->stack->allServers() as $server) { + if (! $server->providerServerId()) { + $server->update([ + 'provider_server_id' => $this->createServer($server), + ]); + } + + if (! $server->provisioningJobDispatched()) { + $server->provision(); + } + } + } + + /** + * Create a server on the server provider. + * + * @param \App\Contracts\Provisionable $server + * @return string + */ + protected function createServer($server) + { + $region = $this->stack->region(); + + return $this->stack->environment->project + ->withProvider() + ->createServer($server->name, $server->size, $region); + } +} diff --git a/app/Jobs/ProvisionWebServer.php b/app/Jobs/ProvisionWebServer.php new file mode 100644 index 00000000..ae4f4900 --- /dev/null +++ b/app/Jobs/ProvisionWebServer.php @@ -0,0 +1,19 @@ +provisionable = $provisionable; + } +} diff --git a/app/Jobs/ProvisionWorkerServer.php b/app/Jobs/ProvisionWorkerServer.php new file mode 100644 index 00000000..e8d48e10 --- /dev/null +++ b/app/Jobs/ProvisionWorkerServer.php @@ -0,0 +1,19 @@ +provisionable = $provisionable; + } +} diff --git a/app/Jobs/PruneStackTasks.php b/app/Jobs/PruneStackTasks.php new file mode 100644 index 00000000..ecedafb4 --- /dev/null +++ b/app/Jobs/PruneStackTasks.php @@ -0,0 +1,26 @@ +subDays(14)); + } +} diff --git a/app/Jobs/PruneTasks.php b/app/Jobs/PruneTasks.php new file mode 100644 index 00000000..1d98f63f --- /dev/null +++ b/app/Jobs/PruneTasks.php @@ -0,0 +1,26 @@ +subDays(21)); + } +} diff --git a/app/Jobs/RemoveKeyFromServer.php b/app/Jobs/RemoveKeyFromServer.php new file mode 100644 index 00000000..e07e8b20 --- /dev/null +++ b/app/Jobs/RemoveKeyFromServer.php @@ -0,0 +1,63 @@ +user = $user; + $this->provisionable = $provisionable; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->provisionable->run(new RemoveKeyFromServerScript( + 'cloud-user-'.$this->user->id + )); + } +} diff --git a/app/Jobs/RestartDaemons.php b/app/Jobs/RestartDaemons.php new file mode 100644 index 00000000..b207375e --- /dev/null +++ b/app/Jobs/RestartDaemons.php @@ -0,0 +1,21 @@ +deployment->deployable->createDaemonGeneration(); + + return new RestartDaemonsScript($this->deployment->fresh()); + } +} diff --git a/app/Jobs/RestoreDatabaseBackup.php b/app/Jobs/RestoreDatabaseBackup.php new file mode 100644 index 00000000..48ba185e --- /dev/null +++ b/app/Jobs/RestoreDatabaseBackup.php @@ -0,0 +1,51 @@ +restore = $restore; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->restore->markAsRunning(); + + $this->restore->database->runInBackground(new RestoreDatabaseBackupScript($this->restore), [ + 'then' => [ + new CheckDatabaseRestore($this->restore->id), + ], + ]); + } +} diff --git a/app/Jobs/RunStackTask.php b/app/Jobs/RunStackTask.php new file mode 100644 index 00000000..5adf1305 --- /dev/null +++ b/app/Jobs/RunStackTask.php @@ -0,0 +1,43 @@ +task = $task; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->task->run(); + } +} diff --git a/app/Jobs/ServerProvisioner.php b/app/Jobs/ServerProvisioner.php new file mode 100644 index 00000000..4bccb68b --- /dev/null +++ b/app/Jobs/ServerProvisioner.php @@ -0,0 +1,88 @@ +provisionable->isProvisioned()) { + try { + $this->provisioned(); + } catch (Exception $e) { + report($e); + } + + return $this->delete(); + } elseif ($this->provisionable->olderThan(15)) { + return $this->fail(ProvisioningTimeout::for($this->provisionable)); + } elseif ($this->provisionable->isProvisioning()) { + return $this->release(30); + } elseif ($this->provisionable->isReadyForProvisioning()) { + $this->provisionable->runProvisioningScript(); + } + + $this->release(30); + } + + /** + * Perform any tasks after the server is provisioned. + * + * @return void + */ + protected function provisioned() + { + // + } + + /** + * Handle a job failure. + * + * @param \Exception $exception + * @return void + */ + public function failed(Exception $exception) + { + try { + $this->provisionable->delete(); + } catch (Exception $e) { + report($e); + } + + $this->provisionable->project->alerts()->create([ + 'type' => 'ServerProvisioningFailed', + 'exception' => (string) $exception, + 'meta' => [], + ]); + } +} diff --git a/app/Jobs/StartDaemons.php b/app/Jobs/StartDaemons.php new file mode 100644 index 00000000..c619ba71 --- /dev/null +++ b/app/Jobs/StartDaemons.php @@ -0,0 +1,19 @@ +deployment); + } +} diff --git a/app/Jobs/StartScheduler.php b/app/Jobs/StartScheduler.php new file mode 100644 index 00000000..8f947c1b --- /dev/null +++ b/app/Jobs/StartScheduler.php @@ -0,0 +1,52 @@ +deployment = $deployment; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + if (empty($this->deployment->schedule())) { + return $this->delete(); + } + + if ($this->deployment->deployable->isWorker()) { + $this->deployment->deployable->runInBackground( + new StartSchedulerScript($this->deployment) + ); + } + } +} diff --git a/app/Jobs/StopDaemons.php b/app/Jobs/StopDaemons.php new file mode 100644 index 00000000..f9f1fb1d --- /dev/null +++ b/app/Jobs/StopDaemons.php @@ -0,0 +1,19 @@ +deployment); + } +} diff --git a/app/Jobs/StopScheduler.php b/app/Jobs/StopScheduler.php new file mode 100644 index 00000000..a910c3a6 --- /dev/null +++ b/app/Jobs/StopScheduler.php @@ -0,0 +1,52 @@ +deployment = $deployment; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + if (empty($this->deployment->schedule())) { + return $this->delete(); + } + + if ($this->deployment->deployable->isWorker()) { + $this->deployment->deployable->runInBackground( + new StopSchedulerScript($this->deployment) + ); + } + } +} diff --git a/app/Jobs/StoreDatabaseBackup.php b/app/Jobs/StoreDatabaseBackup.php new file mode 100644 index 00000000..72ca8649 --- /dev/null +++ b/app/Jobs/StoreDatabaseBackup.php @@ -0,0 +1,51 @@ +backup = $backup; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->backup->markAsRunning(); + + $this->backup->database->runInBackground(new StoreDatabaseBackupScript($this->backup), [ + 'then' => [ + new CheckDatabaseBackup($this->backup->id), + ], + ]); + } +} diff --git a/app/Jobs/SyncBalancer.php b/app/Jobs/SyncBalancer.php new file mode 100644 index 00000000..2a304887 --- /dev/null +++ b/app/Jobs/SyncBalancer.php @@ -0,0 +1,50 @@ +balancer = $balancer; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->balancer->syncNow(); + } +} diff --git a/app/Jobs/SyncBalancers.php b/app/Jobs/SyncBalancers.php new file mode 100644 index 00000000..6a9deaa3 --- /dev/null +++ b/app/Jobs/SyncBalancers.php @@ -0,0 +1,43 @@ +project = $project; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->project->balancers->each->sync(); + } +} diff --git a/app/Jobs/SyncNetwork.php b/app/Jobs/SyncNetwork.php new file mode 100644 index 00000000..a455d827 --- /dev/null +++ b/app/Jobs/SyncNetwork.php @@ -0,0 +1,80 @@ +database = $database; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + if (! $this->database->isProvisioned() || + ! $this->database->networkLock()->get()) { + return $this->release(15); + } + + $this->sync($this->database = $this->database->fresh()); + + $this->database->update([ + 'allows_access_from' => $this->database->shouldAllowAccessFrom(), + ]); + + $this->database->networkLock()->release(); + } + + /** + * Run the sync script for the given database and IP addresses. + * + * @param \App\Database $database + * @return \App\Task + */ + protected function sync(Database $database) + { + return $database->run(new SyncNetworkScript($database)); + } +} diff --git a/app/Jobs/SyncServer.php b/app/Jobs/SyncServer.php new file mode 100644 index 00000000..668888f2 --- /dev/null +++ b/app/Jobs/SyncServer.php @@ -0,0 +1,44 @@ +server = $server; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->server->run(new SyncServerScript($this->server)); + } +} diff --git a/app/Jobs/SyncServers.php b/app/Jobs/SyncServers.php new file mode 100644 index 00000000..ef3c7ef6 --- /dev/null +++ b/app/Jobs/SyncServers.php @@ -0,0 +1,43 @@ +stack = $stack; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->stack->httpServers()->each->sync(); + } +} diff --git a/app/Jobs/SyncStackNetwork.php b/app/Jobs/SyncStackNetwork.php new file mode 100644 index 00000000..bc784f20 --- /dev/null +++ b/app/Jobs/SyncStackNetwork.php @@ -0,0 +1,42 @@ +stack = $stack; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->stack->databases->each->syncNetwork(); + } +} diff --git a/app/Jobs/TimeOutDeploymentIfStillRunning.php b/app/Jobs/TimeOutDeploymentIfStillRunning.php new file mode 100644 index 00000000..91b427f7 --- /dev/null +++ b/app/Jobs/TimeOutDeploymentIfStillRunning.php @@ -0,0 +1,52 @@ +deployment = $deployment; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + if (! $this->deployment->hasEnded()) { + $this->deployment->markAsTimedOut(); + } + } +} diff --git a/app/Jobs/UnpauseDaemons.php b/app/Jobs/UnpauseDaemons.php new file mode 100644 index 00000000..b5a263f8 --- /dev/null +++ b/app/Jobs/UnpauseDaemons.php @@ -0,0 +1,19 @@ +deployment); + } +} diff --git a/app/Jobs/UpdateStackDnsRecords.php b/app/Jobs/UpdateStackDnsRecords.php new file mode 100644 index 00000000..e62ce141 --- /dev/null +++ b/app/Jobs/UpdateStackDnsRecords.php @@ -0,0 +1,60 @@ +project = $project; + $this->ipAddress = $ipAddress; + } + + /** + * Execute the job. + * + * @param \App\Contracts\DnsProvider $dns + * @return void + */ + public function handle(DnsProvider $dns) + { + $this->project->allStacks()->filter(function ($stack) { + return $stack->dns_address == $this->ipAddress; + })->each(function ($stack) use ($dns) { + $dns->addRecord($stack); + }); + } +} diff --git a/app/Jobs/WaitForDnsRecordToPropagate.php b/app/Jobs/WaitForDnsRecordToPropagate.php new file mode 100644 index 00000000..1c570d9e --- /dev/null +++ b/app/Jobs/WaitForDnsRecordToPropagate.php @@ -0,0 +1,54 @@ +stack = $stack; + } + + /** + * Execute the job. + * + * @param \App\Contracts\DnsProvider $dns + * @return void + */ + public function handle(DnsProvider $dns) + { + if (! $dns->propagated($this->stack)) { + return $this->release(15); + } + } +} diff --git a/app/Jobs/WaitForRepositoryInstallation.php b/app/Jobs/WaitForRepositoryInstallation.php new file mode 100644 index 00000000..f424b775 --- /dev/null +++ b/app/Jobs/WaitForRepositoryInstallation.php @@ -0,0 +1,52 @@ +stack = $stack; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + if ($this->stack->isDeploying()) { + return $this->release(15); + } + } +} diff --git a/app/Jobs/WaitForServersToFinishProvisioning.php b/app/Jobs/WaitForServersToFinishProvisioning.php new file mode 100644 index 00000000..8a78aa3d --- /dev/null +++ b/app/Jobs/WaitForServersToFinishProvisioning.php @@ -0,0 +1,71 @@ +stack = $stack; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + if (count($this->stack->allServers()) !== $this->stack->initial_server_count) { + return $this->fail(new StackProvisioningTimeout($this->stack)); + } elseif ($this->stack->serversAreProvisioned() && $this->balancerIsProvisioned()) { + return $this->delete(); + } elseif ($this->stack->olderThan(20)) { + return $this->fail(new StackProvisioningTimeout($this->stack)); + } + + $this->release(30); + } + + /** + * Determine if the project has a provisioned load balancer. + * + * @return bool + */ + protected function balancerIsProvisioned() + { + $balancers = $this->stack->environment->project->balancers; + + return count($balancers) > 0 ? $balancers->contains->isProvisioned() : true; + } +} diff --git a/app/Jobs/WaitForStackToFinishNetworking.php b/app/Jobs/WaitForStackToFinishNetworking.php new file mode 100644 index 00000000..0a4f48c7 --- /dev/null +++ b/app/Jobs/WaitForStackToFinishNetworking.php @@ -0,0 +1,55 @@ +stack = $stack; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + if (count($this->stack->databases) === 0) { + return $this->delete(); + } + + $this->stack->databases->every->networkIsSynced() + ? $this->delete() + : $this->release(15); + } +} diff --git a/app/Listeners/CheckPendingDeployments.php b/app/Listeners/CheckPendingDeployments.php new file mode 100644 index 00000000..840766a5 --- /dev/null +++ b/app/Listeners/CheckPendingDeployments.php @@ -0,0 +1,19 @@ +deployment->stack->deployPending(); + } +} diff --git a/app/Listeners/CreateAlert.php b/app/Listeners/CreateAlert.php new file mode 100644 index 00000000..3dd88b7d --- /dev/null +++ b/app/Listeners/CreateAlert.php @@ -0,0 +1,19 @@ +toAlert(); + } +} diff --git a/app/Listeners/ResetDeploymentStatus.php b/app/Listeners/ResetDeploymentStatus.php new file mode 100644 index 00000000..2a100f27 --- /dev/null +++ b/app/Listeners/ResetDeploymentStatus.php @@ -0,0 +1,19 @@ +stack()->resetDeploymentStatus(); + } +} diff --git a/app/Listeners/TrimAlertsForProject.php b/app/Listeners/TrimAlertsForProject.php new file mode 100644 index 00000000..1e852cb7 --- /dev/null +++ b/app/Listeners/TrimAlertsForProject.php @@ -0,0 +1,23 @@ +alert->project->alerts()->get(); + + if (count($alerts) > 30) { + $alerts->slice(30 - count($alerts))->each->delete(); + } + } +} diff --git a/app/Listeners/UpdateLastAlertTimestampForCollaborators.php b/app/Listeners/UpdateLastAlertTimestampForCollaborators.php new file mode 100644 index 00000000..a715d153 --- /dev/null +++ b/app/Listeners/UpdateLastAlertTimestampForCollaborators.php @@ -0,0 +1,23 @@ +affectedIds())->update( + ['last_alert_received_at' => new DateTime] + ); + } +} diff --git a/app/Mail/BalancerProvisioned.php b/app/Mail/BalancerProvisioned.php new file mode 100644 index 00000000..fc450c82 --- /dev/null +++ b/app/Mail/BalancerProvisioned.php @@ -0,0 +1,44 @@ +balancer = $balancer; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + return $this->subject('Balancer Created') + ->markdown('mail.balancer.provisioned', [ + 'balancer' => $this->balancer, + ]); + } +} diff --git a/app/Mail/DatabaseProvisioned.php b/app/Mail/DatabaseProvisioned.php new file mode 100644 index 00000000..4bb0793d --- /dev/null +++ b/app/Mail/DatabaseProvisioned.php @@ -0,0 +1,44 @@ +database = $database; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + return $this->subject('Database Created') + ->markdown('mail.database.provisioned', [ + 'database' => $this->database, + ]); + } +} diff --git a/app/Mail/StackProvisioned.php b/app/Mail/StackProvisioned.php new file mode 100644 index 00000000..5ef0cdb7 --- /dev/null +++ b/app/Mail/StackProvisioned.php @@ -0,0 +1,44 @@ +stack = $stack; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + return $this->subject('Stack Created') + ->markdown('mail.stack.provisioned', [ + 'stack' => $this->stack, + ]); + } +} diff --git a/app/MemoizesMethods.php b/app/MemoizesMethods.php new file mode 100644 index 00000000..8c405417 --- /dev/null +++ b/app/MemoizesMethods.php @@ -0,0 +1,13 @@ +projects->contains($balancer->project); + } +} diff --git a/app/Policies/DatabaseBackupPolicy.php b/app/Policies/DatabaseBackupPolicy.php new file mode 100644 index 00000000..cf28de0f --- /dev/null +++ b/app/Policies/DatabaseBackupPolicy.php @@ -0,0 +1,25 @@ +projects->contains($backup->database->project); + } +} diff --git a/app/Policies/DatabasePolicy.php b/app/Policies/DatabasePolicy.php new file mode 100644 index 00000000..80cf578c --- /dev/null +++ b/app/Policies/DatabasePolicy.php @@ -0,0 +1,37 @@ +projects->contains($database->project); + } + + /** + * Determine whether the user can delete the database. + * + * @param \App\User $user + * @param \App\Database $database + * @return mixed + */ + public function delete(User $user, Database $database) + { + return $user->projects->contains($database->project); + } +} diff --git a/app/Policies/DatabaseRestorePolicy.php b/app/Policies/DatabaseRestorePolicy.php new file mode 100644 index 00000000..a696ff0b --- /dev/null +++ b/app/Policies/DatabaseRestorePolicy.php @@ -0,0 +1,24 @@ +projects->contains($database->project); + } +} diff --git a/app/Policies/EnvironmentPolicy.php b/app/Policies/EnvironmentPolicy.php new file mode 100644 index 00000000..df112e65 --- /dev/null +++ b/app/Policies/EnvironmentPolicy.php @@ -0,0 +1,26 @@ +projects->contains($environment->project) || + $environment->creator->id == $user->id; + } +} diff --git a/app/Policies/ProjectPolicy.php b/app/Policies/ProjectPolicy.php new file mode 100644 index 00000000..6c88c963 --- /dev/null +++ b/app/Policies/ProjectPolicy.php @@ -0,0 +1,59 @@ +canAccessProject($project); + } + + /** + * Determine whether the user can create projects. + * + * @param \App\User $user + * @return mixed + */ + public function create(User $user) + { + return true; + } + + /** + * Determine whether the user can update the project's collaborators. + * + * @param \App\User $user + * @param \App\Project $project + * @return mixed + */ + public function updateCollaborators(User $user, Project $project) + { + return $user->projects->contains($project); + } + + /** + * Determine whether the user can delete the project. + * + * @param \App\User $user + * @param \App\Project $project + * @return mixed + */ + public function delete(User $user, Project $project) + { + return $user->projects->contains($project); + } +} diff --git a/app/Policies/ServerPolicy.php b/app/Policies/ServerPolicy.php new file mode 100644 index 00000000..b72323bd --- /dev/null +++ b/app/Policies/ServerPolicy.php @@ -0,0 +1,8 @@ +canAccessProject($stack->environment->project); + } + + /** + * Determine whether the user can delete the stack. + * + * @param \App\User $user + * @param \App\Stack $stack + * @return mixed + */ + public function delete(User $user, Stack $stack) + { + return $stack->creator->id == $user->id || + $user->projects->contains($stack->environment->project); + } +} diff --git a/app/Policies/WebServerPolicy.php b/app/Policies/WebServerPolicy.php new file mode 100644 index 00000000..014d412a --- /dev/null +++ b/app/Policies/WebServerPolicy.php @@ -0,0 +1,8 @@ + 'boolean', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array + */ + protected $guarded = []; + + /** + * Get all of the alerts for the project. + */ + public function alerts() + { + return $this->hasMany(Alert::class); + } + + /** + * The user that owns the project. + */ + public function user() + { + return $this->belongsTo(User::class, 'user_id'); + } + + /** + * All of the users the project is shared with. + */ + public function collaborators() + { + return $this->belongsToMany( + User::class, 'project_users', 'project_id', 'user_id' + )->using(Collaborator::class)->withPivot('permissions'); + } + + /** + * Share the project with the given user. + * + * @param User $user + * @return void + */ + public function shareWith(User $user) + { + $this->collaborators()->detach($user); + + $this->collaborators()->attach($user, ['permissions' => []]); + + unset($this->collaborators); + + ProjectShared::dispatch($this, $user); + } + + /** + * Stop sharing the project with the given user. + * + * @param User $user + * @return void + */ + public function stopSharingWith(User $user) + { + $this->collaborators()->detach($user); + + ProjectUnshared::dispatch($this, $user); + } + + /** + * Get the server provider for the project. + */ + public function serverProvider() + { + return $this->belongsTo(ServerProvider::class, 'server_provider_id'); + } + + /** + * Get the source control provider for the project. + */ + public function sourceProvider() + { + return $this->belongsTo(SourceProvider::class, 'source_provider_id'); + } + + /** + * Get all of the tasks for the project. + */ + public function tasks() + { + return $this->hasMany(Task::class)->latest(); + } + + /** + * Get the database associated with the project. + */ + public function databases() + { + return $this->hasMany(Database::class); + } + + /** + * Get all of the balancers associated with the project. + */ + public function balancers() + { + return $this->hasMany(Balancer::class); + } + + /** + * Get the largest available balancer. + * + * @return \App\Balancer|null + */ + public function largestAvailableBalancer() + { + return $this->balancersBySize()->first(); + } + + /** + * Get all of the load balancers for the project sorted by size. + * + * @return \Illuminate\Database\Eloqeunt\Collection + */ + public function balancersBySize() + { + return $this->balancers->sort(function ($a, $b) { + return $a->sizeInMegabytes() <=> $b->sizeInMegabytes(); + })->reverse()->values(); + } + + /** + * Get all of the certificates attached to the project. + */ + public function certificates() + { + return $this->hasMany(Certificate::class); + } + + /** + * Get all of the environments for the project. + */ + public function environments() + { + return $this->hasMany(Environment::class); + } + + /** + * Get all of the stacks for all environments. + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public function allStacks() + { + return $this->environments->flatMap->stacks; + } + + /** + * Get all of the certifiates assigned to stacks. + * + * @return \Illuminate\Support\Collection + */ + public function activeCertificates() + { + return $this->allStacks()->map->certificate; + } + + /** + * Start provisioning a new database for the project. + * + * @param string $name + * @param string $size + * @return \App\Database + */ + public function provisionDatabase($name, $size) + { + $id = $this->withProvider()->createServer($name, $size, $this->region); + + return tap($this->databases()->create([ + 'name' => $name, + 'size' => $size, + 'provider_server_id' => $id, + 'sudo_password' => str_random(40), + 'username' => 'cloud', + 'password' => str_random(40), + 'allows_access_from' => [], + 'status' => 'creating', + ]))->provision(); + } + + /** + * Start provisioning a new balancer for the project. + * + * @param string $name + * @param string $size + * @param bool $selfSigns + * @return \App\Balancer + */ + public function provisionBalancer($name, $size, $selfSigns = false) + { + $id = $this->withProvider()->createServer($name, $size, $this->region); + + return tap($this->balancers()->create([ + 'name' => $name, + 'size' => $size, + 'provider_server_id' => $id, + 'sudo_password' => str_random(40), + 'tls' => $selfSigns ? 'self-signed' : null, + 'status' => 'creating', + ]))->provision(); + } + + /** + * Get the provider gateway for the project. + * + * @return mixed + */ + public function withProvider() + { + return ServerProviderClientFactory::make($this->serverProvider); + } + + /** + * Purge all of the project's resources. + * + * @return void + */ + public function purge() + { + $this->databases->each->delete(); + $this->balancers->each->delete(); + $this->environments->each->delete(); + } + + /** + * Archive the given project and mark it for deletion. + * + * @return void + */ + public function archive() + { + $this->update([ + 'archived' => true, + ]); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php new file mode 100644 index 00000000..cdc9c8f9 --- /dev/null +++ b/app/Providers/AppServiceProvider.php @@ -0,0 +1,45 @@ +app->bind(YamlParser::class, LocalYamlParser::class); + $this->app->bind(DnsProvider::class, Route53::class); + + $this->app->bind(Route53Client::class, function () { + return new Route53Client([ + 'version' => 'latest', + 'region' => 'us-east-1', + 'credentials' => [ + 'key' => config('services.route53.key'), + 'secret' => config('services.route53.secret'), + ], + ]); + }); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php new file mode 100644 index 00000000..fd32c142 --- /dev/null +++ b/app/Providers/AuthServiceProvider.php @@ -0,0 +1,39 @@ + 'App\Policies\BalancerPolicy', + 'App\Database' => 'App\Policies\DatabasePolicy', + 'App\DatabaseBackup' => 'App\Policies\DatabaseBackupPolicy', + 'App\DatabaseRestore' => 'App\Policies\DatabaseRestorePolicy', + 'App\Project' => 'App\Policies\ProjectPolicy', + 'App\Environment' => 'App\Policies\EnvironmentPolicy', + 'App\Stack' => 'App\Policies\StackPolicy', + 'App\AppServer' => 'App\Policies\AppServerPolicy', + 'App\WebServer' => 'App\Policies\WebServerPolicy', + 'App\WorkerServer' => 'App\Policies\WorkerServerPolicy', + ]; + + /** + * Register any authentication / authorization services. + * + * @return void + */ + public function boot() + { + $this->registerPolicies(); + + Passport::routes(); + } +} diff --git a/app/Providers/BroadcastServiceProvider.php b/app/Providers/BroadcastServiceProvider.php new file mode 100644 index 00000000..352cce44 --- /dev/null +++ b/app/Providers/BroadcastServiceProvider.php @@ -0,0 +1,21 @@ + [ + 'App\Listeners\UpdateLastAlertTimestampForCollaborators', + 'App\Listeners\TrimAlertsForProject', + ], + + 'App\Events\DatabaseBackupRunning' => [ + // + ], + + 'App\Events\DatabaseBackupFinished' => [ + // + ], + + 'App\Events\DatabaseBackupFailed' => [ + // + ], + + 'App\Events\DatabaseRestoreRunning' => [ + // + ], + + 'App\Events\DatabaseRestoreFinished' => [ + // + ], + + 'App\Events\DatabaseRestoreFailed' => [ + // + ], + + 'App\Events\ProjectShared' => [ + // + ], + + 'App\Events\ProjectUnshared' => [ + // + ], + + 'App\Events\StackProvisioning' => [ + // + ], + + 'App\Events\StackProvisioned' => [ + 'App\Listeners\CreateAlert' + ], + + 'App\Events\StackDeleting' => [ + // + ], + + 'App\Events\DeploymentBuilding' => [ + // + ], + + 'App\Events\DeploymentActivating' => [ + // + ], + + 'App\Events\DeploymentFinished' => [ + 'App\Listeners\ResetDeploymentStatus', + 'App\Listeners\CreateAlert', + 'App\Listeners\CheckPendingDeployments', + ], + + 'App\Events\DeploymentTimedOut' => [ + 'App\Listeners\ResetDeploymentStatus', + 'App\Listeners\CreateAlert', + ], + + 'App\Events\DeploymentFailed' => [ + 'App\Listeners\ResetDeploymentStatus', + 'App\Listeners\CreateAlert', + ], + + 'App\Events\DeploymentCancelled' => [ + 'App\Listeners\ResetDeploymentStatus', + 'App\Listeners\CreateAlert', + ], + + 'App\Events\ServerDeploymentBuilt' => [ + // + ], + + 'App\Events\ServerDeploymentActivated' => [ + // + ], + + 'App\Events\ServerDeploymentFailed' => [ + // + ], + + 'App\Events\StackTaskRunning' => [ + // + ], + + 'App\Events\StackTaskFinished' => [ + // + ], + + 'App\Events\StackTaskFailed' => [ + // + ], + + 'App\Events\ServerTaskFinished' => [ + // + ], + + 'App\Events\ServerTaskFailed' => [ + // + ], + ]; + + /** + * Register any events for your application. + * + * @return void + */ + public function boot() + { + parent::boot(); + + // + } +} diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php new file mode 100644 index 00000000..ff300072 --- /dev/null +++ b/app/Providers/RouteServiceProvider.php @@ -0,0 +1,101 @@ +mapApiRoutes(); + $this->mapWebRoutes(); + $this->mapScheduleRoutes(); + } + + /** + * Define the "api" routes for the application. + * + * These routes are typically stateless. + * + * @return void + */ + protected function mapApiRoutes() + { + Route::prefix('api') + ->middleware('api') + ->namespace($this->namespace) + ->group(base_path('routes/api.php')); + } + + /** + * Define the "web" routes for the application. + * + * These routes all receive session state, CSRF protection, etc. + * + * @return void + */ + protected function mapWebRoutes() + { + Route::middleware('web') + ->namespace($this->namespace) + ->group(base_path('routes/web.php')); + } + + /** + * Define the "schedule" routes for the application. + * + * @return void + */ + protected function mapScheduleRoutes() + { + Route::namespace($this->namespace) + ->group(base_path('routes/schedule.php')); + } +} diff --git a/app/Provisionable.php b/app/Provisionable.php new file mode 100644 index 00000000..de5f91e3 --- /dev/null +++ b/app/Provisionable.php @@ -0,0 +1,259 @@ +project->id; + } + + /** + * Get the project that owns the server. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function project() + { + return $this->belongsTo(Project::class, 'project_id'); + } + + /** + * Get the IP address information for the server. + */ + public function address() + { + return $this->morphOne(IpAddress::class, 'addressable'); + } + + /** + * Get the tasks that belong to the server. + */ + public function tasks() + { + return $this->morphMany(Task::class, 'provisionable'); + } + + /** + * Get the server's provider server ID. + * + * @return string + */ + public function providerServerId() + { + return $this->provider_server_id; + } + + /** + * Get the IP address for the server. + * + * @return string + */ + public function ipAddress() + { + return $this->address ? $this->address->public_address : null; + } + + /** + * Get the private IP address for the server. + * + * @return string + */ + public function privateIpAddress() + { + return $this->address ? $this->address->private_address : null; + } + + /** + * Get the SSH port for the server. + * + * @return string + */ + public function port() + { + return $this->port; + } + + /** + * Get the size of the server in megabytes. + * + * @return int + */ + public function sizeInMegabytes() + { + return $this->project->withProvider()->sizeInMegabytes($this->size); + } + + /** + * Get the path to the server owner's worker SSH key. + * + * @return string + */ + public function ownerKeyPath() + { + return $this->project->user->keyPath(); + } + + /** + * Set the SSH key attributes on the model. + * + * @param object $value + * @return void + */ + public function setKeypairAttribute($value) + { + $this->attributes = [ + 'public_key' => $value->publicKey, + 'private_key' => $value->privateKey, + ] + $this->attributes; + } + + /** + * Determine if the server is ready for provisioning. + * + * @return bool + */ + public function isReadyForProvisioning() + { + if (! $this->ipAddress()) { + $this->retrieveIpAddresses(); + } + + $canAccess = $this->fresh()->ipAddress() && $this->run( + new GetCurrentDirectory + )->output == '/root'; + + if ($canAccess) { + $apt = $this->run(new GetAptLockStatus); + } else { + return false; + } + + return $apt->successful() && + $apt->output === ''; + } + + /** + * Attempt to retrieve and store the server's IP addresses. + * + * @return void + */ + protected function retrieveIpAddresses() + { + $project = $this->project; + + list($publicIp, $privateIp) = [ + $project->withProvider()->getPublicIpAddress($this), + $project->withProvider()->getPrivateIpAddress($this), + ]; + + if (! $publicIp || ! $privateIp) { + return; + } + + $this->address()->create([ + 'public_address' => $publicIp, + 'private_address' => $privateIp, + ]); + } + + /** + * Determine if the server is currently provisioning. + * + * @return bool + */ + public function isProvisioning() + { + return $this->status == 'provisioning'; + } + + /** + * Mark the server as provisioning. + * + * @return $this + */ + public function markAsProvisioning() + { + return tap($this)->update(['status' => 'provisioning']); + } + + /** + * Determine if the server is currently provisioned. + * + * @return bool + */ + public function isProvisioned() + { + return $this->status == 'provisioned'; + } + + /** + * Mark the server as provisioned. + * + * @return $this + */ + public function markAsProvisioned() + { + return tap($this)->update(['status' => 'provisioned']); + } + + /** + * Determine if the provisioning job has been dispatched. + * + * @return bool + */ + public function provisioningJobDispatched() + { + return ! is_null($this->provisioning_job_dispatched_at); + } + + /** + * Run the given script on the server. + * + * @param \App\Scripts\Script $script + * @param array $options + * @return Task + */ + public function run(Script $script, array $options = []) + { + if (! array_key_exists('timeout', $options)) { + $options['timeout'] = $script->timeout(); + } + + return $this->tasks()->create([ + 'project_id' => $this->projectId(), + 'name' => $script->name(), + 'user' => $script->sshAs, + 'options' => $options, + 'script' => (string) $script, + 'output' => '', + ])->run(); + } + + /** + * Run the given script in the background the server. + * + * @param \App\Scripts\Script $script + * @param array $options + * @return Task + */ + public function runInBackground(Script $script, array $options = []) + { + return TaskFactory::createFromScript( + $this, $script, $options + )->runInBackground(); + } +} diff --git a/app/Prunable.php b/app/Prunable.php new file mode 100644 index 00000000..c4c4bf67 --- /dev/null +++ b/app/Prunable.php @@ -0,0 +1,34 @@ +getTable().' where created_at <= ? order by id limit '.$limit, + [$date->format('Y-m-d H:i:s')] + ); + + $total += $affected; + } while ($affected > 0); + + return $total; + } +} diff --git a/app/Rules/DatabaseIsProvisioned.php b/app/Rules/DatabaseIsProvisioned.php new file mode 100644 index 00000000..e6089dc2 --- /dev/null +++ b/app/Rules/DatabaseIsProvisioned.php @@ -0,0 +1,57 @@ +project = $project; + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + if (! $this->project instanceof Project) { + return true; + } + + if (is_null($database = $this->project->databases->where('name', $value)->first())) { + return true; + } + + return $database->status == 'provisioned'; + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return 'The specified database has not finished provisioning.'; + } +} diff --git a/app/Rules/StackIsPromotable.php b/app/Rules/StackIsPromotable.php new file mode 100644 index 00000000..548b56ae --- /dev/null +++ b/app/Rules/StackIsPromotable.php @@ -0,0 +1,49 @@ +stack = $stack; + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + return $this->stack->promotable(); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return 'The specified stack is not promotable. Please verify the stack has a "serves" directive.'; + } +} diff --git a/app/Rules/ValidAppServerStack.php b/app/Rules/ValidAppServerStack.php new file mode 100644 index 00000000..299346da --- /dev/null +++ b/app/Rules/ValidAppServerStack.php @@ -0,0 +1,48 @@ +request = $request; + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + return empty($value) || (empty($this->request->web) && empty($this->request->worker)); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return 'App servers may not be provisioned with web and worker servers.'; + } +} diff --git a/app/Rules/ValidBranch.php b/app/Rules/ValidBranch.php new file mode 100644 index 00000000..bb9b7d8b --- /dev/null +++ b/app/Rules/ValidBranch.php @@ -0,0 +1,64 @@ +source = $source; + $this->repository = $repository; + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + if (! $this->source instanceof SourceProvider) { + return false; + } + + return $this->source->client()->validRepository( + $this->repository, $value + ); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return 'The given repository or branch is invalid.'; + } +} diff --git a/app/Rules/ValidCommit.php b/app/Rules/ValidCommit.php new file mode 100644 index 00000000..1f9f3905 --- /dev/null +++ b/app/Rules/ValidCommit.php @@ -0,0 +1,64 @@ +source = $source; + $this->repository = $repository; + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + if (! $this->source instanceof SourceProvider) { + return false; + } + + return $this->source->client()->validCommit( + $this->repository, $value + ); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return 'The given commit hash is invalid.'; + } +} diff --git a/app/Rules/ValidDatabaseName.php b/app/Rules/ValidDatabaseName.php new file mode 100644 index 00000000..05bdba64 --- /dev/null +++ b/app/Rules/ValidDatabaseName.php @@ -0,0 +1,55 @@ +project = $project; + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + if (! $this->project instanceof Project) { + return true; + } + + return ! is_null( + $this->project->databases->where('name', $value)->first() + ); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return 'The specified database name is invalid.'; + } +} diff --git a/app/Rules/ValidRepository.php b/app/Rules/ValidRepository.php new file mode 100644 index 00000000..34e8b3eb --- /dev/null +++ b/app/Rules/ValidRepository.php @@ -0,0 +1,64 @@ +source = $source; + $this->branch = $branch; + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + if (! $this->source instanceof SourceProvider) { + return false; + } + + return $this->source->client()->validRepository( + $value, $this->branch + ); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return 'The given repository or branch is invalid.'; + } +} diff --git a/app/Rules/ValidServeList.php b/app/Rules/ValidServeList.php new file mode 100644 index 00000000..1b0ba769 --- /dev/null +++ b/app/Rules/ValidServeList.php @@ -0,0 +1,74 @@ +project = $project; + $this->except = $exceptEnvironment; + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + if (! $this->project instanceof Project || empty($value)) { + return true; + } + + foreach ($this->project->environments as $environment) { + if ($environment->name == $this->except) { + continue; + } + + foreach ($environment->stacks as $stack) { + if (array_intersect($value, $stack->serves())) { + return false; + } + } + } + + return true; + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return 'These domains are already being served by another environment.'; + } +} diff --git a/app/Rules/ValidSize.php b/app/Rules/ValidSize.php new file mode 100644 index 00000000..14c474fc --- /dev/null +++ b/app/Rules/ValidSize.php @@ -0,0 +1,53 @@ +project = $project; + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + if (! $this->project instanceof Project) { + return true; + } + + return $this->project->serverProvider->validSize($value); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return 'The specified server size is invalid.'; + } +} diff --git a/app/Rules/ValidSourceName.php b/app/Rules/ValidSourceName.php new file mode 100644 index 00000000..922dba54 --- /dev/null +++ b/app/Rules/ValidSourceName.php @@ -0,0 +1,55 @@ +project = $project; + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + if (! $this->project instanceof Project) { + return true; + } + + return ! is_null( + $this->project->user->sourceProviders->where('name', $value)->first() + ); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return 'The given source control provider name is invalid.'; + } +} diff --git a/app/Scripts/Activate.php b/app/Scripts/Activate.php new file mode 100644 index 00000000..3eaccbde --- /dev/null +++ b/app/Scripts/Activate.php @@ -0,0 +1,77 @@ +deployment = $deployment; + } + + /** + * Get the name of the script. + * + * @return string + */ + public function name() + { + return "Activating Deployment ({$this->deployment->stack()->name})"; + } + + /** + * Get the contents of the script. + * + * @return string + */ + public function script() + { + return view('scripts.deployment.activate', [ + 'script' => $this, + 'deployment' => $this->deployment, + 'deployable' => $this->deployment->deployable, + ])->render(); + } + + /** + * Determine if the script should restart FPM. + * + * @return bool + */ + public function shouldRestartFpm() + { + return ! $this->deployment->deployable->isTrueWorker(); + } + + /** + * Get the timeout for the script. + * + * @return int|null + */ + public function timeout() + { + return 20 * 60; + } +} diff --git a/app/Scripts/AddKeyToServer.php b/app/Scripts/AddKeyToServer.php new file mode 100644 index 00000000..6c78eef8 --- /dev/null +++ b/app/Scripts/AddKeyToServer.php @@ -0,0 +1,76 @@ +key = $key; + $this->name = $name; + } + + /** + * Get the name of the script. + * + * @return string + */ + public function name() + { + return "Syncing SSH Key"; + } + + /** + * Get the contents of the script. + * + * @return string + */ + public function script() + { + return view('scripts.provisionable.addKey', [ + 'name' => $this->name, + 'key' => $this->key, + ])->render(); + } + + /** + * Get the timeout for the script. + * + * @return int|null + */ + public function timeout() + { + return 10; + } +} diff --git a/app/Scripts/Build.php b/app/Scripts/Build.php new file mode 100644 index 00000000..771dee51 --- /dev/null +++ b/app/Scripts/Build.php @@ -0,0 +1,68 @@ +deployment = $deployment; + } + + /** + * Get the name of the script. + * + * @return string + */ + public function name() + { + return "Building Deployment ({$this->deployment->stack()->name})"; + } + + /** + * Get the contents of the script. + * + * @return string + */ + public function script() + { + return view('scripts.deployment.build', [ + 'script' => $this, + 'deployment' => $this->deployment, + 'deployable' => $this->deployment->deployable, + 'directories' => $this->deployment->deployment->directories, + ])->render(); + } + + /** + * Get the timeout for the script. + * + * @return int|null + */ + public function timeout() + { + return 20 * 60; + } +} diff --git a/app/Scripts/DaemonScript.php b/app/Scripts/DaemonScript.php new file mode 100644 index 00000000..4b2e6ddd --- /dev/null +++ b/app/Scripts/DaemonScript.php @@ -0,0 +1,46 @@ +deployment = $deployment; + } + + /** + * Get the name of the script to run. + * + * @return string + */ + abstract public function scriptName(); + + /** + * Get the contents of the script. + * + * @return string + */ + public function script() + { + return view($this->scriptName(), [ + 'script' => $this, + 'generation' => $this->deployment->currentDaemonGeneration(), + ])->render(); + } +} diff --git a/app/Scripts/GetAptLockStatus.php b/app/Scripts/GetAptLockStatus.php new file mode 100644 index 00000000..1ded2360 --- /dev/null +++ b/app/Scripts/GetAptLockStatus.php @@ -0,0 +1,34 @@ +deployment->deployable->name})"; + } + + /** + * Get the name of the script to run. + * + * @return string + */ + public function scriptName() + { + return 'scripts.daemons.pause'; + } +} diff --git a/app/Scripts/ProvisionAppServer.php b/app/Scripts/ProvisionAppServer.php new file mode 100644 index 00000000..3fbdc651 --- /dev/null +++ b/app/Scripts/ProvisionAppServer.php @@ -0,0 +1,55 @@ +server = $server; + } + + /** + * Get the name of the script. + * + * @return string + */ + public function name() + { + return "Provisioning App Server ({$this->server->name})"; + } + + /** + * Get the contents of the script. + * + * @return string + */ + public function script() + { + return view('scripts.app.provision', [ + 'script' => $this, + 'server' => $this->server, + 'databasePassword' => $this->server->database_password, + 'customScripts' => $this->server->stack->meta['scripts']['app'] ?? [], + ])->render(); + } +} diff --git a/app/Scripts/ProvisionBalancer.php b/app/Scripts/ProvisionBalancer.php new file mode 100644 index 00000000..054b67d3 --- /dev/null +++ b/app/Scripts/ProvisionBalancer.php @@ -0,0 +1,45 @@ +balancer = $balancer; + } + + /** + * Get the contents of the script. + * + * @return string + */ + public function script() + { + return view('scripts.balancer.provision', ['script' => $this])->render(); + } +} diff --git a/app/Scripts/ProvisionDatabase.php b/app/Scripts/ProvisionDatabase.php new file mode 100644 index 00000000..515ed779 --- /dev/null +++ b/app/Scripts/ProvisionDatabase.php @@ -0,0 +1,49 @@ +database = $database; + } + + /** + * Get the contents of the script. + * + * @return string + */ + public function script() + { + return view('scripts.database.provision', [ + 'script' => $this, + 'database' => $this->database, + 'databasePassword' => $this->database->password, + ])->render(); + } +} diff --git a/app/Scripts/ProvisionWebServer.php b/app/Scripts/ProvisionWebServer.php new file mode 100644 index 00000000..bde2bca9 --- /dev/null +++ b/app/Scripts/ProvisionWebServer.php @@ -0,0 +1,54 @@ +server = $server; + } + + /** + * Get the name of the script. + * + * @return string + */ + public function name() + { + return "Provisioning Web Server ({$this->server->name})"; + } + + /** + * Get the contents of the script. + * + * @return string + */ + public function script() + { + return view('scripts.web.provision', [ + 'script' => $this, + 'server' => $this->server, + 'customScripts' => $this->server->stack->meta['scripts']['web'] ?? [], + ])->render(); + } +} diff --git a/app/Scripts/ProvisionWorkerServer.php b/app/Scripts/ProvisionWorkerServer.php new file mode 100644 index 00000000..72a0960c --- /dev/null +++ b/app/Scripts/ProvisionWorkerServer.php @@ -0,0 +1,51 @@ +server = $server; + } + + /** + * Get the name of the script. + * + * @return string + */ + public function name() + { + return "Provisioning Worker Server ({$this->server->name})"; + } + + /** + * Get the contents of the script. + * + * @return string + */ + public function script() + { + return view('scripts.worker.provision', [ + 'script' => $this, + 'customScripts' => $this->server->stack->meta['scripts']['worker'] ?? [], + ])->render(); + } +} diff --git a/app/Scripts/ProvisioningScript.php b/app/Scripts/ProvisioningScript.php new file mode 100644 index 00000000..469c4b25 --- /dev/null +++ b/app/Scripts/ProvisioningScript.php @@ -0,0 +1,26 @@ +provisionable = $provisionable; + } +} diff --git a/app/Scripts/RemoveKeyFromServer.php b/app/Scripts/RemoveKeyFromServer.php new file mode 100644 index 00000000..2b66a9dd --- /dev/null +++ b/app/Scripts/RemoveKeyFromServer.php @@ -0,0 +1,66 @@ +name = $name; + } + + /** + * Get the name of the script. + * + * @return string + */ + public function name() + { + return "Removing SSH Key"; + } + + /** + * Get the contents of the script. + * + * @return string + */ + public function script() + { + return view('scripts.provisionable.removeKey', [ + 'name' => $this->name, + ])->render(); + } + + /** + * Get the timeout for the script. + * + * @return int|null + */ + public function timeout() + { + return 10; + } +} diff --git a/app/Scripts/RestartDaemons.php b/app/Scripts/RestartDaemons.php new file mode 100644 index 00000000..026115c1 --- /dev/null +++ b/app/Scripts/RestartDaemons.php @@ -0,0 +1,54 @@ +deployment->deployable->name})"; + } + + /** + * Get the name of the script to run. + * + * @return string + */ + public function scriptName() + { + return 'scripts.daemon.restart'; + } + + /** + * Get the daemon configuration script. + * + * @return string + */ + public function daemonConfiguration() + { + return view('scripts.daemon.build', [ + 'script' => $this, + 'deployment' => $this->deployment, + 'generation' => $this->deployment->currentDaemonGeneration(), + ])->render(); + } + + /** + * Get the daemon activation script. + * + * @return string + */ + public function activateDaemons() + { + return view('scripts.daemon.activate', [ + 'script' => $this, + 'generation' => $this->deployment->currentDaemonGeneration(), + 'previousGenerations' => $this->deployment->previousDaemonGenerations(), + ])->render(); + } +} diff --git a/app/Scripts/RestoreDatabaseBackup.php b/app/Scripts/RestoreDatabaseBackup.php new file mode 100644 index 00000000..611b8565 --- /dev/null +++ b/app/Scripts/RestoreDatabaseBackup.php @@ -0,0 +1,67 @@ +restore = $restore; + } + + /** + * Get the name of the script. + * + * @return string + */ + public function name() + { + return "Restoring Database Backup ({$this->restore->database->name})"; + } + + /** + * Get the contents of the script. + * + * @return string + */ + public function script() + { + return view('scripts.database.restore', [ + 'script' => $this, + 'restore' => $this->restore, + 'backup' => $this->restore->backup, + ])->render(); + } + + /** + * Get the timeout for the script. + * + * @return int|null + */ + public function timeout() + { + return 3600; + } +} diff --git a/app/Scripts/RunServerTask.php b/app/Scripts/RunServerTask.php new file mode 100644 index 00000000..5155838f --- /dev/null +++ b/app/Scripts/RunServerTask.php @@ -0,0 +1,64 @@ +task = $task; + $this->sshAs = $task->stackTask->user; + } + + /** + * Get the name of the script. + * + * @return string + */ + public function name() + { + return "Running Server Task"; + } + + /** + * Get the contents of the script. + * + * @return string + */ + public function script() + { + return implode(PHP_EOL, $this->task->commands); + } + + /** + * Get the timeout for the script. + * + * @return int|null + */ + public function timeout() + { + return 60 * 60; + } +} diff --git a/app/Scripts/Script.php b/app/Scripts/Script.php new file mode 100644 index 00000000..e04c3734 --- /dev/null +++ b/app/Scripts/Script.php @@ -0,0 +1,52 @@ +name ?? ''; + } + + /** + * Get the timeout for the script. + * + * @return int|null + */ + public function timeout() + { + return Task::DEFAULT_TIMEOUT; + } + + /** + * Render the script as a string. + * + * @return string + */ + public function __toString() + { + return $this->script(); + } +} diff --git a/app/Scripts/Sleep.php b/app/Scripts/Sleep.php new file mode 100644 index 00000000..9e589058 --- /dev/null +++ b/app/Scripts/Sleep.php @@ -0,0 +1,23 @@ +deployment->deployable->name})"; + } + + /** + * Get the name of the script to run. + * + * @return string + */ + public function scriptName() + { + return 'scripts.daemon.start'; + } +} diff --git a/app/Scripts/StartScheduler.php b/app/Scripts/StartScheduler.php new file mode 100644 index 00000000..a91668d0 --- /dev/null +++ b/app/Scripts/StartScheduler.php @@ -0,0 +1,58 @@ +deployment = $deployment; + } + + /** + * Get the name of the script. + * + * @return string + */ + public function name() + { + return "Starting Scheduler ({$this->deployment->deployable->name})"; + } + + /** + * Get the contents of the script. + * + * @return string + */ + public function script() + { + return view('scripts.scheduler.start', [ + 'deployment' => $this->deployment, + ])->render(); + } + + /** + * Get the timeout for the script. + * + * @return int|null + */ + public function timeout() + { + return 15; + } +} diff --git a/app/Scripts/StopDaemons.php b/app/Scripts/StopDaemons.php new file mode 100644 index 00000000..491f745f --- /dev/null +++ b/app/Scripts/StopDaemons.php @@ -0,0 +1,26 @@ +deployment->deployable->name})"; + } + + /** + * Get the name of the script to run. + * + * @return string + */ + public function scriptName() + { + return 'scripts.daemon.stop'; + } +} diff --git a/app/Scripts/StopScheduler.php b/app/Scripts/StopScheduler.php new file mode 100644 index 00000000..4967447e --- /dev/null +++ b/app/Scripts/StopScheduler.php @@ -0,0 +1,56 @@ +deployment = $deployment; + } + + /** + * Get the name of the script. + * + * @return string + */ + public function name() + { + return "Stopping Scheduler ({$this->deployment->deployable->name})"; + } + + /** + * Get the contents of the script. + * + * @return string + */ + public function script() + { + return view('scripts.scheduler.stop')->render(); + } + + /** + * Get the timeout for the script. + * + * @return int|null + */ + public function timeout() + { + return 15; + } +} diff --git a/app/Scripts/StoreDatabaseBackup.php b/app/Scripts/StoreDatabaseBackup.php new file mode 100644 index 00000000..2ef2bdfe --- /dev/null +++ b/app/Scripts/StoreDatabaseBackup.php @@ -0,0 +1,66 @@ +backup = $backup; + } + + /** + * Get the name of the script. + * + * @return string + */ + public function name() + { + return "Storing Database Backup ({$this->backup->database->name})"; + } + + /** + * Get the contents of the script. + * + * @return string + */ + public function script() + { + return view('scripts.database.backup', [ + 'script' => $this, + 'backup' => $this->backup, + ])->render(); + } + + /** + * Get the timeout for the script. + * + * @return int|null + */ + public function timeout() + { + return 3600; + } +} diff --git a/app/Scripts/SyncBalancer.php b/app/Scripts/SyncBalancer.php new file mode 100644 index 00000000..d8386589 --- /dev/null +++ b/app/Scripts/SyncBalancer.php @@ -0,0 +1,107 @@ +balancer = $balancer; + } + + /** + * Get the name of the script. + * + * @return string + */ + public function name() + { + return "Syncing Load Balancer ({$this->balancer->name})"; + } + + /** + * Get the contents of the script. + * + * @return string + */ + public function script() + { + return view('scripts.balancer.sync', ['script' => $this])->render(); + } + + /** + * Get the Caddy server configuration for the actual domains. + * + * @return string + */ + public function actualDomainConfiguration() + { + return collect($this->balancer->project->allStacks())->flatMap(function ($stack) { + return $this->balancerConfigurations( + $stack, + $stack->actualDomainsWithPorts(), + $stack->privateWebAddresses() + ); + })->implode(PHP_EOL); + } + + /** + * Get the Caddy server configuration for the vanity domains. + * + * @return string + */ + public function vanityDomainConfiguration() + { + return collect($this->balancer->project->allStacks())->flatMap(function ($stack) { + return $this->balancerConfigurations( + $stack, + $stack->vanityDomainsWithPorts(), + $stack->privateWebAddresses() + ); + })->implode(PHP_EOL); + } + + /** + * Get the balancer configurations for the given domain and proxies. + * + * @param \App\Stack $stack + * @param array $domains + * @param array $proxyTo + * @return array + */ + protected function balancerConfigurations(Stack $stack, $domains, $proxyTo) + { + return collect($domains)->map(function ($domain) use ($stack, $proxyTo) { + return (new CaddyBalancerConfiguration( + $this->balancer, $stack, $domain, $proxyTo + ))->render(); + })->all(); + } + + /** + * Get the timeout for the script. + * + * @return int|null + */ + public function timeout() + { + return 15; + } +} diff --git a/app/Scripts/SyncNetwork.php b/app/Scripts/SyncNetwork.php new file mode 100644 index 00000000..4b8133d4 --- /dev/null +++ b/app/Scripts/SyncNetwork.php @@ -0,0 +1,61 @@ +database = $database; + } + + /** + * Get the name of the script. + * + * @return string + */ + public function name() + { + return "Networking Database Servers ({$this->database->name})"; + } + + /** + * Get the contents of the script. + * + * @return string + */ + public function script() + { + return view('scripts.database.network', [ + 'script' => $this, + 'database' => $this->database, + 'ipAddresses' => $this->database->shouldAllowAccessFrom(), + 'previousIpAddresses' => $this->database->allows_access_from, + ])->render(); + } + + /** + * Get the timeout for the script. + * + * @return int|null + */ + public function timeout() + { + return 15; + } +} diff --git a/app/Scripts/SyncServer.php b/app/Scripts/SyncServer.php new file mode 100644 index 00000000..5719c289 --- /dev/null +++ b/app/Scripts/SyncServer.php @@ -0,0 +1,58 @@ +server = $server; + } + + /** + * Get the name of the script. + * + * @return string + */ + public function name() + { + return "Syncing Server Configuration ({$this->server->name})"; + } + + /** + * Get the contents of the script. + * + * @return string + */ + public function script() + { + return view('scripts.server.sync', ['script' => $this])->render(); + } + + /** + * Get the timeout for the script. + * + * @return int|null + */ + public function timeout() + { + return 15; + } +} diff --git a/app/Scripts/UnpauseDaemons.php b/app/Scripts/UnpauseDaemons.php new file mode 100644 index 00000000..99c6cc7c --- /dev/null +++ b/app/Scripts/UnpauseDaemons.php @@ -0,0 +1,26 @@ +deployment->deployable->name})"; + } + + /** + * Get the name of the script to run. + * + * @return string + */ + public function scriptName() + { + return 'scripts.daemon.unpause'; + } +} diff --git a/app/Scripts/WriteDummyFile.php b/app/Scripts/WriteDummyFile.php new file mode 100644 index 00000000..2fef4d8e --- /dev/null +++ b/app/Scripts/WriteDummyFile.php @@ -0,0 +1,23 @@ + /root/dummy'; + } +} diff --git a/app/Scripts/WritesCaddyServerConfigurations.php b/app/Scripts/WritesCaddyServerConfigurations.php new file mode 100644 index 00000000..4f20f7e0 --- /dev/null +++ b/app/Scripts/WritesCaddyServerConfigurations.php @@ -0,0 +1,36 @@ +server->actualDomainsWithPorts())) { + return ''; + } + + return collect($this->server->actualDomainsWithPorts())->map(function ($domain) { + return new CaddyServerConfiguration($this->server, $domain); + })->map->render()->implode(PHP_EOL); + } + + /** + * Get the Caddy server configuration for the vanity domains. + * + * @return string + */ + public function vanityDomainConfiguration() + { + return collect($this->server->vanityDomainsWithPorts())->map(function ($domain) { + return new CaddyServerConfiguration($this->server, $domain); + })->map->render()->implode(PHP_EOL); + } +} diff --git a/app/SecureShellCommand.php b/app/SecureShellCommand.php new file mode 100644 index 00000000..25fd4329 --- /dev/null +++ b/app/SecureShellCommand.php @@ -0,0 +1,48 @@ +environment(/*'local',*/ 'testing') + ? static::forTesting() + : static::make($password); + } + + /** + * Create a new SSH key for testing. + * + * @return object + */ + protected static function forTesting() + { + return (object) [ + 'publicKey' => file_get_contents(env('TEST_SSH_CONTAINER_PUBLIC_KEY')), + 'privateKey' => file_get_contents(env('TEST_SSH_CONTAINER_KEY')), + ]; + } + + /** + * Create a new SSH key. + * + * @param string $password + * @return object + */ + public static function make($password = '') + { + $name = str_random(20); + + (new Process( + "ssh-keygen -C \"robot@laravel.com\" -f {$name} -t rsa -b 4096 -N ".escapeshellarg($password), + storage_path('app') + ))->run(); + + [$publicKey, $privateKey] = [ + file_get_contents(storage_path('app/'.$name.'.pub')), + file_get_contents(storage_path('app/'.$name)), + ]; + + @unlink(storage_path('app/'.$name.'.pub')); + @unlink(storage_path('app/'.$name)); + + return (object) compact('publicKey', 'privateKey'); + } + + /** + * Store a secure shell key for the given user. + * + * @param \App\User $user + * @return string + */ + public static function storeFor(User $user) + { + return tap(storage_path('app/keys/'.$user->id), function ($path) use ($user) { + static::ensureKeyDirectoryExists(); + + static::ensureFileExists($path, $user->private_worker_key, 0600); + }); + } + + /** + * Ensure the SSH key directory exists. + * + * @return void + */ + protected static function ensureKeyDirectoryExists() + { + if (! is_dir(storage_path('app/keys'))) { + mkdir(storage_path('app/keys'), 0755, true); + } + } + + /** + * Ensure the given file exists. + * + * @param string $path + * @param string $contents + * @param string $chmod + * @return string + */ + protected static function ensureFileExists($path, $contents, $chmod) + { + file_put_contents($path, $contents); + + chmod($path, $chmod); + } +} diff --git a/app/Server.php b/app/Server.php new file mode 100644 index 00000000..ba327ab3 --- /dev/null +++ b/app/Server.php @@ -0,0 +1,305 @@ + 'json', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array + */ + protected $guarded = []; + + /** + * The attributes that should be hidden for arrays. + * + * @var array + */ + protected $hidden = [ + 'private_key', 'sudo_password', + ]; + + /** + * Get the provisioning script for the server. + * + * @return \App\Scripts\Script + */ + abstract public function provisioningScript(); + + /** + * Determine if this server will run a given deployment command. + * + * @param string $command + * @return bool + */ + abstract public function runsCommand($command); + + /** + * Get the stack the server belongs to. + */ + public function stack() + { + return $this->belongsTo(Stack::class, 'stack_id'); + } + + /** + * Get all of the deployments to the server. + */ + public function deployments() + { + return $this->morphMany(ServerDeployment::class, 'deployable')->latest('id'); + } + + /** + * Get the last deployment for the server. + * + * @return \App\ServerDeployment|null + */ + public function lastDeployment() + { + return $this->deployments->first(); + } + + /** + * Determine if this server is the "master" server for the stack. + * + * @return bool + */ + public function isMaster() + { + return false; + } + + /** + * Determine if this server processes queued jobs. + * + * @return bool + */ + public function isWorker() + { + return false; + } + + /** + * Determine if this server is an actual WorkerServer. + * + * @return bool + */ + public function isTrueWorker() + { + return $this instanceof WorkerServer; + } + + /** + * Determine if this server is the "master" worker for the stack. + * + * @return bool + */ + public function isMasterWorker() + { + return false; + } + + /** + * Get the recommended balancer size for the server. + * + * @return string + */ + public function recommendedBalancerSize() + { + return $this->stack->environment + ->project + ->withProvider() + ->recommendedBalancerSize($this->size); + } + + /** + * Determine if the given user can SSH into the server. + * + * @param \App\User $user + * @return bool + */ + public function canSsh(User $user) + { + return $user->canAccessProject($this->project); + } + + /** + * Enable the background services for the server. + * + * @return void + */ + public function startBackgroundServices() + { + if ($this->isWorker() && $this->lastDeployment()) { + $this->lastDeployment()->startScheduler(); + + $this->lastDeployment()->restartDaemons(); + } + } + + /** + * Disable the background services for the stack. + * + * @return void + */ + public function stopBackgroundServices() + { + if ($this->isWorker() && $this->lastDeployment()) { + $this->lastDeployment()->stopScheduler(); + + $this->lastDeployment()->stopDaemons(); + } + } + + /** + * Get all of the daemon generations for the server. + */ + public function daemonGenerations() + { + return $this->morphMany( + DaemonGeneration::class, 'generationable' + )->latest('id'); + } + + /** + * Create a fresh daemon generation. + * + * @return \App\DaemonGeneration + */ + public function createDaemonGeneration() + { + return tap($this->daemonGenerations()->create([]), function () { + $this->trimDaemonGenerations(); + }); + } + + /** + * Trim the daemon generations for the server. + * + * @return void + */ + protected function trimDaemonGenerations() + { + $generations = $this->daemonGenerations()->get(); + + if (count($generations) > 10) { + $generations->slice(10 - count($generations))->each->delete(); + } + } + + /** + * Determine if the server's daemons are pending. + * + * @return bool + */ + public function daemonsArePending() + { + return $this->daemon_status === 'pending'; + } + + /** + * Determine if the server's daemons are running. + * + * @return bool + */ + public function daemonsAreRunning() + { + return $this->daemon_status === 'running'; + } + + /** + * Mark the stack's daemons as stopped. + * + * @return $this + */ + public function markDaemonsAsRunning() + { + return tap($this)->update([ + 'daemon_status' => 'running', + ]); + } + + /** + * Mark the stack's daemons as stopped. + * + * @return $this + */ + public function markDaemonsAsStopped() + { + return tap($this)->update([ + 'daemon_status' => 'stopped', + ]); + } + + /** + * Run the provisioning script on the server. + * + * @return \App\Task|null + */ + public function runProvisioningScript() + { + if (! $this->isProvisioning()) { + $this->markAsProvisioning(); + + return $this->runInBackground($this->provisioningScript(), [ + 'then' => [ + MarkAsProvisioned::class, + ], + ]); + } + } + + /** + * Delete the model from the database. + * + * @return bool|null + * + * @throws \Exception + */ + public function delete() + { + $this->deleteOnProvider(); + + $this->address()->delete(); + $this->daemonGenerations()->delete(); + $this->tasks()->delete(); + + parent::delete(); + } + + /** + * Delete the server on the provider. + * + * @return void + */ + protected function deleteOnProvider() + { + if (! $this->providerServerId()) { + return; + } + + DeleteServerOnProvider::dispatch( + $this->project, $this->providerServerId() + ); + } +} diff --git a/app/ServerDeployment.php b/app/ServerDeployment.php new file mode 100644 index 00000000..fbfa74a7 --- /dev/null +++ b/app/ServerDeployment.php @@ -0,0 +1,431 @@ + 'json', + 'activation_commands' => 'json', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array + */ + protected $guarded = []; + + /** + * Get the project for the server deployment. + * + * @return \App\Project + */ + public function project() + { + return $this->deployment->project(); + } + + /** + * Get the stack for the server deployment. + * + * @return \App\Stack + */ + public function stack() + { + return $this->deployment->stack; + } + + /** + * Get the environment variables for the deployment's environment. + * + * @return string + */ + public function environmentVariables() + { + return trim($this->stack()->environment->variables); + } + + /** + * Get the database host for the deployment. + * + * @return string|null + */ + public function databaseHost() + { + if (count($this->stack()->databases) === 1) { + return $this->stack()->databases[0]->address->private_address; + } + + if ($this->deployable instanceof AppServer) { + return '127.0.0.1'; + } + } + + /** + * Get the database password for the deployment. + * + * @return string|null + */ + public function databasePassword() + { + if (count($this->stack()->databases) === 1) { + return $this->stack()->databases[0]->password; + } + + if ($this->deployable instanceof AppServer) { + return $this->deployable->database_password; + } + } + + /** + * Get the deployment the server deployment belongs to. + */ + public function deployment() + { + return $this->belongsTo(Deployment::class, 'deployment_id'); + } + + /** + * Determine if the deployment is for a production environment. + * + * @return bool + */ + public function isProduction() + { + return $this->deployment->isProduction(); + } + + /** + * Get the deployable server for this server deployment. + */ + public function deployable() + { + return $this->morphTo(); + } + + /** + * Get the task associated with the build. + */ + public function buildTask() + { + return $this->belongsTo(Task::class, 'build_task_id'); + } + + /** + * Get the task associated with the build. + */ + public function activationTask() + { + return $this->belongsTo(Task::class, 'activation_task_id'); + } + + /** + * Get the PHP version for the stack that owns the deployment. + * + * @return string + */ + public function phpVersion() + { + return $this->stack()->phpVersion(); + } + + /** + * Get the tarball URL for the deployment. + * + * @return string + */ + public function hash() + { + return $this->deployment->hash(); + } + + /** + * Get the tarball URL for the deployment. + * + * @return string + */ + public function tarballUrl() + { + return $this->deployment->tarballUrl(); + } + + /** + * Get the UNIX timestamp of the deployment's creation date. + * + * @return int + */ + public function timestamp() + { + return $this->deployment->timestamp(); + } + + /** + * Determine if the deployment is building. + * + * @return bool + */ + public function isBuilding() + { + return $this->status == 'building'; + } + + /** + * Build the deployment. + * + * @return void + */ + public function build() + { + Build::dispatch($this); + } + + /** + * Determine if the deployment has finished building. + * + * @return bool + */ + public function isBuilt() + { + return $this->status == 'built'; + } + + /** + * Mark the server deployment as built. + * + * @return void + */ + public function markAsBuilt() + { + $this->update(['status' => 'built']); + + ServerDeploymentBuilt::dispatch($this); + } + + /** + * Activate the deployment. + * + * @return void + */ + public function activate() + { + $this->markAsActivating(); + + Activate::dispatch($this); + } + + /** + * Mark the server deployment as activating. + * + * @return void + */ + public function markAsActivating() + { + $this->update(['status' => 'activating']); + } + + /** + * Determine if the deployment has finished activating. + * + * @return bool + */ + public function isActivated() + { + return $this->status == 'activated'; + } + + /** + * Mark the server deployment as activated. + * + * @return void + */ + public function markAsActivated() + { + $this->update(['status' => 'activated']); + + ServerDeploymentActivated::dispatch($this); + } + + /** + * Get the current daemon generation. + * + * @return \App\DaemonGeneration + */ + public function currentDaemonGeneration() + { + return $this->deployable->daemonGenerations->first(); + } + + /** + * Get the previous daemon generations. + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public function previousDaemonGenerations() + { + return $this->deployable->daemonGenerations->slice(1); + } + + /** + * Start the daemons defined for a given deployment. + * + * @return void + */ + public function startDaemons() + { + if (empty($this->daemons())) { + return; + } + + $this->deployable->markDaemonsAsRunning(); + + StartDaemons::dispatch($this); + } + + /** + * Restart the daemons defined for a given deployment. + * + * @return void + */ + public function restartDaemons() + { + if (empty($this->daemons())) { + return; + } + + $this->deployable->markDaemonsAsRunning(); + + RestartDaemons::dispatch($this); + } + + /** + * Pause the daemons defined for a given deployment. + * + * @return void + */ + public function pauseDaemons() + { + if (empty($this->daemons())) { + return; + } + + PauseDaemons::dispatch($this); + } + + /** + * Unpause the daemons defined for a given deployment. + * + * @return void + */ + public function unpauseDaemons() + { + if (empty($this->daemons())) { + return; + } + + $this->deployable->markDaemonsAsRunning(); + + UnpauseDaemons::dispatch($this); + } + + /** + * Stop the daemons defined for a given deployment. + * + * @return void + */ + public function stopDaemons() + { + if (empty($this->daemons())) { + return; + } + + $this->deployable->markDaemonsAsStopped(); + + StopDaemons::dispatch($this); + } + + /** + * Get the daemons for the deployment. + * + * @return array + */ + public function daemons() + { + return $this->deployment->daemons; + } + + /** + * Start the scheduler for the server. + * + * @return void + */ + public function startScheduler() + { + if (! empty($this->schedule())) { + StartScheduler::dispatch($this); + } + } + + /** + * Stop the scheduler for the server. + * + * @return void + */ + public function stopScheduler() + { + if (! empty($this->schedule())) { + StopScheduler::dispatch($this); + } + } + + /** + * Get the scheduled tasks for the deployment. + * + * @return array + */ + public function schedule() + { + return $this->deployment->schedule; + } + + /** + * Determine if the deployment has failed. + * + * @return bool + */ + public function hasFailed() + { + return $this->status == 'failed'; + } + + /** + * Mark the server deployment as failed. + * + * @return void + */ + public function markAsFailed() + { + $this->update(['status' => 'failed']); + + ServerDeploymentFailed::dispatch($this); + } +} diff --git a/app/ServerProvider.php b/app/ServerProvider.php new file mode 100644 index 00000000..c8c2ef1f --- /dev/null +++ b/app/ServerProvider.php @@ -0,0 +1,94 @@ + 'json', + ]; + + /** + * The attributes that should be hidden for arrays. + * + * @var array + */ + protected $hidden = [ + 'meta', + ]; + + /** + * The user that owns the provider. + */ + public function user() + { + return $this->belongsTo(User::class, 'user_id'); + } + + /** + * Determine if the given region is valid for this provider. + * + * @param string $region + * @return bool + */ + public function validRegion($region) + { + return array_key_exists($region, $this->regions()); + } + + /** + * Determine if the given size is valid for this provider. + * + * @param string $size + * @return bool + */ + public function validSize($size) + { + return array_key_exists($size, $this->sizes()); + } + + /** + * Get all of the valid regions for the provider. + * + * @return array + */ + public function regions() + { + return $this->client()->regions(); + } + + /** + * Get all of the valid server sizes for the provider. + * + * @return array + */ + public function sizes() + { + return $this->client()->sizes(); + } + + /** + * Get a provider client for the provider. + * + * @return \App\Contracts\ServerProviderClient + */ + public function client() + { + return ServerProviderClientFactory::make($this); + } +} diff --git a/app/ServerProviderClientFactory.php b/app/ServerProviderClientFactory.php new file mode 100644 index 00000000..04f825f5 --- /dev/null +++ b/app/ServerProviderClientFactory.php @@ -0,0 +1,25 @@ +type) { + case 'DigitalOcean': + return new DigitalOcean($provider); + default: + throw new InvalidArgumentException("Invalid provider type."); + } + } +} diff --git a/app/ServerRecordCreator.php b/app/ServerRecordCreator.php new file mode 100644 index 00000000..dcf2d30f --- /dev/null +++ b/app/ServerRecordCreator.php @@ -0,0 +1,87 @@ +stack = $stack; + $this->definition = $definition; + } + + /** + * Create the server records for the stack. + * + * @return void + */ + public function create() + { + $definition = $this->definition->toArray(); + + if (! isset($definition[$this->type]) || empty($definition[$this->type])) { + return; + } + + foreach (range(1, $definition[$this->type]['scale'] ?? 1) as $index) { + $this->relation()->create( + $this->baseAttributes($index, $definition[$this->type]) + + $this->attributes($definition) + ); + } + } + + /** + * Get the base server attributes for the given definition. + * + * @param int $index + * @param array $definition + * @return array + */ + protected function baseAttributes($index, array $definition) + { + return [ + 'project_id' => $this->stack->environment->project->id, + 'name' => "{$this->stack->name}-{$this->type}-{$index}", + 'size' => $definition['size'], + 'sudo_password' => str_random(40), + 'meta' => array_filter([ + 'serves' => $definition['serves'] ?? null, + 'tls' => $definition['tls'] ?? null, + ]), + ]; + } + + /** + * Get the custom attributes for the servers. + * + * @return array + */ + protected function attributes() + { + return []; + } +} diff --git a/app/ServerTask.php b/app/ServerTask.php new file mode 100644 index 00000000..a3468e45 --- /dev/null +++ b/app/ServerTask.php @@ -0,0 +1,137 @@ + 'json', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array + */ + protected $guarded = []; + + /** + * Get the stack task that the server task belongs to. + */ + public function stackTask() + { + return $this->belongsTo(StackTask::class, 'stack_task_id'); + } + + /** + * Get the entity the task belongs to. + */ + public function taskable() + { + return $this->morphTo(); + } + + /** + * Get the underlying task for the server task. + */ + public function task() + { + return $this->belongsTo(Task::class, 'task_id'); + } + + /** + * Determine if the server task is pending. + * + * @return bool + */ + public function isPending() + { + return $this->status === 'pending'; + } + + /** + * Determine if the server task is running. + * + * @return bool + */ + public function isRunning() + { + return $this->status === 'running'; + } + + /** + * Run the server task. + * + * @return void + */ + public function run() + { + $task = $this->taskable->runInBackground(new RunServerTask($this), [ + 'then' => [new CheckServerTask($this->id)], + ]); + + $this->update([ + 'status' => 'running', + 'task_id' => $task->id, + ]); + } + + /** + * Determine if the server task has finished. + * + * @return bool + */ + public function isFinished() + { + return $this->status === 'finished'; + } + + /** + * Mark the server task as finished. + * + * @return void + */ + public function markAsFinished() + { + $this->update(['status' => 'finished']); + + ServerTaskFinished::dispatch($this); + + $this->stackTask->syncStatus(); + } + + /** + * Determine if the server task has failed. + * + * @return bool + */ + public function hasFailed() + { + return $this->status === 'failed'; + } + + /** + * Mark the server task as failed. + * + * @return void + */ + public function markAsFailed() + { + $this->update(['status' => 'failed']); + + ServerTaskFailed::dispatch($this); + + $this->stackTask->syncStatus(); + } +} diff --git a/app/Services/DigitalOcean.php b/app/Services/DigitalOcean.php new file mode 100644 index 00000000..7f999cc6 --- /dev/null +++ b/app/Services/DigitalOcean.php @@ -0,0 +1,350 @@ +provider = $provider; + } + + /** + * Determine if the provider credentials are valid. + * + * @return bool + */ + public function valid() + { + try { + $this->request('get', '/regions'); + + return true; + } catch (Exception $e) { + return false; + } + } + + /** + * Get all of the valid regions for the provider. + * + * @return array + */ + public function regions() + { + return [ + 'ams2' => 'Amsterdam 2', + 'ams3' => 'Amsterdam 3', + 'blr1' => 'Bangalore', + 'lon1' => 'London', + 'fra1' => 'Frankfurt', + 'nyc1' => 'New York 1', + 'nyc2' => 'New York 2', + 'nyc3' => 'New York 3', + 'sfo1' => 'San Francisco 1', + 'sfo2' => 'San Francisco 2', + 'sgp1' => 'Singapore', + 'tor1' => 'Toronto', + ]; + } + + /** + * Get all of the valid server sizes for the provider. + * + * @return array + */ + public function sizes() + { + return [ + '512MB' => ['cpu' => '1 Core', 'ram' => '512MB', 'hdd' => '20GB', 'price' => 5], + '1GB' => ['cpu' => '1 Core', 'ram' => '1GB', 'hdd' => '30GB', 'price' => 10], + '2GB' => ['cpu' => '2 Core', 'ram' => '2GB', 'hdd' => '40GB', 'price' => 20], + '4GB' => ['cpu' => '2 Core', 'ram' => '4GB', 'hdd' => '60GB', 'price' => 40], + '8GB' => ['cpu' => '4 Core', 'ram' => '8GB', 'hdd' => '80GB', 'price' => 80], + '16GB' => ['cpu' => '8 Core', 'ram' => '16GB', 'hdd' => '160GB', 'price' => 160], + 'm16GB' => ['cpu' => '2 Core', 'ram' => '16GB (High Memory)', 'hdd' => '30GB', 'price' => 120], + '32GB' => ['cpu' => '12 Core', 'ram' => '32GB', 'hdd' => '320GB', 'price' => 320], + 'm32GB' => ['cpu' => '4 Core', 'ram' => '32GB (High Memory)', 'hdd' => '90GB', 'price' => 240], + '64GB' => ['cpu' => '20 Core', 'ram' => '64GB', 'hdd' => '640GB', 'price' => 640], + 'm64GB' => ['cpu' => '8 Core', 'ram' => '64GB (High Memory)', 'hdd' => '200GB', 'price' => 480], + ]; + } + + /** + * Get the size of the server in megabytes. + * + * @param string $size + * @return int + */ + public function sizeInMegabytes($size) + { + switch ($size) { + case '512MB': + return 512; + case '1GB': + return 1024; + case '2GB': + return 2048; + case '4GB': + return 4096; + case '8GB': + return 8192; + case '16GB': + return 16384; + case 'm16GB': + return 16384 + 1; + case '32GB': + return 32768; + case 'm32GB': + return 32768 + 1; + case '64GB': + return 65536; + case 'm64GB': + return 65536 + 1; + } + + throw new InvalidArgumentException("Invalid size."); + } + + /** + * Get the recommended balancer size for a given server size. + * + * @param string $size + * @return string + */ + public function recommendedBalancerSize($size) + { + switch ($size) { + case '512MB': + case '1GB': + return '512MB'; + case '2GB': + return '1GB'; + case '4GB': + case '8GB': + return '2GB'; + case '16GB': + case 'm16GB': + case '32GB': + case 'm32GB': + return '4GB'; + case '64GB': + case 'm64GB': + return '8GB'; + } + + throw new InvalidArgumentException("Invalid size."); + } + + /** + * Create a new server. + * + * @param string $name + * @param string $size + * @param string $region + * @return string + */ + public function createServer($name, $size, $region) + { + return $this->request('post', '/droplets', [ + 'name' => $name, + 'size' => $size, + 'region' => $region, + 'image' => 'ubuntu-17-04-x64', + 'ipv6' => true, + 'private_networking' => true, + 'ssh_keys' => [$this->keyId()], + 'monitoring' => true, + ])['droplet']['id']; + } + + /** + * Get the SSH key ID for our SSH key. + * + * @return int + */ + public function keyId() + { + return tap($this->findKey()['id'] ?? $this->addKey(), function ($id) { + $this->provider->user->update([ + 'provider_key_id' => $id, + ]); + }); + } + + /** + * Attempt to find our SSH key on the DigitalOcean account. + * + * @return array|null + */ + public function findKey() + { + if ($id = $this->provider->user->provider_key_id) { + return $this->request('get', '/account/keys/'.$id)['ssh_key']; + } + + return collect($this->aggregate('get', '/account/keys', 'ssh_keys'))->first(function ($key) { + return $key['public_key'] == trim($this->provider->user->public_worker_key); + }); + } + + /** + * Add our SSH key to the DigitalOcean account. + * + * @return int + */ + public function addKey() + { + return $this->request('post', '/account/keys', [ + 'name' => 'Laravel Cloud', + 'public_key' => $this->provider->user->public_worker_key, + ])['ssh_key']['id']; + } + + /** + * Remove our SSH key from the account. + * + * @return void + */ + public function removeKey() + { + if ($id = $this->keyId()) { + $this->request('delete', '/account/keys/'.$id); + + $this->provider->user->update([ + 'provider_key_id' => null, + ]); + } + } + + /** + * Get the public IP address for a server by ID. + * + * @param \App\Contracts\Provisionable $server + * @return string|null + */ + public function getPublicIpAddress(Provisionable $server) + { + return $this->getIpAddress($server); + } + + /** + * Get the private IP address for a server by ID. + * + * @param \App\Contracts\Provisionable $server + * @return string|null + */ + public function getPrivateIpAddress(Provisionable $server) + { + return $this->getIpAddress($server, 'private'); + } + + /** + * Delete the given server. + * + * @param \App\Contracts\Provisionable $server + * @return void + */ + public function deleteServer(Provisionable $server) + { + $this->deleteServerById($server->providerServerId()); + } + + /** + * Delete the given server. + * + * @param string $providerServerId + * @return void + */ + public function deleteServerById($providerServerId) + { + $this->request('delete', "/droplets/{$providerServerId}"); + } + + /** + * Get an IP address for the server. + * + * @param \App\Contracts\Provisionable $server + * @param string $type + * @return string|null + */ + protected function getIpAddress(Provisionable $server, $type = 'public') + { + $networks = $this->request( + 'get', "/droplets/{$server->providerServerId()}" + )['droplet']['networks']['v4'] ?? []; + + return collect($networks)->filter(function ($network) use ($type) { + return ($network['type'] ?? null) == $type; + })->first()['ip_address'] ?? null; + } + + /** + * Make an HTTP request to DigitalOcean. + * + * @param string $method + * @param string $path + * @param array $parameters + * @return array + */ + protected function request($method, $path, array $parameters = []) + { + $response = (new Client)->{$method}('https://api.digitalocean.com/v2/'.ltrim($path, '/'), [ + 'headers' => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer '.$this->provider->meta['token'] + ], + 'json' => $parameters, + ]); + + return json_decode((string) $response->getBody(), true); + } + + /** + * Aggregate pages of results into a single result array. + * + * @param string $method + * @param string $path + * @param array $target + * @param array $parameters + * @return array + */ + protected function aggregate($method, $path, $target, array $parameters = []) + { + $page = 1; + + $results = []; + + do { + $response = $this->request( + $method, $path.'?page='.$page.'&per_page=100', $parameters + ); + + $results = array_merge($results, $response[$target]); + + $page++; + } while (isset($response['links']['pages']['next'])); + + return $results; + } +} diff --git a/app/Services/GitHub.php b/app/Services/GitHub.php new file mode 100644 index 00000000..fb15ac77 --- /dev/null +++ b/app/Services/GitHub.php @@ -0,0 +1,313 @@ +source = $source; + } + + /** + * Determine if the source control credentials are valid. + * + * @return bool + */ + public function valid() + { + try { + $response = $this->request('get', '/user/repos'); + + return true; + } catch (Exception $e) { + return false; + } + } + + /** + * Validate the given repository and branch are valid. + * + * @param string $repository + * @param string $branch + * @return bool + */ + public function validRepository($repository, $branch) + { + if (empty($repository)) { + return false; + } + + try { + $response = $this->request('get', "/repos/{$repository}/branches"); + } catch (ClientException $e) { + return false; + } + + if (empty($branch)) { + return true; + } + + return collect($response)->contains(function ($b) use ($branch) { + return $b['name'] === $branch; + }); + } + + /** + * Validate the given repository and commit hash are valid. + * + * @param string $repository + * @param string $hash + * @return bool + */ + public function validCommit($repository, $hash) + { + if (empty($repository) || empty($hash)) { + return false; + } + + try { + $response = $this->request('get', "/repos/{$repository}/commits/{$hash}"); + } catch (ClientException $e) { + return false; + } + + return $response['sha'] === $hash; + } + + /** + * Get the latest commit hash for the given repository and branch. + * + * @param string $repository + * @param string $branch + * @return string + */ + public function latestHashFor($repository, $branch) + { + return $this->request( + 'get', "/repos/{$repository}/commits?sha={$branch}&per_page=1" + )[0]['sha']; + } + + /** + * Get the tarball URL for the given deployment. + * + * @param \App\Deployment $deployment + * @return string + */ + public function tarballUrl(Deployment $deployment) + { + return sprintf( + 'https://api.github.com/repos/%s/tarball/%s?access_token=%s', + $deployment->repository(), + $deployment->commit_hash, + $this->token() + ); + } + + /** + * Publish the given hook. + * + * @param \App\Hook $hook + * @return void + */ + public function publishHook(Hook $hook) + { + $this->deleteHooksWithMatchingUrl($hook); + + $response = $this->request('post', '/repos/'.$hook->project()->repository.'/hooks', [ + 'name' => 'web', + 'config' => [ + 'url' => $hook->url(), + 'content_type' => 'json' + ], + 'events' => ['push'], + 'active' => true, + ]); + + $hook->update([ + 'published' => true, + 'meta' => array_merge($hook->meta, [ + 'provider_hook_id' => $response['id'], + ]) + ]); + } + + /** + * Determine if the given hook payload is a test. + * + * @param \App\Hook $hook + * @param array $payload + * @return bool + */ + public function isTestHookPayload(Hook $hook, array $payload) + { + return isset($payload['zen']); + } + + /** + * Determine if the given hook payload applies to the hook. + * + * @param \App\Hook $hook + * @param array $payload + * @return bool + */ + public function receivesHookPayload(Hook $hook, array $payload) + { + return ! $this->isTestHookPayload($hook, $payload) && + $payload['ref'] == "refs/heads/{$hook->branch}" && + $payload['repository']['full_name'] == $hook->project()->repository; + } + + /** + * Get the commit hash from the given hook payload. + * + * @param array $payload + * @return string|null + */ + public function extractCommitFromHookPayload(array $payload) + { + return $payload['head_commit']['id'] ?? null; + } + + /** + * Unpublish the given hook. + * + * @param \App\Hook $hook + * @return void + */ + public function unpublishHook(Hook $hook) + { + if (! $providerHookId = ($hook->meta['provider_hook_id'] ?? null)) { + return; + } + + $this->deleteHookById($hook->project()->repository, $providerHookId); + + $hook->update([ + 'published' => false, + 'meta' => array_filter(array_merge($hook->meta, [ + 'provider_hook_id' => null, + ])) + ]); + } + + /** + * Delete any hooks matching the given hooks URL. + * + * @param \App\Hook $hook + * @return void + */ + protected function deleteHooksWithMatchingUrl(Hook $hook) + { + if ($existingHook = $this->findHookWithMatchingUrl($hook)) { + $this->deleteHookById($hook->project()->repository, $existingHook['id']); + } + } + + /** + * Find a hook by the given hook's URL. + * + * @param \App\Hook $hook + * @return array|null + */ + protected function findHookWithMatchingUrl(Hook $hook) + { + $url = $hook->url(); + + return collect($this->request('get', '/repos/'.$hook->project()->repository.'/hooks')) + ->first(function ($hook) use ($url) { + return ($hook['config']['url'] ?? null) == $url; + }); + } + + /** + * Delete a hook by the given repository and ID. + * + * @param string $repository + * @param string $id + * @return void + */ + protected function deleteHookById($repository, $id) + { + $this->request('delete', '/repos/'.$repository.'/hooks/'.$id); + } + + /** + * Get the manifest content for the given stack and hash. + * + * @param \App\Stack $stack + * @param string $repository + * @param string $hash + * @return string + */ + public function manifest(Stack $stack, $repository, $hash) + { + try { + $response = $this->request( + 'get', '/repos/'.$repository.'/contents/.cloud/'.$stack->environment->name.'/'.$stack->name.'.yml?ref='.$hash + ); + + return base64_decode($response['content']); + } catch (Exception $e) { + report($e); + + throw new ManifestNotFoundException($stack, $repository, $hash); + } + } + + /** + * Make an HTTP request to GitHub. + * + * @param string $method + * @param string $path + * @param array $parameters + * @return array + */ + protected function request($method, $path, array $parameters = []) + { + $response = (new Client)->{$method}('https://api.github.com/'.ltrim($path, '/'), [ + 'headers' => [ + 'Accept' => 'application/vnd.github.v3+json', + 'Authorization' => 'token '.$this->token(), + ], + 'json' => $parameters, + ]); + + return json_decode((string) $response->getBody(), true); + } + + /** + * Get the authentication token for the provider. + * + * @return string + */ + protected function token() + { + return $this->source->meta['token']; + } +} diff --git a/app/Services/LocalYamlParser.php b/app/Services/LocalYamlParser.php new file mode 100644 index 00000000..67731ec0 --- /dev/null +++ b/app/Services/LocalYamlParser.php @@ -0,0 +1,20 @@ +client = $client; + } + + /** + * Add a DNS record for the given stack. + * + * @param \App\Stack $stack + * @return string + */ + public function addRecord(Stack $stack) + { + $this->deleteRecord($stack); + + return tap($this->updateRecord('CREATE', $stack)['ChangeInfo']['Id'], function ($id) use ($stack) { + $stack->update([ + 'dns_record_id' => $id, + 'dns_address' => $stack->entrypoint(), + ]); + }); + } + + /** + * Determine if the stack's DNS record has propagated. + * + * @param \App\Stack $stack + * @return bool + */ + public function propagated(Stack $stack) + { + return $stack->dns_record_id && $this->client->getChange([ + 'Id' => $stack->dns_record_id, + ])['ChangeInfo']['Status'] == 'INSYNC'; + } + + /** + * Delete a DNS record for the given stack. + * + * @param \App\Stack $stack + * @return void + */ + public function deleteRecord(Stack $stack) + { + if (! $stack->dns_address) { + return; + } + + try { + $this->updateRecord('DELETE', $stack); + } catch (Exception $e) { + report($e); + } + + $stack->update([ + 'dns_record_id' => null, + 'dns_address' => null, + ]); + } + + /** + * Delete a DNS record for the given name and address. + * + * @param string $name + * @param string $ipAddress + * @return void + */ + public function deleteRecordByName($name, $ipAddress) + { + return $this->updateRecordByName('DELETE', $name, $ipAddress); + } + + /** + * Perform an action on the Route 53 record. + * + * @param string $action + * @param \App\Stack $stack + * @return mixed + */ + protected function updateRecord($action, Stack $stack) + { + return $this->updateRecordByName( + $action, $stack->url, + $action == 'CREATE' ? $stack->entrypoint() : $stack->dns_address + ); + } + + /** + * Perform an action on the Route 53 record. + * + * @param string $action + * @param string $name + * @param string $ipAddress + * @return mixed + */ + protected function updateRecordByName($action, $name, $ipAddress) + { + return $this->client->changeResourceRecordSets([ + 'HostedZoneId' => 'ZDV4H7FXFYGN0', + 'ChangeBatch' => [ + 'Changes' => [ + [ + 'Action' => $action, + 'ResourceRecordSet' => [ + 'Name' => $name.'.laravel.build', + 'Type' => 'A', + 'ResourceRecords' => [ + ['Value' => $ipAddress], + ], + 'TTL' => 60, + ], + ], + ], + ], + ]); + } +} diff --git a/app/Services/S3.php b/app/Services/S3.php new file mode 100644 index 00000000..09e26bfa --- /dev/null +++ b/app/Services/S3.php @@ -0,0 +1,228 @@ +provider = $provider; + } + + /** + * Determine if the provider credentials are valid. + * + * @return bool + */ + public function valid() + { + try { + if (! isset($this->provider->meta['bucket'])) { + return false; + } + + return $this->hasBucket($this->provider->meta['bucket']); + } catch (Exception $e) { + report($e); + + return false; + } + } + + /** + * Determine if the given bucket exists. + * + * @param string $name + * @return bool + */ + public function hasBucket($name) + { + try { + $this->client()->getBucketLocation([ + 'Bucket' => $name, + ]); + + return true; + } catch (Exception $e) { + return false; + } + } + + /** + * Create a bucket with the given name. + * + * @param string $name + * @return void + */ + public function createBucket($name) + { + $this->client()->createBucket([ + 'Bucket' => $name, + ]); + } + + /** + * Delete the given bucket. + * + * @param string $name + * @return void + */ + public function deleteBucket($name) + { + $this->client()->deleteBucket([ + 'Bucket' => $name, + ]); + } + + /** + * Determine if the given object exists. + * + * @param string $path + * @return bool + */ + public function has($path) + { + try { + $response = $this->client()->headObject([ + 'Bucket' => $this->provider->bucket(), + 'Key' => $path, + ]); + + return true; + } catch (Exception $e) { + return false; + } + } + + /** + * Store an object at the given path. + * + * @param string $path + * @param string $data + * @return void + */ + public function put($path, $data) + { + $this->client()->putObject([ + 'Bucket' => $this->provider->bucket(), + 'Key' => $path, + 'Body' => $data, + ]); + } + + /** + * Get the size of the object in megabytes. + * + * @param string $path + * @return int + */ + public function size($path) + { + try { + $response = $this->client()->headObject([ + 'Bucket' => $this->provider->bucket(), + 'Key' => $path, + ]); + + return (int) round($response['ContentLength'] / 1024 / 1024); + } catch (Exception $e) { + report($e); + + return; + } + } + + /** + * Delete the object at the given path. + * + * @param string $path + * @return void + */ + public function delete($path) + { + $this->client()->deleteObject([ + 'Bucket' => $this->provider->bucket(), + 'Key' => $path, + ]); + } + + /** + * Get the configuration script for the storage provider. + * + * @return string + */ + public function configurationScript() + { + return view('scripts.storage-provider-configuration.s3', [ + 'provider' => $this->provider, + ])->render(); + } + + /** + * Get the upload script for the storage provider. + * + * @param \App\DatabaseBackup $backup + * @return string + */ + public function uploadScript(DatabaseBackup $backup) + { + return sprintf( + "aws s3 cp /home/cloud/backups/%s s3://%s/%s", + basename($backup->backup_path), + $backup->storageProvider->bucket(), + $backup->backup_path + ); + } + + /** + * Get the download script for the storage provider. + * + * @param \App\DatabaseBackup $backup + * @return string + */ + public function downloadScript(DatabaseBackup $backup) + { + return sprintf( + "aws s3 cp s3://%s/%s /home/cloud/restores/%s", + $backup->storageProvider->bucket(), + $backup->backup_path, + basename($backup->backup_path) + ); + } + + /** + * Get an S3 client instance for the storage provider. + * + * @return \AWS\S3\S3Client + */ + protected function client() + { + return new S3Client([ + 'version' => 'latest', + 'credentials' => [ + 'key' => $this->provider->meta['key'], + 'secret' => $this->provider->meta['secret'], + ], + 'region' => $this->provider->meta['region'], + ]); + } +} diff --git a/app/ShellCommand.php b/app/ShellCommand.php new file mode 100644 index 00000000..e0812f7c --- /dev/null +++ b/app/ShellCommand.php @@ -0,0 +1,64 @@ +command = $command; + } + + /** + * Determine if the given server can run the build command. + * + * @param \App\Server $server + * @return bool + */ + public function appliesTo($server) + { + return $server->runsCommand($this->command); + } + + /** + * Determine if the command is prefixed with the given false. + * + * @param string $prefix + * @return bool + */ + public function prefixed($prefix) + { + return $prefix ? Str::startsWith($this->command, $prefix) : false; + } + + /** + * Convert the command to a formatted string. + * + * @return string + */ + public function trim() + { + $command = $this->command; + + $command = Str::startsWith($command, 'once:') ? Str::replaceFirst('once:', '', $command) : $command; + $command = Str::startsWith($command, 'web:') ? Str::replaceFirst('web:', '', $command) : $command; + $command = Str::startsWith($command, 'worker:') ? Str::replaceFirst('worker:', '', $command) : $command; + + return trim($command); + } +} diff --git a/app/ShellOutput.php b/app/ShellOutput.php new file mode 100644 index 00000000..3eacb26e --- /dev/null +++ b/app/ShellOutput.php @@ -0,0 +1,41 @@ +output .= $line; + } + + /** + * Render the output as a string. + * + * @return string + */ + public function __toString() + { + if (Str::startsWith($this->output, 'Warning:')) { + $this->output = substr($this->output, strpos($this->output, "\n") + 1); + } + + return trim($this->output); + } +} diff --git a/app/ShellProcessRunner.php b/app/ShellProcessRunner.php new file mode 100644 index 00000000..a5119c73 --- /dev/null +++ b/app/ShellProcessRunner.php @@ -0,0 +1,46 @@ +run($output = new ShellOutput); + } catch (ProcessTimedOutException $e) { + $timedOut = true; + } + + return new ShellResponse( + $process->getExitCode(), (string) ($output ?? ''), $timedOut ?? false + ); + } + + /** + * Mock the responses for the process runner. + * + * @param array $responses + * @return void + */ + public static function mock(array $responses) + { + Facade::shouldReceive('run')->andReturn(...collect($responses)->flatMap(function ($response) { + return [ + (object) ['exitCode' => 0], // Ensure Directory Exists... + (object) ['exitCode' => 0], // Upload... + (object) $response, + ]; + })->all()); + } +} diff --git a/app/ShellResponse.php b/app/ShellResponse.php new file mode 100644 index 00000000..6bd5fd57 --- /dev/null +++ b/app/ShellResponse.php @@ -0,0 +1,42 @@ +output = $output; + $this->exitCode = $exitCode; + $this->timedOut = $timedOut; + } +} diff --git a/app/SourceProvider.php b/app/SourceProvider.php new file mode 100644 index 00000000..4f7f73fc --- /dev/null +++ b/app/SourceProvider.php @@ -0,0 +1,60 @@ + 'json', + ]; + + /** + * The attributes that should be hidden for arrays. + * + * @var array + */ + protected $hidden = [ + 'meta', + ]; + + /** + * The user that owns the source. + */ + public function user() + { + return $this->belongsTo(User::class, 'user_id'); + } + + /** + * Get the projects the source provider is attached to. + */ + public function projects() + { + return $this->hasMany(Project::class); + } + + /** + * Get a source control provider client for the provider. + * + * @return \App\Contracts\SourceProviderClient + */ + public function client() + { + return SourceProviderClientFactory::make($this); + } +} diff --git a/app/SourceProviderClientFactory.php b/app/SourceProviderClientFactory.php new file mode 100644 index 00000000..c0ffa4ca --- /dev/null +++ b/app/SourceProviderClientFactory.php @@ -0,0 +1,25 @@ +type) { + case 'GitHub': + return new GitHub($source); + default: + throw new InvalidArgumentException("Invalid source control provider type."); + } + } +} diff --git a/app/Stack.php b/app/Stack.php new file mode 100644 index 00000000..a2b21116 --- /dev/null +++ b/app/Stack.php @@ -0,0 +1,916 @@ + 'boolean', + 'meta' => 'json', + 'pending_deployment' => 'json', + 'promoted' => 'boolean', + 'under_maintenance' => 'boolean', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array + */ + protected $guarded = []; + + /** + * Get the project for the stack. + * + * @return \App\Project + */ + public function project() + { + return $this->environment->project; + } + + /** + * Get the environment that the project belongs to. + */ + public function environment() + { + return $this->belongsTo(Environment::class, 'environment_id'); + } + + /** + * Get the creator of the stack. + */ + public function creator() + { + return $this->belongsTo(User::class, 'creator_id'); + } + + /** + * Get the region the stack is located in. + * + * @return string + */ + public function region() + { + return $this->environment->project->region; + } + + /** + * Get all of the hooks attached to the stack. + */ + public function hooks() + { + return $this->hasMany(Hook::class); + } + + /** + * Get the databases attached to the stack. + */ + public function databases() + { + return $this->belongsToMany( + Database::class, 'stack_databases', 'stack_id', 'database_id' + ); + } + + /** + * Get all of the load balancers available to the stack. + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public function balancers() + { + return $this->environment->project->balancers; + } + + /** + * Get the largest available balancer. + * + * @return \App\Balancer|null + */ + public function largestAvailableBalancer() + { + return $this->environment->project->largestAvailableBalancer(); + } + + /** + * Get the recommended balancer size for the stack. + * + * @return string + */ + public function recommendedBalancerSize() + { + return count($this->webServers) > 0 + ? $this->webServers->first()->recommendedBalancerSize() + : $this->appServer->recommendedBalancerSize(); + } + + /** + * Get the custom certificate associated with the stack. + */ + public function certificate() + { + return $this->belongsTo(Certificate::class, 'certificate_id'); + } + + /** + * Determine if this stack is promotable. + * + * @return bool + */ + public function promotable() + { + return count($this->serves()) > 0; + } + + /** + * Get the domains served by the stack. + * + * @return array + */ + public function serves() + { + return $this->appServer + ? ($this->appServer->meta['serves'] ?? []) + : ($this->webServers->first()->meta['serves'] ?? []); + } + + /** + * Get all of the servers in the stack. + * + * @return \Illuminate\Support\Collection + */ + public function allServers() + { + return collect(array_merge( + $this->appServers->sortBy('id')->all(), + $this->webServers->sortBy('id')->all(), + $this->workerServers->sortBy('id')->all() + )); + } + + /** + * Get all of the application / web servers in the stack. + * + * @return \Illuminate\Support\Collection + */ + public function httpServers() + { + return collect(array_merge( + $this->appServers->sortBy('id')->all(), + $this->webServers->sortBy('id')->all() + )); + } + + /** + * Refresh the stack's server configurations. + * + * @return void + */ + public function syncServers() + { + SyncServers::dispatch($this); + } + + /** + * Determine if the stack is completely provisioned. + * + * @return bool + */ + public function serversAreProvisioned() + { + return $this->allServers()->where( + 'status', 'provisioned' + )->count() === count($this->allServers()); + } + + /** + * Get the master web server for the stack. + * + * @return \App\WebServer|\App\AppServer|null + */ + public function masterServer() + { + return $this->appServer ?: $this->webServers->sortBy->id->first(); + } + + /** + * Get the master worker server for the stack. + * + * @return \App\AppServer|\App\WorkerServer|null + */ + public function masterWorker() + { + if ($this->appServer && count($this->workerServers) === 0) { + return $this->appServer; + } + + return count($this->workerServers) > 0 + ? $this->workerServers->sortBy->id->first() : null; + } + + /** + * Get the app server that belong to the stack. + */ + public function appServer() + { + return $this->hasOne(AppServer::class); + } + + /** + * Get the app server that belong to the stack in an array. + */ + public function appServers() + { + return $this->hasMany(AppServer::class); + } + + /** + * Get all of the web servers that belong to the stack. + */ + public function webServers() + { + return $this->hasMany(WebServer::class); + } + + /** + * Get all of the worker servers that belong to the stack. + */ + public function workerServers() + { + return $this->hasMany(WorkerServer::class); + } + + /** + * Get all of the stack tasks associated with the stack. + */ + public function tasks() + { + return $this->hasMany(StackTask::class)->latest('id'); + } + + /** + * Run a new task on the stack. + * + * @param string $name + * @param string $user + * @param array $commands + * @return \App\StackTask + */ + public function dispatchTask($name, $user, array $commands) + { + return tap($this->tasks()->create([ + 'name' => $name, + 'user' => $user, + 'commands' => $commands, + ]), function ($task) { + $task->dispatch(); + + $this->trimTasks(); + }); + } + + /** + * Trim the stack's tasks. + * + * @return void + */ + protected function trimTasks() + { + $tasks = $this->tasks()->get(); + + if (count($tasks) > 20) { + $tasks->slice(20 - count($tasks))->each->delete(); + } + } + + /** + * Get the entrypoint IP address for the stack. + * + * @return string + */ + public function entrypoint() + { + if ($this->balanced) { + return $this->largestAvailableBalancer()->address->public_address; + } + + if ($this->masterServer() && $this->masterServer()->address) { + return $this->masterServer()->address->public_address; + } + } + + /** + * Get all of the IP addresses for the stack. + * + * @return array + */ + public function allIpAddresses() + { + return $this->allServers()->flatMap(function ($server) { + return array_filter([ + $server->address->public_address, + $server->address->private_address, + ]); + }); + } + + /** + * Get all of the private web addresses the stack exposes. + * + * @return array + */ + public function privateWebAddresses() + { + return collect(array_merge( + $this->appServers()->with('address')->get()->all(), + $this->webServers()->with('address')->get()->all() + ))->map(function ($server) { + return 'https://'.$server->address->private_address; + })->all(); + } + + /** + * Get the array of domains the server should respond to. + * + * @return array + */ + public function shouldRespondTo() + { + if (! $this->promoted) { + return [$this->url.'.laravel.build']; + } + + return array_unique(array_merge( + [$this->url.'.laravel.build'], + $this->masterServer()->shouldRespondTo() + )); + } + + /** + * Get the array of domains / ports the server should respond to. + * + * @return array + */ + public function shouldRespondToWithPorts() + { + return collect($this->shouldRespondTo())->flatMap(function ($domain) { + return [$domain.':80', $domain.':443']; + })->all(); + } + + /** + * Get all of the stack's vanity domain's with ports. + * + * @return array + */ + public function actualDomainsWithPorts() + { + return collect($this->shouldRespondToWithPorts())->reject(function ($domain) { + return Str::contains($domain, 'laravel.build'); + })->values()->all(); + } + + /** + * Get all of the stack's vanity domain's with ports. + * + * @return array + */ + public function vanityDomainsWithPorts() + { + return collect($this->shouldRespondToWithPorts())->filter(function ($domain) { + return Str::contains($domain, 'laravel.build'); + })->unique()->values()->all(); + } + + /** + * Determine if the given domain is the canonical domain. + * + * @param string $domain + * @return bool + */ + public function isCanonicalDomain($domain) + { + $domain = str_replace(['http://', 'https://', ':80', ':443'], '', $domain); + + return $this->canonicalDomain($domain) === $domain; + } + + /** + * Determine the canonical domain for the given domain. + * + * @param string $domain + * @return string + */ + public function canonicalDomain($domain) + { + $domain = str_replace(['http://', 'https://', ':80', ':443'], '', $domain); + + if (in_array($domain, $canonicals = $this->masterServer()->meta['serves'] ?? [])) { + return $domain; + } + + if (Str::startsWith($domain, 'www.') && + in_array(Str::replaceFirst('www.', '', $domain), $canonicals)) { + return Str::replaceFirst('www.', '', $domain); + } + + if (! Str::startsWith($domain, 'www.') && in_array('www.'.$domain, $canonicals)) { + return 'www.'.$domain; + } + + return $domain; + } + + /** + * Get the reverse of the given domain's canonical domain. + * + * @param string $domain + * @return string + */ + public function nonCanonicalDomain($domain) + { + $canonical = $this->canonicalDomain($domain); + + if (Str::startsWith($canonical, 'www.')) { + return Str::replaceFirst('www.', '', $canonical); + } + + if (substr_count($canonical, '.') >= 2) { + return $canonical; + } + + return 'www.'.$canonical; + } + + /** + * Get all of the stack's deployments. + */ + public function deployments() + { + return $this->hasMany(Deployment::class)->latest('id'); + } + + /** + * Get the last deployment for the stack. + * + * @return \App\Deployment|null + */ + public function lastDeployment() + { + return $this->deployments->first(); + } + + /** + * Determine if the stack is deploying. + * + * @return bool + */ + public function isDeploying() + { + return $this->deployment_status == 'deploying'; + } + + /** + * Deploy fresh code using the deployment instructions. + * + * @param \App\DeploymentInstructions $instructions + * @return \App\Deployment + */ + public function deployUsing(DeploymentInstructions $instructions) + { + return $this->deployHash( + $instructions->hash, + $instructions->build, $instructions->activate, + $instructions->directories, $instructions->daemons, + $instructions->schedule + ); + } + + /** + * Deploy fresh code to the stack. + * + * @param string $hash + * @param array $build + * @param array $activate + * @param string $hash + * @param array $directories + * @param array $daemons + * @param array $schedule + * @return \App\Deployment + */ + public function deploy($hash, array $build = [], array $activate = [], + array $directories = [], array $daemons = [], + array $schedule = []) + { + // First we will make sure we can actually deploy this stack. If the stack is not + // provisioned or we cannot obtain a lock, we will return out since we are not + // able to safely deploy to the stack. Otherwise, we can keep on going here. + if ($this->isDeploying() || ! $this->deploymentLock()->get()) { + throw new AlreadyDeployingException; + } + + $this->markAsDeploying(); + + // We will create a deployment record and start building this deployment, as well + // monitor it for its progress. This will update the deployment status as this + // deployment progresses as well as time it out if this never actually ends. + $deployment = $this->createDeployment( + $hash, $build, $activate, + $directories, $daemons, $schedule + ); + + return tap(tap($deployment, function ($deployment) { + $deployment->monitor(); + }))->build(); + } + + /** + * Deploy the given branch to the stack. + * + * @param string $branch + * @param array $build + * @param array $activate + * @param array $directories + * @param array $daemons + * @param array $schedule + * @return \App\Deployment + */ + public function deployBranch($branch, array $build = [], array $activate = [], + array $directories = [], array $daemons = [], + array $schedule = []) + { + $hash = $this->project()->sourceProvider->client()->latestHashFor( + $this->project()->repository, $branch + ); + + return tap($this->deploy( + $hash, $build, $activate, $directories, $daemons, $schedule + ))->update([ + 'branch' => $branch + ]); + } + + /** + * Deploy the given hash to the stack. + * + * @param string $hash + * @param array $build + * @param array $activate + * @param array $directories + * @param array $daemons + * @param array $schedule + * @return \App\Deployment + */ + public function deployHash($hash, array $build = [], array $activate = [], + array $directories = [], array $daemons = [], + array $schedule = []) + { + return $this->deploy( + $hash, $build, $activate, $directories, $daemons, $schedule + ); + } + + /** + * Mark the stack as deploying. + * + * @return void + */ + public function markAsDeploying() + { + $this->update([ + 'deployment_status' => 'deploying', + 'deployment_started_at' => Carbon::now(), + ]); + } + + /** + * Create a new deployment record for the stack. + * + * @param string $hash + * @param array $build + * @param array $activate + * @param array $directories + * @param array $daemons + * @param array $schedule + * @return \App\Deployment + */ + protected function createDeployment($hash, array $build = [], array $activate = [], + array $directories = [], array $daemons = [], + array $schedule = []) + { + return tap($this->deployments()->create([ + 'commit_hash' => $hash, + 'build_commands' => $build, + 'activation_commands' => $activate, + 'directories' => collect($directories)->map(function ($directory) { + return trim($directory, '/'); + })->all(), + 'daemons' => $daemons, + 'schedule' => $schedule, + 'status' => 'pending', + ]), function () { + $this->trimDeployments(); + }); + } + + /** + * Trim the total deployments for the stack. + * + * @return void + */ + protected function trimDeployments() + { + $deployments = $this->deployments()->get(); + + if (count($deployments) > 20) { + $deployments->slice(20 - count($deployments))->each->delete(); + } + } + + /** + * Determine if the stack has a pending deployment. + * + * @return bool + */ + public function hasPendingDeployment() + { + return ! empty($this->pending_deployment); + } + + /** + * Store the information for a pending deployment. + * + * @param \App\Hook $hook + * @param string $hash + * @return void + */ + public function storePendingDeployment(Hook $hook, $hash) + { + $this->update([ + 'pending_deployment' => [ + 'hook_id' => $hook->id, + 'commit_hash' => $hash, + ], + ]); + } + + /** + * Deploy the pending hook deployment if applicable. + * + * @return \App\Deployment + */ + public function deployPending() + { + if (! $this->hasPendingDeployment() || + ! $hook = Hook::find($this->pending_deployment['hook_id'])) { + return $this->resetPendingDeployment(); + } + + try { + $instructions = $this->pendingCommitHash() + ? DeploymentInstructions::fromHookCommit($hook, $this->pendingCommitHash()) + : DeploymentInstructions::forLatestHookCommit($hook); + + return $this->deployUsing($instructions); + } catch (Exception $e) { + report($e); + } finally { + $this->resetPendingDeployment(); + } + } + + /** + * Get the pending deployment's commit hash. + * + * @return string|null + */ + protected function pendingCommitHash() + { + return $this->pending_deployment['commit_hash'] ?? null; + } + + /** + * Reset the pending deployment information. + * + * @return void + */ + protected function resetPendingDeployment() + { + $this->update(['pending_deployment' => []]); + } + + /** + * Reset the deployment status for the stack. + * + * @return void + */ + public function resetDeploymentStatus() + { + $this->deploymentLock()->release(); + + $this->update([ + 'deployment_status' => null, + 'deployment_started_at' => null, + ]); + } + + /** + * Get a deployment lock instance for the stack. + * + * @return \Illuminate\Contracts\Cache\Lock + */ + public function deploymentLock() + { + return Cache::store('redis')->lock('deploy:'.$this->id, 60); + } + + /** + * Define the stack using the given definition. + * + * @param \App\Environment $environment + * @param \App\Contracts\StackDefinition $definition + * @return $this + */ + public static function createForEnvironment(Environment $environment, + StackDefinition $definition) + { + $project = $definition->project(); + + $stack = $environment->stacks()->create([ + 'creator_id' => $definition->creator()->id, + 'name' => $definition['name'], + 'url' => Haiku::withToken(), + 'pending_deployment' => [], + 'meta' => [ + 'php' => '7.1', + 'initial_branch' => $definition['branch'], + 'initial_build_commands' => $definition['build'] ?? [], + 'initial_activation_commands' => $definition['activate'] ?? [], + 'initial_directories' => $definition['directories'] ?? [], + 'initial_daemons' => $definition->daemons(), + 'initial_schedule' => $definition->schedule(), + 'scripts' => $definition->scripts(), + ], + ]); + + $stack->createServerRecords($definition); + + $databases = collect($definition['databases']); + + $stack->databases()->sync($databases->map(function ($name) use ($project) { + return $project->databases->where('name', $name)->first()->id; + })->all()); + + return $stack; + } + + /** + * Create the server records for the stack. + * + * @param \App\Contracts\StackDefinition $definition + * @return $this + */ + protected function createServerRecords(StackDefinition $definition) + { + (new AppServerRecordCreator($this, $definition))->create(); + (new WebServerRecordCreator($this, $definition))->create(); + (new WorkerServerRecordCreator($this, $definition))->create(); + + return $this; + } + + /** + * Determine if the stack is provisioned. + * + * @return bool + */ + public function isProvisioned() + { + return $this->status == 'provisioned'; + } + + /** + * Provision the stack. + * + * @return $this + */ + public function provision() + { + if ($this->status == 'provisioning') { + return; + } + + $this->update([ + 'status' => 'provisioning' + ]); + + StackProvisioning::dispatch($this); + + Jobs\CreateLoadBalancerIfNecessary::dispatch($this)->chain([ + new Jobs\ProvisionServers($this), + new Jobs\WaitForServersToFinishProvisioning($this), + new Jobs\SyncStackNetwork($this), + new Jobs\WaitForStackToFinishNetworking($this), + new Jobs\InstallRepository($this), + new Jobs\WaitForRepositoryInstallation($this), + new Jobs\SyncBalancers($this->environment->project), + new Jobs\AddDnsRecord($this), + new Jobs\WaitForDnsRecordToPropagate($this), + new Jobs\MarkStackAsProvisioned($this), + ]); + + return $this; + } + + /** + * Mark the stack as provisioned. + * + * @return void + */ + public function markAsProvisioned() + { + $this->update([ + 'status' => 'provisioned', + ]); + + StackProvisioned::dispatch($this); + } + + /** + * Get the PHP version for the stack. + * + * @return string + */ + public function phpVersion() + { + return $this->meta['php']; + } + + /** + * Delete the model from the database. + * + * @return bool|null + * + * @throws \Exception + */ + public function delete() + { + StackDeleting::dispatch($this); + + $this->balancers()->each->sync(10); + + if ($this->dns_address) { + Jobs\DeleteDnsRecord::dispatch( + $this->url, $this->dns_address + ); + } + + $this->databases->each->syncNetwork(5); + $this->databases()->detach(); + + rescue(function () { + $this->hooks->each->unpublish(); + }); + + $this->hooks()->delete(); + + $this->tasks()->delete(); + $this->deployments()->delete(); + $this->allServers()->each->delete(); + + parent::delete(); + } + + /** + * Convert the model instance to an array. + * + * @return array + */ + public function toArray() + { + return array_merge(parent::toArray(), [ + 'entrypoint' => $this->entrypoint(), + 'last_deployment' => $this->lastDeployment(), + ]); + } +} diff --git a/app/StackMetadata.php b/app/StackMetadata.php new file mode 100644 index 00000000..4871d374 --- /dev/null +++ b/app/StackMetadata.php @@ -0,0 +1,45 @@ +meta = $meta; + } + + /** + * Create a new metadata instance. + * + * @param array $meta + * @return static + */ + public static function from(array $meta) + { + return new static($meta); + } + + /** + * Prepare the metadata. + * + * @return array + */ + public function prepare() + { + return $this->meta; + } +} diff --git a/app/StackTask.php b/app/StackTask.php new file mode 100644 index 00000000..9ec83355 --- /dev/null +++ b/app/StackTask.php @@ -0,0 +1,163 @@ + 'json', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array + */ + protected $guarded = []; + + /** + * Get the stack that the task belongs to. + */ + public function stack() + { + return $this->belongsTo(Stack::class, 'stack_id'); + } + + /** + * Get all of the server tasks for the stack task. + */ + public function serverTasks() + { + return $this->hasMany(ServerTask::class, 'stack_task_id'); + } + + /** + * Queue the stack task to execution. + * + * @return void + */ + public function dispatch() + { + RunStackTask::dispatch($this); + } + + /** + * Run the task. + * + * @return void + */ + public function run() + { + $this->update([ + 'status' => 'running', + ]); + + $this->stack->allServers()->each(function ($server) { + $commands = $this->shellCommands()->filter->appliesTo($server)->reject->prefixed( + ! $server->isMaster() ? 'once:' : null + )->map->trim()->values()->all(); + + if (empty($commands)) { + return; + } + + $this->serverTasks()->create([ + 'taskable_id' => $server->id, + 'taskable_type' => get_class($server), + 'commands' => $commands, + ])->run(); + }); + + if ($this->serverTasks()->count() === 0) { + return $this->markAsFinished(); + } + + StackTaskRunning::dispatch($this); + } + + /** + * Get the task commands mapped into ShellCommand instances. + * + * @return \Illuminate\Support\Collection + */ + protected function shellCommands() + { + return collect($this->commands)->mapInto(ShellCommand::class); + } + + /** + * Sync the stack task status based on the server tasks. + * + * @return void + */ + public function syncStatus() + { + if ($this->serverTasks->contains->isRunning() || + $this->serverTasks->contains->isPending()) { + return; + } + + if ($this->serverTasks->contains->hasFailed()) { + $this->markAsFailed(); + } else { + $this->markAsFinished(); + } + } + + /** + * Determine if the stack task has finished. + * + * @return bool + */ + public function isFinished() + { + return $this->status === 'finished'; + } + + /** + * Mark the stack task as finished. + * + * @return void + */ + public function markAsFinished() + { + $this->update(['status' => 'finished']); + + StackTaskFinished::dispatch($this); + } + + /** + * Determine if th stack task has failed. + * + * @return bool + */ + public function hasFailed() + { + return $this->status === 'failed'; + } + + /** + * Mark the stack task as failed. + * + * @return void + */ + public function markAsFailed() + { + $this->update(['status' => 'failed']); + + StackTaskFailed::dispatch($this); + } +} diff --git a/app/StorageProvider.php b/app/StorageProvider.php new file mode 100644 index 00000000..87320249 --- /dev/null +++ b/app/StorageProvider.php @@ -0,0 +1,70 @@ + 'json', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array + */ + protected $guarded = []; + + /** + * The attributes that should be hidden for arrays. + * + * @var array + */ + protected $hidden = [ + 'meta', + ]; + + /** + * Get the user that owns the storage provider. + */ + public function user() + { + return $this->belongsTo(User::class, 'user_id'); + } + + /** + * Get all of the database backups attached to the provider. + */ + public function backups() + { + return $this->hasMany(DatabaseBackup::class); + } + + /** + * Get the name of the storage bucket. + * + * @return string + */ + public function bucket() + { + return $this->meta['bucket']; + } + + /** + * Get a storage provider client for the provider. + * + * @return \App\Contracts\StorageProviderClient + */ + public function client() + { + return StorageProviderClientFactory::make($this); + } +} diff --git a/app/StorageProviderClientFactory.php b/app/StorageProviderClientFactory.php new file mode 100644 index 00000000..4e883591 --- /dev/null +++ b/app/StorageProviderClientFactory.php @@ -0,0 +1,25 @@ +type) { + case 'S3': + return new S3($provider); + default: + throw new InvalidArgumentException("Invalid provider type."); + } + } +} diff --git a/app/Task.php b/app/Task.php new file mode 100644 index 00000000..bbbc2f09 --- /dev/null +++ b/app/Task.php @@ -0,0 +1,167 @@ +belongsTo(Project::class, 'project_id'); + } + + /** + * Get the provisionable entity the task belongs to. + */ + public function provisionable() + { + return $this->morphTo(); + } + + /** + * Determine if the task was successful. + * + * @return bool + */ + public function successful() + { + return (int) $this->exit_code === 0; + } + + /** + * Get the maximum execution time for the task. + * + * @return int + */ + public function timeout() + { + return (int) ($this->options['timeout'] ?? Task::DEFAULT_TIMEOUT); + } + + /** + * Get the value of the options array. + * + * @param string $value + * @return array + */ + public function getOptionsAttribute($value) + { + return unserialize($value); + } + + /** + * Set the value of the options array. + * + * @param array $value + * @return array + */ + public function setOptionsAttribute(array $value) + { + $this->attributes['options'] = serialize($value); + } + + /** + * Mark the task as finished and gather its output. + * + * @param int $exitCode + * @return void + */ + public function finish($exitCode = 0) + { + $this->markAsFinished($exitCode); + + $this->update([ + 'output' => $this->retrieveOutput(), + ]); + + foreach ($this->options['then'] ?? [] as $callback) { + is_object($callback) + ? $callback->handle($this) + : app($callback)->handle($this); + } + } + + /** + * Mark the task as running. + * + * @return $this + */ + protected function markAsRunning() + { + return tap($this)->update([ + 'status' => 'running', + ]); + } + + /** + * Determine if the task is running. + * + * @return bool + */ + public function isRunning() + { + return $this->status === 'running'; + } + + /** + * Mark the task as timed out. + * + * @param string $output + * @return $this + */ + protected function markAsTimedOut($output = '') + { + return tap($this)->update([ + 'exit_code' => 1, + 'status' => 'timeout', + 'output' => $output, + ]); + } + + /** + * Mark the task as finished. + * + * @param int $exitCode + * @param string $output + * @return $this + */ + protected function markAsFinished($exitCode = 0, $output = '') + { + return tap($this)->update([ + 'exit_code' => $exitCode, + 'status' => 'finished', + 'output' => $output, + ]); + } +} diff --git a/app/TaskFactory.php b/app/TaskFactory.php new file mode 100644 index 00000000..1b5b0d84 --- /dev/null +++ b/app/TaskFactory.php @@ -0,0 +1,30 @@ +timeout(); + } + + return $provisionable->tasks()->create([ + 'project_id' => $provisionable->projectId(), + 'name' => $script->name(), + 'user' => $script->sshAs, + 'options' => $options, + 'script' => (string) $script, + 'output' => '', + ]); + } +} diff --git a/app/User.php b/app/User.php new file mode 100644 index 00000000..33a9880f --- /dev/null +++ b/app/User.php @@ -0,0 +1,147 @@ + 'datetime', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array + */ + protected $guarded = []; + + /** + * The attributes that should be hidden for arrays. + * + * @var array + */ + protected $hidden = [ + 'api_token', + 'meta', + 'password', + 'private_key', + 'public_worker_key', + 'private_worker_key', + 'provider_key_id', + 'remember_token', + ]; + + /** + * Get all of the server providers owned by the user. + */ + public function serverProviders() + { + return $this->hasMany(ServerProvider::class); + } + + /** + * Get all of the source control providers owned by the user. + */ + public function sourceProviders() + { + return $this->hasMany(SourceProvider::class); + } + + /** + * Get all of the storage providers owned by the user. + */ + public function storageProviders() + { + return $this->hasMany(StorageProvider::class); + } + + /** + * All of the projects that belong to the user. + */ + public function projects() + { + return $this->hasMany(Project::class); + } + + /** + * All of the projects that have been shared with the user. + */ + public function teamProjects() + { + return $this->belongsToMany( + Project::class, 'project_users', 'user_id', 'project_id' + )->using(Collaborator::class); + } + + /** + * Determine if the user has access to the given project. + * + * @param Project $project + * @return bool + */ + public function canAccessProject($project) + { + return $this->projects->contains($project) || + $this->teamProjects->contains($project); + } + + /** + * Set the SSH key attributes on the model. + * + * @param object $value + * @return void + */ + public function setKeypairAttribute($value) + { + $this->attributes = [ + 'public_key' => $value->publicKey, + 'private_key' => $value->privateKey, + ] + $this->attributes; + } + + /** + * Set the SSH key attributes on the model. + * + * @param object $value + * @return void + */ + public function setWorkerKeypairAttribute($value) + { + $this->attributes = [ + 'public_worker_key' => $value->publicKey, + 'private_worker_key' => $value->privateKey, + ] + $this->attributes; + } + + /** + * Revoke API tokens with the given name. + * + * @param string $name + * @return void + */ + public function revokeTokens($name) + { + $this->tokens()->where('name', $name)->update(['revoked' => true]); + } + + /** + * Get the path to the user's worker SSH key. + * + * @return string + */ + public function keyPath() + { + return SecureShellKey::storeFor($this); + } +} diff --git a/app/WebServer.php b/app/WebServer.php new file mode 100644 index 00000000..9b5943f2 --- /dev/null +++ b/app/WebServer.php @@ -0,0 +1,53 @@ +is($this->stack->masterServer()); + } + + /** + * Dispatch the job to provision the server. + * + * @return void + */ + public function provision() + { + ProvisionWebServer::dispatch($this); + + $this->update(['provisioning_job_dispatched_at' => Carbon::now()]); + } + + /** + * Get the provisioning script for the server. + * + * @return \App\Scripts\Script + */ + public function provisioningScript() + { + return new Scripts\ProvisionWebServer($this); + } +} diff --git a/app/WebServerRecordCreator.php b/app/WebServerRecordCreator.php new file mode 100644 index 00000000..9c50510c --- /dev/null +++ b/app/WebServerRecordCreator.php @@ -0,0 +1,23 @@ +stack->webServers(); + } +} diff --git a/app/WorkerServer.php b/app/WorkerServer.php new file mode 100644 index 00000000..813b4887 --- /dev/null +++ b/app/WorkerServer.php @@ -0,0 +1,63 @@ +is($this->stack->masterWorker()); + } + + /** + * Determine if this server will run a given deployment command. + * + * @param string $command + * @return bool + */ + public function runsCommand($command) + { + return ! Str::startsWith($command, 'web:'); + } + + /** + * Dispatch the job to provision the server. + * + * @return void + */ + public function provision() + { + ProvisionWorkerServer::dispatch($this); + + $this->update(['provisioning_job_dispatched_at' => Carbon::now()]); + } + + /** + * Get the provisioning script for the server. + * + * @return \App\Scripts\Script + */ + public function provisioningScript() + { + return new Scripts\ProvisionWorkerServer($this); + } +} diff --git a/app/WorkerServerRecordCreator.php b/app/WorkerServerRecordCreator.php new file mode 100644 index 00000000..91dd0412 --- /dev/null +++ b/app/WorkerServerRecordCreator.php @@ -0,0 +1,23 @@ +stack->workerServers(); + } +} diff --git a/artisan b/artisan new file mode 100644 index 00000000..df630d0d --- /dev/null +++ b/artisan @@ -0,0 +1,51 @@ +#!/usr/bin/env php +make(Illuminate\Contracts\Console\Kernel::class); + +$status = $kernel->handle( + $input = new Symfony\Component\Console\Input\ArgvInput, + new Symfony\Component\Console\Output\ConsoleOutput +); + +/* +|-------------------------------------------------------------------------- +| Shutdown The Application +|-------------------------------------------------------------------------- +| +| Once Artisan has finished running. We will fire off the shutdown events +| so that any final work may be done by the application before we shut +| down the process. This is the last thing to happen to the request. +| +*/ + +$kernel->terminate($input, $status); + +exit($status); diff --git a/bootstrap/app.php b/bootstrap/app.php new file mode 100644 index 00000000..f2801adf --- /dev/null +++ b/bootstrap/app.php @@ -0,0 +1,55 @@ +singleton( + Illuminate\Contracts\Http\Kernel::class, + App\Http\Kernel::class +); + +$app->singleton( + Illuminate\Contracts\Console\Kernel::class, + App\Console\Kernel::class +); + +$app->singleton( + Illuminate\Contracts\Debug\ExceptionHandler::class, + App\Exceptions\Handler::class +); + +/* +|-------------------------------------------------------------------------- +| Return The Application +|-------------------------------------------------------------------------- +| +| This script returns the application instance. The instance is given to +| the calling script so we can separate the building of the instances +| from the actual running of the application and sending responses. +| +*/ + +return $app; diff --git a/bootstrap/autoload.php b/bootstrap/autoload.php new file mode 100644 index 00000000..94adc997 --- /dev/null +++ b/bootstrap/autoload.php @@ -0,0 +1,17 @@ +=7.0.0", + "aws/aws-sdk-php": "^3.28", + "barryvdh/laravel-cors": "^0.9.2", + "doctrine/dbal": "^2.5", + "guzzlehttp/guzzle": "^6.2", + "hashids/hashids": "^2.0", + "laravel/framework": "5.5.*", + "laravel/passport": "^3.0", + "laravel/tinker": "~1.0", + "predis/predis": "^1.1", + "pusher/pusher-php-server": "^3.0", + "ramsey/uuid": "^3.7", + "spatie/once": "^1.0", + "symfony/yaml": "^3.3" + }, + "require-dev": { + "fzaninotto/faker": "~1.4", + "mockery/mockery": "0.9.*", + "phpunit/phpunit": "~6.0" + }, + "autoload": { + "classmap": [ + "database" + ], + "psr-4": { + "App\\": "app/" + }, + "files": [ + "helpers.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "scripts": { + "post-root-package-install": [ + "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" + ], + "post-create-project-cmd": [ + "@php artisan key:generate" + ], + "post-autoload-dump": [ + "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", + "@php artisan package:discover" + ] + }, + "config": { + "preferred-install": "dist", + "sort-packages": true, + "optimize-autoloader": true + }, + "minimum-stability": "dev" +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 00000000..a52f0c8d --- /dev/null +++ b/composer.lock @@ -0,0 +1,5354 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "content-hash": "be323195effb7b84c355c19522337158", + "packages": [ + { + "name": "aws/aws-sdk-php", + "version": "3.36.2", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "98bff6b24863bb0da970449d18f6745b9270add9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/98bff6b24863bb0da970449d18f6745b9270add9", + "reference": "98bff6b24863bb0da970449d18f6745b9270add9", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "ext-spl": "*", + "guzzlehttp/guzzle": "^5.3.1|^6.2.1", + "guzzlehttp/promises": "~1.0", + "guzzlehttp/psr7": "^1.4.1", + "mtdowling/jmespath.php": "~2.2", + "php": ">=5.5" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "nette/neon": "^2.3", + "phpunit/phpunit": "^4.8.35|^5.4.0", + "psr/cache": "^1.0" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Aws\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "time": "2017-09-06T16:44:34+00:00" + }, + { + "name": "barryvdh/laravel-cors", + "version": "v0.9.3", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-cors.git", + "reference": "2551489de60486471434b0c7050f7fc65f9c9119" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-cors/zipball/2551489de60486471434b0c7050f7fc65f9c9119", + "reference": "2551489de60486471434b0c7050f7fc65f9c9119", + "shasum": "" + }, + "require": { + "illuminate/support": "5.3.x|5.4.x|5.5.x", + "php": ">=5.5.9", + "symfony/http-foundation": "~3.1", + "symfony/http-kernel": "~3.1" + }, + "require-dev": { + "orchestra/testbench": "3.x", + "phpunit/phpunit": "^4.8|^5.2", + "squizlabs/php_codesniffer": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.9-dev" + }, + "laravel": { + "providers": [ + "Barryvdh\\Cors\\ServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Barryvdh\\Cors\\": "src/" + }, + "classmap": [ + "tests" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "Adds CORS (Cross-Origin Resource Sharing) headers support in your Laravel application", + "keywords": [ + "api", + "cors", + "crossdomain", + "laravel" + ], + "time": "2017-08-28T11:42:05+00:00" + }, + { + "name": "defuse/php-encryption", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/defuse/php-encryption.git", + "reference": "5176f5abb38d3ea8a6e3ac6cd3bbb54d8185a689" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/defuse/php-encryption/zipball/5176f5abb38d3ea8a6e3ac6cd3bbb54d8185a689", + "reference": "5176f5abb38d3ea8a6e3ac6cd3bbb54d8185a689", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "paragonie/random_compat": "~2.0", + "php": ">=5.4.0" + }, + "require-dev": { + "nikic/php-parser": "^2.0|^3.0", + "phpunit/phpunit": "^4|^5" + }, + "bin": [ + "bin/generate-defuse-key" + ], + "type": "library", + "autoload": { + "psr-4": { + "Defuse\\Crypto\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Hornby", + "email": "taylor@defuse.ca", + "homepage": "https://defuse.ca/" + }, + { + "name": "Scott Arciszewski", + "email": "info@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "Secure PHP Encryption Library", + "keywords": [ + "aes", + "authenticated encryption", + "cipher", + "crypto", + "cryptography", + "encrypt", + "encryption", + "openssl", + "security", + "symmetric key cryptography" + ], + "time": "2017-05-18T21:28:48+00:00" + }, + { + "name": "dnoegel/php-xdg-base-dir", + "version": "0.1", + "source": { + "type": "git", + "url": "https://github.com/dnoegel/php-xdg-base-dir.git", + "reference": "265b8593498b997dc2d31e75b89f053b5cc9621a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/265b8593498b997dc2d31e75b89f053b5cc9621a", + "reference": "265b8593498b997dc2d31e75b89f053b5cc9621a", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "@stable" + }, + "type": "project", + "autoload": { + "psr-4": { + "XdgBaseDir\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "implementation of xdg base directory specification for php", + "time": "2014-10-24T07:27:01+00:00" + }, + { + "name": "doctrine/annotations", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "2497b1f9db56278d3ad2248f9e4bdbbbaa271c3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/2497b1f9db56278d3ad2248f9e4bdbbbaa271c3e", + "reference": "2497b1f9db56278d3ad2248f9e4bdbbbaa271c3e", + "shasum": "" + }, + "require": { + "doctrine/lexer": "1.*", + "php": "^7.1" + }, + "require-dev": { + "doctrine/cache": "1.*", + "phpunit/phpunit": "^5.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "time": "2017-07-22T11:08:38+00:00" + }, + { + "name": "doctrine/cache", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "beb0fa35b61e9073f8612d9ffd34920bdaec406a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/beb0fa35b61e9073f8612d9ffd34920bdaec406a", + "reference": "beb0fa35b61e9073f8612d9ffd34920bdaec406a", + "shasum": "" + }, + "require": { + "php": "~7.1" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "alcaeus/mongo-php-adapter": "^1.1", + "doctrine/coding-standard": "^1.0", + "mongodb/mongodb": "^1.1", + "phpunit/phpunit": "^6.3", + "predis/predis": "~1.0", + "squizlabs/php_codesniffer": "^3.0" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Caching library offering an object-oriented API for many cache backends", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "cache", + "caching" + ], + "time": "2017-08-25T06:51:37+00:00" + }, + { + "name": "doctrine/collections", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "42c4039eca9535e4f201f50d2695648658e9e5a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/42c4039eca9535e4f201f50d2695648658e9e5a8", + "reference": "42c4039eca9535e4f201f50d2695648658e9e5a8", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "doctrine/coding-standard": "^1.0", + "phpunit/phpunit": "^6.3", + "squizlabs/php_codesniffer": "^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Collections\\": "lib/Doctrine/Common/Collections" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Collections Abstraction library", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "array", + "collections", + "iterator" + ], + "time": "2017-08-30T09:16:50+00:00" + }, + { + "name": "doctrine/common", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/doctrine/common.git", + "reference": "a3e240fa07ec9748387c13534949d543c0e177fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/common/zipball/a3e240fa07ec9748387c13534949d543c0e177fa", + "reference": "a3e240fa07ec9748387c13534949d543c0e177fa", + "shasum": "" + }, + "require": { + "doctrine/annotations": "1.*", + "doctrine/cache": "1.*", + "doctrine/collections": "1.*", + "doctrine/inflector": "1.*", + "doctrine/lexer": "1.*", + "php": "~7.1" + }, + "require-dev": { + "doctrine/coding-standard": "^1.0", + "phpunit/phpunit": "^6.3", + "squizlabs/php_codesniffer": "^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.9.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Common Library for Doctrine projects", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "annotations", + "collections", + "eventmanager", + "persistence", + "spl" + ], + "time": "2017-09-02T17:51:46+00:00" + }, + { + "name": "doctrine/dbal", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "8d18a33043edd1dd0b2a996262b67523e1b83f05" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/8d18a33043edd1dd0b2a996262b67523e1b83f05", + "reference": "8d18a33043edd1dd0b2a996262b67523e1b83f05", + "shasum": "" + }, + "require": { + "doctrine/common": "^2.7.1", + "ext-pdo": "*", + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^5.4.6", + "phpunit/phpunit-mock-objects": "!=3.2.4,!=3.2.5", + "symfony/console": "2.*||^3.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\DBAL\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Database Abstraction Layer", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "database", + "dbal", + "persistence", + "queryobject" + ], + "time": "2017-08-31T15:12:49+00:00" + }, + { + "name": "doctrine/inflector", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "03d4145619c2762948e4a8c53a899af24a1e4f73" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/03d4145619c2762948e4a8c53a899af24a1e4f73", + "reference": "03d4145619c2762948e4a8c53a899af24a1e4f73", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Inflector\\": "lib/Doctrine/Common/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Common String Manipulations with regard to casing and singular/plural rules.", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "inflection", + "pluralize", + "singularize", + "string" + ], + "time": "2017-08-25T04:43:19+00:00" + }, + { + "name": "doctrine/lexer", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "cc709ba91eee09540091ad5a5f2616727662e41b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/cc709ba91eee09540091ad5a5f2616727662e41b", + "reference": "cc709ba91eee09540091ad5a5f2616727662e41b", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "lexer", + "parser" + ], + "time": "2017-07-24T09:37:08+00:00" + }, + { + "name": "egulias/email-validator", + "version": "2.1.2", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "bc31baa11ea2883e017f0a10d9722ef9d50eac1c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/bc31baa11ea2883e017f0a10d9722ef9d50eac1c", + "reference": "bc31baa11ea2883e017f0a10d9722ef9d50eac1c", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^1.0.1", + "php": ">= 5.5" + }, + "require-dev": { + "dominicsayers/isemail": "dev-master", + "phpunit/phpunit": "^4.8.0", + "satooshi/php-coveralls": "dev-master" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "EmailValidator" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "time": "2017-01-30T22:07:36+00:00" + }, + { + "name": "erusev/parsedown", + "version": "1.6.3", + "source": { + "type": "git", + "url": "https://github.com/erusev/parsedown.git", + "reference": "728952b90a333b5c6f77f06ea9422b94b585878d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/erusev/parsedown/zipball/728952b90a333b5c6f77f06ea9422b94b585878d", + "reference": "728952b90a333b5c6f77f06ea9422b94b585878d", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Parsedown": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Emanuil Rusev", + "email": "hello@erusev.com", + "homepage": "http://erusev.com" + } + ], + "description": "Parser for Markdown.", + "homepage": "http://parsedown.org", + "keywords": [ + "markdown", + "parser" + ], + "time": "2017-05-14T14:47:48+00:00" + }, + { + "name": "firebase/php-jwt", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "dccf163dc8ed7ed6a00afc06c51ee5186a428d35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/dccf163dc8ed7ed6a00afc06c51ee5186a428d35", + "reference": "dccf163dc8ed7ed6a00afc06c51ee5186a428d35", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "time": "2016-07-18T04:51:16+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4db5a78a5ea468d4831de7f0bf9d9415e348699", + "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699", + "shasum": "" + }, + "require": { + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.4", + "php": ">=5.5" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.0 || ^5.0", + "psr/log": "^1.0" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.2-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2017-06-22T18:50:49+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "09e549f5534380c68761260a71f847644d8f65aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/09e549f5534380c68761260a71f847644d8f65aa", + "reference": "09e549f5534380c68761260a71f847644d8f65aa", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2017-05-20T23:14:18+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "811b676fbab9c99e359885032e5ebc70e442f5b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/811b676fbab9c99e359885032e5ebc70e442f5b8", + "reference": "811b676fbab9c99e359885032e5ebc70e442f5b8", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2017-07-17T09:11:21+00:00" + }, + { + "name": "hashids/hashids", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/ivanakimov/hashids.php.git", + "reference": "413912d01651414c3b488b46da13689751d5dceb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ivanakimov/hashids.php/zipball/413912d01651414c3b488b46da13689751d5dceb", + "reference": "413912d01651414c3b488b46da13689751d5dceb", + "shasum": "" + }, + "require": { + "ext-bcmath": "*", + "php": "^5.6.4 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "psr-4": { + "Hashids\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ivan Akimov", + "email": "ivan@barreleye.com", + "homepage": "https://twitter.com/IvanAkimov" + }, + { + "name": "Vincent Klaiber", + "email": "hello@vinkla.com", + "homepage": "https://vinkla.com" + } + ], + "description": "Generate short, unique, non-sequential ids (like YouTube and Bitly) from numbers", + "homepage": "http://hashids.org/php", + "keywords": [ + "bitly", + "decode", + "encode", + "hash", + "hashid", + "hashids", + "ids", + "obfuscate", + "youtube" + ], + "time": "2017-06-15T13:53:56+00:00" + }, + { + "name": "jakub-onderka/php-console-color", + "version": "0.1", + "source": { + "type": "git", + "url": "https://github.com/JakubOnderka/PHP-Console-Color.git", + "reference": "e0b393dacf7703fc36a4efc3df1435485197e6c1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Color/zipball/e0b393dacf7703fc36a4efc3df1435485197e6c1", + "reference": "e0b393dacf7703fc36a4efc3df1435485197e6c1", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "jakub-onderka/php-code-style": "1.0", + "jakub-onderka/php-parallel-lint": "0.*", + "jakub-onderka/php-var-dump-check": "0.*", + "phpunit/phpunit": "3.7.*", + "squizlabs/php_codesniffer": "1.*" + }, + "type": "library", + "autoload": { + "psr-0": { + "JakubOnderka\\PhpConsoleColor": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Jakub Onderka", + "email": "jakub.onderka@gmail.com", + "homepage": "http://www.acci.cz" + } + ], + "time": "2014-04-08T15:00:19+00:00" + }, + { + "name": "jakub-onderka/php-console-highlighter", + "version": "v0.3.2", + "source": { + "type": "git", + "url": "https://github.com/JakubOnderka/PHP-Console-Highlighter.git", + "reference": "7daa75df45242c8d5b75a22c00a201e7954e4fb5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Highlighter/zipball/7daa75df45242c8d5b75a22c00a201e7954e4fb5", + "reference": "7daa75df45242c8d5b75a22c00a201e7954e4fb5", + "shasum": "" + }, + "require": { + "jakub-onderka/php-console-color": "~0.1", + "php": ">=5.3.0" + }, + "require-dev": { + "jakub-onderka/php-code-style": "~1.0", + "jakub-onderka/php-parallel-lint": "~0.5", + "jakub-onderka/php-var-dump-check": "~0.1", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~1.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JakubOnderka\\PhpConsoleHighlighter": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jakub Onderka", + "email": "acci@acci.cz", + "homepage": "http://www.acci.cz/" + } + ], + "time": "2015-04-20T18:58:01+00:00" + }, + { + "name": "laravel/framework", + "version": "5.5.x-dev", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "48ba3c0cd087c3b55cfe16033e7261bcd9c57239" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/48ba3c0cd087c3b55cfe16033e7261bcd9c57239", + "reference": "48ba3c0cd087c3b55cfe16033e7261bcd9c57239", + "shasum": "" + }, + "require": { + "doctrine/inflector": "~1.1", + "erusev/parsedown": "~1.6", + "ext-mbstring": "*", + "ext-openssl": "*", + "league/flysystem": "~1.0", + "monolog/monolog": "~1.12", + "mtdowling/cron-expression": "~1.0", + "nesbot/carbon": "~1.20", + "php": ">=7.0", + "psr/container": "~1.0", + "psr/simple-cache": "^1.0", + "ramsey/uuid": "~3.0", + "swiftmailer/swiftmailer": "~6.0", + "symfony/console": "~3.3", + "symfony/debug": "~3.3", + "symfony/finder": "~3.3", + "symfony/http-foundation": "~3.3", + "symfony/http-kernel": "~3.3", + "symfony/process": "~3.3", + "symfony/routing": "~3.3", + "symfony/var-dumper": "~3.3", + "tijsverkoyen/css-to-inline-styles": "~2.2", + "vlucas/phpdotenv": "~2.2" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/exception": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/log": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "tightenco/collect": "self.version" + }, + "require-dev": { + "aws/aws-sdk-php": "~3.0", + "doctrine/dbal": "~2.5", + "filp/whoops": "^2.1.4", + "mockery/mockery": "~1.0", + "orchestra/testbench-core": "3.5.*", + "pda/pheanstalk": "~3.0", + "phpunit/phpunit": "~6.0", + "predis/predis": "^1.1.1", + "symfony/css-selector": "~3.3", + "symfony/dom-crawler": "~3.3" + }, + "suggest": { + "aws/aws-sdk-php": "Required to use the SQS queue driver and SES mail driver (~3.0).", + "doctrine/dbal": "Required to rename columns and drop SQLite columns (~2.5).", + "fzaninotto/faker": "Required to use the eloquent factory builder (~1.4).", + "guzzlehttp/guzzle": "Required to use the Mailgun and Mandrill mail drivers and the ping methods on schedules (~6.0).", + "laravel/tinker": "Required to use the tinker console command (~1.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (~1.0).", + "league/flysystem-rackspace": "Required to use the Flysystem Rackspace driver (~1.0).", + "nexmo/client": "Required to use the Nexmo transport (~1.0).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (~3.0).", + "predis/predis": "Required to use the redis cache and queue drivers (~1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (~2.0).", + "symfony/css-selector": "Required to use some of the crawler integration testing tools (~3.3).", + "symfony/dom-crawler": "Required to use most of the crawler integration testing tools (~3.3).", + "symfony/psr-http-message-bridge": "Required to psr7 bridging features (~1.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.5-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "time": "2017-09-07 13:09:56" + }, + { + "name": "laravel/passport", + "version": "3.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/laravel/passport.git", + "reference": "c6a23ff4f05543767b2828b582dd9e74cdb9b014" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/passport/zipball/c6a23ff4f05543767b2828b582dd9e74cdb9b014", + "reference": "c6a23ff4f05543767b2828b582dd9e74cdb9b014", + "shasum": "" + }, + "require": { + "firebase/php-jwt": "~3.0|~4.0", + "guzzlehttp/guzzle": "~6.0", + "illuminate/auth": "~5.4", + "illuminate/console": "~5.4", + "illuminate/container": "~5.4", + "illuminate/contracts": "~5.4", + "illuminate/database": "~5.4", + "illuminate/encryption": "~5.4", + "illuminate/http": "~5.4", + "illuminate/support": "~5.4", + "league/oauth2-server": "^6.0", + "php": ">=5.6.4", + "phpseclib/phpseclib": "^2.0", + "symfony/psr-http-message-bridge": "~1.0", + "zendframework/zend-diactoros": "~1.0" + }, + "require-dev": { + "mockery/mockery": "~0.9", + "phpunit/phpunit": "~5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Passport\\PassportServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Passport\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Passport provides OAuth2 server support to Laravel.", + "keywords": [ + "laravel", + "oauth", + "passport" + ], + "time": "2017-08-16T13:10:52+00:00" + }, + { + "name": "laravel/tinker", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "203978fd67f118902acff95925847e70b72e3daf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/203978fd67f118902acff95925847e70b72e3daf", + "reference": "203978fd67f118902acff95925847e70b72e3daf", + "shasum": "" + }, + "require": { + "illuminate/console": "~5.1", + "illuminate/contracts": "~5.1", + "illuminate/support": "~5.1", + "php": ">=5.5.9", + "psy/psysh": "0.7.*|0.8.*", + "symfony/var-dumper": "~3.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0|~5.0" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (~5.1)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "time": "2017-07-13T13:11:05+00:00" + }, + { + "name": "lcobucci/jwt", + "version": "3.3.x-dev", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "2c2c77fb863a20cc90b7b686c88275e85581bd03" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/2c2c77fb863a20cc90b7b686c88275e85581bd03", + "reference": "2c2c77fb863a20cc90b7b686c88275e85581bd03", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=5.5" + }, + "require-dev": { + "mdanter/ecc": "~0.3.1", + "mikey179/vfsstream": "~1.5", + "phpmd/phpmd": "~2.2", + "phpunit/php-invoker": "~1.1", + "phpunit/phpunit": "~4.5", + "squizlabs/php_codesniffer": "~2.3" + }, + "suggest": { + "mdanter/ecc": "Required to use Elliptic Curves based algorithms." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Luís Otávio Cobucci Oblonczyk", + "email": "lcobucci@gmail.com", + "role": "Developer" + } + ], + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "keywords": [ + "JWS", + "jwt" + ], + "time": "2016-10-31T20:12:20+00:00" + }, + { + "name": "league/event", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/event.git", + "reference": "0bfa3954c93a7b596168b4fd32d0e6631fd175e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/event/zipball/0bfa3954c93a7b596168b4fd32d0e6631fd175e2", + "reference": "0bfa3954c93a7b596168b4fd32d0e6631fd175e2", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "henrikbjorn/phpspec-code-coverage": "~1.0.1", + "phpspec/phpspec": "^2.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Event\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Event package", + "keywords": [ + "emitter", + "event", + "listener" + ], + "time": "2016-07-23T09:33:29+00:00" + }, + { + "name": "league/flysystem", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "3245df3acae2dded99694b499d3bbf937679c8f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/3245df3acae2dded99694b499d3bbf937679c8f3", + "reference": "3245df3acae2dded99694b499d3bbf937679c8f3", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "conflict": { + "league/flysystem-sftp": "<1.0.6" + }, + "require-dev": { + "ext-fileinfo": "*", + "mockery/mockery": "~0.9", + "phpspec/phpspec": "^2.2", + "phpunit/phpunit": "~4.8" + }, + "suggest": { + "ext-fileinfo": "Required for MimeType", + "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", + "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", + "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", + "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", + "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", + "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", + "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", + "league/flysystem-webdav": "Allows you to use WebDAV storage", + "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", + "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", + "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Filesystem abstraction: Many filesystems, one API.", + "keywords": [ + "Cloud Files", + "WebDAV", + "abstraction", + "aws", + "cloud", + "copy.com", + "dropbox", + "file systems", + "files", + "filesystem", + "filesystems", + "ftp", + "rackspace", + "remote", + "s3", + "sftp", + "storage" + ], + "time": "2017-09-04T09:35:39+00:00" + }, + { + "name": "league/oauth2-server", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-server.git", + "reference": "925776958fc3f5278e74363663c20147af32b668" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/925776958fc3f5278e74363663c20147af32b668", + "reference": "925776958fc3f5278e74363663c20147af32b668", + "shasum": "" + }, + "require": { + "defuse/php-encryption": "^2.1", + "ext-openssl": "*", + "lcobucci/jwt": "^3.1", + "league/event": "^2.1", + "paragonie/random_compat": "^2.0", + "php": ">=5.6.0", + "psr/http-message": "^1.0" + }, + "replace": { + "league/oauth2server": "*", + "lncd/oauth2": "*" + }, + "require-dev": { + "phpunit/phpunit": "^4.8 || ^5.0", + "zendframework/zend-diactoros": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\OAuth2\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + } + ], + "description": "A lightweight and powerful OAuth 2.0 authorization and resource server library with support for all the core specification grants. This library will allow you to secure your API with OAuth and allow your applications users to approve apps that want to access their data from your API.", + "homepage": "https://oauth2.thephpleague.com/", + "keywords": [ + "Authentication", + "api", + "auth", + "authorisation", + "authorization", + "oauth", + "oauth 2", + "oauth 2.0", + "oauth2", + "protect", + "resource", + "secure", + "server" + ], + "time": "2017-08-03T15:09:23+00:00" + }, + { + "name": "monolog/monolog", + "version": "1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/fd8c787753b3a2ad11bc60c063cff1358a32a3b4", + "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/log": "~1.0" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "graylog2/gelf-php": "~1.0", + "jakub-onderka/php-parallel-lint": "0.9", + "php-amqplib/php-amqplib": "~2.4", + "php-console/php-console": "^3.1.3", + "phpunit/phpunit": "~4.5", + "phpunit/phpunit-mock-objects": "2.3.0", + "ruflin/elastica": ">=0.90 <3.0", + "sentry/sentry": "^0.13", + "swiftmailer/swiftmailer": "^5.3|^6.0" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mongo": "Allow sending log messages to a MongoDB server", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server", + "sentry/sentry": "Allow sending log messages to a Sentry server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "http://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "time": "2017-06-19T01:22:40+00:00" + }, + { + "name": "mtdowling/cron-expression", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/mtdowling/cron-expression.git", + "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mtdowling/cron-expression/zipball/9504fa9ea681b586028adaaa0877db4aecf32bad", + "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.0|~5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "time": "2017-01-23T04:29:33+00:00" + }, + { + "name": "mtdowling/jmespath.php", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/adcc9531682cf87dfda21e1fd5d0e7a41d292fac", + "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "JmesPath\\": "src/" + }, + "files": [ + "src/JmesPath.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "time": "2016-12-03T22:08:25+00:00" + }, + { + "name": "nesbot/carbon", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/briannesbitt/Carbon.git", + "reference": "926aee5ab38c2868816aa760f862a85ad01cb61a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/926aee5ab38c2868816aa760f862a85ad01cb61a", + "reference": "926aee5ab38c2868816aa760f862a85ad01cb61a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "symfony/translation": "~2.6 || ~3.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2", + "phpunit/phpunit": "~4.0 || ~5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.23-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "http://nesbot.com" + } + ], + "description": "A simple API extension for DateTime.", + "homepage": "http://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "time": "2017-02-06T22:02:47+00:00" + }, + { + "name": "nikic/php-parser", + "version": "3.x-dev", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "a1e8e1a30e1352f118feff1a8481066ddc2f234a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a1e8e1a30e1352f118feff1a8481066ddc2f234a", + "reference": "a1e8e1a30e1352f118feff1a8481066ddc2f234a", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "~4.0|~5.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "time": "2017-09-02T17:10:46+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v2.0.10", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "634bae8e911eefa89c1abfbf1b66da679ac8f54d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/634bae8e911eefa89c1abfbf1b66da679ac8f54d", + "reference": "634bae8e911eefa89c1abfbf1b66da679ac8f54d", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "autoload": { + "files": [ + "lib/random.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "pseudorandom", + "random" + ], + "time": "2017-03-13T16:27:32+00:00" + }, + { + "name": "phpseclib/phpseclib", + "version": "2.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "74d3a51183a57f3773623b043d91ee432edd9483" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/74d3a51183a57f3773623b043d91ee432edd9483", + "reference": "74d3a51183a57f3773623b043d91ee432edd9483", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phing/phing": "~2.7", + "phpunit/phpunit": "~4.0", + "sami/sami": "~2.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "suggest": { + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "time": "2017-09-06T05:27:52+00:00" + }, + { + "name": "predis/predis", + "version": "v1.1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/nrk/predis.git", + "reference": "111d100ee389d624036b46b35ed0c9ac59c71313" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nrk/predis/zipball/111d100ee389d624036b46b35ed0c9ac59c71313", + "reference": "111d100ee389d624036b46b35ed0c9ac59c71313", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "suggest": { + "ext-curl": "Allows access to Webdis when paired with phpiredis", + "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol" + }, + "type": "library", + "autoload": { + "psr-4": { + "Predis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniele Alessandri", + "email": "suppakilla@gmail.com", + "homepage": "http://clorophilla.net" + } + ], + "description": "Flexible and feature-complete Redis client for PHP and HHVM", + "homepage": "http://github.com/nrk/predis", + "keywords": [ + "nosql", + "predis", + "redis" + ], + "time": "2017-07-12T14:39:17+00:00" + }, + { + "name": "psr/container", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "2cc4a01788191489dc7459446ba832fa79a216a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/2cc4a01788191489dc7459446ba832fa79a216a7", + "reference": "2cc4a01788191489dc7459446ba832fa79a216a7", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2017-06-28T15:35:32+00:00" + }, + { + "name": "psr/http-message", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/log", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2016-10-10T12:19:37+00:00" + }, + { + "name": "psr/simple-cache", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "753fa598e8f3b9966c886fe13f370baa45ef0e24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/753fa598e8f3b9966c886fe13f370baa45ef0e24", + "reference": "753fa598e8f3b9966c886fe13f370baa45ef0e24", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "time": "2017-01-02T13:31:39+00:00" + }, + { + "name": "psy/psysh", + "version": "dev-develop", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "71b422a177d10d266a8dcb8154ae3a5bfbc26adf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/71b422a177d10d266a8dcb8154ae3a5bfbc26adf", + "reference": "71b422a177d10d266a8dcb8154ae3a5bfbc26adf", + "shasum": "" + }, + "require": { + "dnoegel/php-xdg-base-dir": "0.1", + "jakub-onderka/php-console-highlighter": "0.3.*", + "nikic/php-parser": "~1.3|~2.0|~3.0", + "php": ">=5.3.9", + "symfony/console": "~2.3.10|^2.4.2|~3.0", + "symfony/var-dumper": "~2.7|~3.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~1.11", + "hoa/console": "~3.16|~1.14", + "phpunit/phpunit": "~4.4|~5.0", + "symfony/finder": "~2.1|~3.0" + }, + "suggest": { + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-pdo-sqlite": "The doc command requires SQLite to work.", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well.", + "ext-readline": "Enables support for arrow-key history navigation, and showing and manipulating command history.", + "hoa/console": "A pure PHP readline implementation. You'll want this if your PHP install doesn't already support readline or libedit." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-develop": "0.8.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psy/functions.php" + ], + "psr-4": { + "Psy\\": "src/Psy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info", + "homepage": "http://justinhileman.com" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "http://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "time": "2017-09-06T07:27:32+00:00" + }, + { + "name": "pusher/pusher-php-server", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/pusher/pusher-http-php.git", + "reference": "866175e7923fbe2ad65750e179e1d9ed2875089e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/866175e7923fbe2ad65750e179e1d9ed2875089e", + "reference": "866175e7923fbe2ad65750e179e1d9ed2875089e", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": "^5.4 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8 || ^5.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Pusher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Library for interacting with the Pusher REST API", + "keywords": [ + "events", + "messaging", + "php-pusher-server", + "publish", + "push", + "pusher", + "real time", + "real-time", + "realtime", + "rest", + "trigger" + ], + "time": "2017-08-10T09:34:41+00:00" + }, + { + "name": "ramsey/uuid", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "0ef23d1b10cf1bc576e9d865a7e9c47982c5715e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/0ef23d1b10cf1bc576e9d865a7e9c47982c5715e", + "reference": "0ef23d1b10cf1bc576e9d865a7e9c47982c5715e", + "shasum": "" + }, + "require": { + "paragonie/random_compat": "^1.0|^2.0", + "php": "^5.4 || ^7.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "apigen/apigen": "^4.1", + "codeception/aspect-mock": "^1.0 | ^2.0", + "doctrine/annotations": "~1.2.0", + "goaop/framework": "1.0.0-alpha.2 | ^1.0 | ^2.1", + "ircmaxell/random-lib": "^1.1", + "jakub-onderka/php-parallel-lint": "^0.9.0", + "mockery/mockery": "^0.9.4", + "moontoast/math": "^1.1", + "php-mock/php-mock-phpunit": "^0.3|^1.1", + "phpunit/phpunit": "^4.7|>=5.0 <5.4", + "satooshi/php-coveralls": "^0.6.1", + "squizlabs/php_codesniffer": "^2.3" + }, + "suggest": { + "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator", + "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator", + "ircmaxell/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).", + "ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marijn Huizendveld", + "email": "marijn.huizendveld@gmail.com" + }, + { + "name": "Thibaud Fabre", + "email": "thibaud@aztech.io" + }, + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).", + "homepage": "https://github.com/ramsey/uuid", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "time": "2017-08-04T13:39:04+00:00" + }, + { + "name": "spatie/once", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/once.git", + "reference": "26b52e4571882c526d0bc017f354d997d263a98e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/once/zipball/26b52e4571882c526d0bc017f354d997d263a98e", + "reference": "26b52e4571882c526d0bc017f354d997d263a98e", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "illuminate/support": "~5.3.0", + "phpunit/phpunit": "5.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Once\\": "src" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "A magic memoization function", + "homepage": "https://github.com/spatie/once", + "keywords": [ + "cache", + "callable", + "memozation", + "once", + "spatie" + ], + "time": "2017-08-24T21:16:25+00:00" + }, + { + "name": "swiftmailer/swiftmailer", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/swiftmailer/swiftmailer.git", + "reference": "1a2f9df896c8bfa6e22e8e1a78cca637c6f70613" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/1a2f9df896c8bfa6e22e8e1a78cca637c6f70613", + "reference": "1a2f9df896c8bfa6e22e8e1a78cca637c6f70613", + "shasum": "" + }, + "require": { + "egulias/email-validator": "~2.0", + "php": ">=7.0.0" + }, + "require-dev": { + "mockery/mockery": "~0.9.1", + "symfony/phpunit-bridge": "~3.3@dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.0-dev" + } + }, + "autoload": { + "files": [ + "lib/swift_required.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Corbyn" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Swiftmailer, free feature-rich PHP mailer", + "homepage": "http://swiftmailer.symfony.com", + "keywords": [ + "email", + "mail", + "mailer" + ], + "time": "2017-08-29T20:44:31+00:00" + }, + { + "name": "symfony/console", + "version": "3.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "604b8cb9f694b05292bf3b580b298d50c1974e7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/604b8cb9f694b05292bf3b580b298d50c1974e7e", + "reference": "604b8cb9f694b05292bf3b580b298d50c1974e7e", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/debug": "~2.8|~3.0|~4.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/dependency-injection": "<3.3", + "symfony/process": "<3.3" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.3|~4.0", + "symfony/dependency-injection": "~3.3|~4.0", + "symfony/event-dispatcher": "~2.8|~3.0|~4.0", + "symfony/lock": "~3.4|~4.0", + "symfony/process": "~3.3|~4.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2017-09-06T19:47:44+00:00" + }, + { + "name": "symfony/css-selector", + "version": "3.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "7857a524c7e23c59be49261e196337d1444f0f02" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/7857a524c7e23c59be49261e196337d1444f0f02", + "reference": "7857a524c7e23c59be49261e196337d1444f0f02", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony CssSelector Component", + "homepage": "https://symfony.com", + "time": "2017-08-03T09:34:20+00:00" + }, + { + "name": "symfony/debug", + "version": "3.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "33f6f36c28f4f280e5c999e556473bc98d8b173f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/33f6f36c28f4f280e5c999e556473bc98d8b173f", + "reference": "33f6f36c28f4f280e5c999e556473bc98d8b173f", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "psr/log": "~1.0" + }, + "conflict": { + "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" + }, + "require-dev": { + "symfony/http-kernel": "~2.8|~3.0|~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Debug Component", + "homepage": "https://symfony.com", + "time": "2017-09-03T14:49:34+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "96440b5f3392aa7415ed720a290ceb7dcae3b00c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/96440b5f3392aa7415ed720a290ceb7dcae3b00c", + "reference": "96440b5f3392aa7415ed720a290ceb7dcae3b00c", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "conflict": { + "symfony/dependency-injection": "<3.4" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.4|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/expression-language": "~3.4|~4.0", + "symfony/stopwatch": "~3.4|~4.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony EventDispatcher Component", + "homepage": "https://symfony.com", + "time": "2017-06-12T18:12:26+00:00" + }, + { + "name": "symfony/finder", + "version": "3.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "bf0450cfe7282c5f06539c4733ba64273e91e918" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/bf0450cfe7282c5f06539c4733ba64273e91e918", + "reference": "bf0450cfe7282c5f06539c4733ba64273e91e918", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Finder Component", + "homepage": "https://symfony.com", + "time": "2017-08-03T09:34:20+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "3.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "d5f928e4304321a846d259adaea6224cf7e86014" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/d5f928e4304321a846d259adaea6224cf7e86014", + "reference": "d5f928e4304321a846d259adaea6224cf7e86014", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/polyfill-mbstring": "~1.1" + }, + "require-dev": { + "symfony/expression-language": "~2.8|~3.0|~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony HttpFoundation Component", + "homepage": "https://symfony.com", + "time": "2017-09-06T19:03:12+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "3.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "4c7f1f5fabc50670eaeaa4850e598435207e0708" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/4c7f1f5fabc50670eaeaa4850e598435207e0708", + "reference": "4c7f1f5fabc50670eaeaa4850e598435207e0708", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "psr/log": "~1.0", + "symfony/debug": "~2.8|~3.0|~4.0", + "symfony/event-dispatcher": "~2.8|~3.0|~4.0", + "symfony/http-foundation": "~3.3|~4.0" + }, + "conflict": { + "symfony/config": "<2.8", + "symfony/dependency-injection": "<3.4", + "symfony/var-dumper": "<3.3", + "twig/twig": "<1.34|<2.4,>=2" + }, + "require-dev": { + "psr/cache": "~1.0", + "symfony/browser-kit": "~2.8|~3.0|~4.0", + "symfony/class-loader": "~2.8|~3.0", + "symfony/config": "~2.8|~3.0|~4.0", + "symfony/console": "~2.8|~3.0|~4.0", + "symfony/css-selector": "~2.8|~3.0|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/dom-crawler": "~2.8|~3.0|~4.0", + "symfony/expression-language": "~2.8|~3.0|~4.0", + "symfony/finder": "~2.8|~3.0|~4.0", + "symfony/process": "~2.8|~3.0|~4.0", + "symfony/routing": "~2.8|~3.0|~4.0", + "symfony/stopwatch": "~2.8|~3.0|~4.0", + "symfony/templating": "~2.8|~3.0|~4.0", + "symfony/translation": "~2.8|~3.0|~4.0", + "symfony/var-dumper": "~3.3|~4.0" + }, + "suggest": { + "symfony/browser-kit": "", + "symfony/config": "", + "symfony/console": "", + "symfony/dependency-injection": "", + "symfony/finder": "", + "symfony/var-dumper": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony HttpKernel Component", + "homepage": "https://symfony.com", + "time": "2017-09-03T14:48:09+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "7c8fae0ac1d216eb54349e6a8baa57d515fe8803" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7c8fae0ac1d216eb54349e6a8baa57d515fe8803", + "reference": "7c8fae0ac1d216eb54349e6a8baa57d515fe8803", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2017-06-14T15:44:48+00:00" + }, + { + "name": "symfony/process", + "version": "3.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "9794f948d9af3be0157185051275d78b24d68b92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/9794f948d9af3be0157185051275d78b24d68b92", + "reference": "9794f948d9af3be0157185051275d78b24d68b92", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "time": "2017-08-03T09:34:20+00:00" + }, + { + "name": "symfony/psr-http-message-bridge", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "b2098405d8644f6dc4c36febcee6a77c0fdecdff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/b2098405d8644f6dc4c36febcee6a77c0fdecdff", + "reference": "b2098405d8644f6dc4c36febcee6a77c0fdecdff", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "psr/http-message": "~1.0", + "symfony/http-foundation": "~2.3|~3.0|~4.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "~3.2|4.0" + }, + "suggest": { + "psr/http-message-implementation": "To use the HttpFoundation factory", + "zendframework/zend-diactoros": "To use the Zend Diactoros factory" + }, + "type": "symfony-bridge", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bridge\\PsrHttpMessage\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "PSR HTTP message bridge", + "homepage": "http://symfony.com", + "keywords": [ + "http", + "http-message", + "psr-7" + ], + "time": "2017-07-23T09:13:43+00:00" + }, + { + "name": "symfony/routing", + "version": "3.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "a7dbe78a94c75b99475aebb0c0d77bfaf434ee4c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/a7dbe78a94c75b99475aebb0c0d77bfaf434ee4c", + "reference": "a7dbe78a94c75b99475aebb0c0d77bfaf434ee4c", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "conflict": { + "symfony/config": "<2.8", + "symfony/dependency-injection": "<3.3", + "symfony/yaml": "<3.3" + }, + "require-dev": { + "doctrine/annotations": "~1.0", + "doctrine/common": "~2.2", + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0|~4.0", + "symfony/dependency-injection": "~3.3|~4.0", + "symfony/expression-language": "~2.8|~3.0|~4.0", + "symfony/http-foundation": "~2.8|~3.0|~4.0", + "symfony/yaml": "~3.3|~4.0" + }, + "suggest": { + "doctrine/annotations": "For using the annotation loader", + "symfony/config": "For using the all-in-one router or any loader", + "symfony/dependency-injection": "For loading routes from a service", + "symfony/expression-language": "For using expression matching", + "symfony/http-foundation": "For using a Symfony Request object", + "symfony/yaml": "For using the YAML loader" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Routing Component", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "time": "2017-08-29T22:38:20+00:00" + }, + { + "name": "symfony/translation", + "version": "3.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "f9448c88e4fee566626d4bea08948535b714ffd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/f9448c88e4fee566626d4bea08948535b714ffd4", + "reference": "f9448c88e4fee566626d4bea08948535b714ffd4", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/config": "<2.8", + "symfony/dependency-injection": "<3.4", + "symfony/yaml": "<3.3" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/finder": "~2.8|~3.0|~4.0", + "symfony/intl": "^2.8.18|^3.2.5|~4.0", + "symfony/yaml": "~3.3|~4.0" + }, + "suggest": { + "psr/log": "To use logging capability in translator", + "symfony/config": "", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Translation Component", + "homepage": "https://symfony.com", + "time": "2017-08-29T14:37:52+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "3.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "6445048e36614b075c0ac30eb1898299b0ab7359" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/6445048e36614b075c0ac30eb1898299b0ab7359", + "reference": "6445048e36614b075c0ac30eb1898299b0ab7359", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0" + }, + "require-dev": { + "ext-iconv": "*", + "twig/twig": "~1.34|~2.4" + }, + "suggest": { + "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", + "ext-intl": "To show region name in time zone dump", + "ext-symfony_debug": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony mechanism for exploring and dumping PHP variables", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "time": "2017-09-04T18:03:48+00:00" + }, + { + "name": "symfony/yaml", + "version": "3.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "c076a2d6f809130a84f99616b425b9665b3ce0a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/c076a2d6f809130a84f99616b425b9665b3ce0a5", + "reference": "c076a2d6f809130a84f99616b425b9665b3ce0a5", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "conflict": { + "symfony/console": "<3.4" + }, + "require-dev": { + "symfony/console": "~3.4|~4.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2017-09-04T13:18:59+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "ab03919dfd85a74ae0372f8baf9f3c7d5c03b04b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/ab03919dfd85a74ae0372f8baf9f3c7d5c03b04b", + "reference": "ab03919dfd85a74ae0372f8baf9f3c7d5c03b04b", + "shasum": "" + }, + "require": { + "php": "^5.5 || ^7", + "symfony/css-selector": "^2.7|~3.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8|5.1.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "time": "2016-09-20T12:50:39+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "3dd3d8f90e6604e75cca48ec32b033b50b05135b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/3dd3d8f90e6604e75cca48ec32b033b50b05135b", + "reference": "3dd3d8f90e6604e75cca48ec32b033b50b05135b", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "phpunit/phpunit": "^4.8 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause-Attribution" + ], + "authors": [ + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "http://www.vancelucas.com" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "time": "2017-08-08T20:02:34+00:00" + }, + { + "name": "zendframework/zend-diactoros", + "version": "dev-develop", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-diactoros.git", + "reference": "5f0c491736afbe07a4599eb778fbb6d9729a10d6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/5f0c491736afbe07a4599eb778fbb6d9729a10d6", + "reference": "5f0c491736afbe07a4599eb778fbb6d9729a10d6", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0", + "psr/http-message": "^1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-dom": "*", + "ext-libxml": "*", + "phpunit/phpunit": "^5.7.16 || ^6.0.8", + "zendframework/zend-coding-standard": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev", + "dev-develop": "1.6-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\Diactoros\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "description": "PSR HTTP Message implementations", + "homepage": "https://github.com/zendframework/zend-diactoros", + "keywords": [ + "http", + "psr", + "psr-7" + ], + "time": "2017-08-22T20:40:13+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "^6.2.3", + "squizlabs/php_codesniffer": "^3.0.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2017-07-22T11:58:36+00:00" + }, + { + "name": "fzaninotto/faker", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/fzaninotto/Faker.git", + "reference": "d35b1165f34f09ffefef77a3344e84705cde4077" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/d35b1165f34f09ffefef77a3344e84705cde4077", + "reference": "d35b1165f34f09ffefef77a3344e84705cde4077", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "ext-intl": "*", + "phpunit/phpunit": "^4.0 || ^5.0", + "squizlabs/php_codesniffer": "^1.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "time": "2017-09-06T07:11:10+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "1.2.x-dev", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "b72949ccf2f640e7de66ff7dd92d83f577ce782e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/b72949ccf2f640e7de66ff7dd92d83f577ce782e", + "reference": "b72949ccf2f640e7de66ff7dd92d83f577ce782e", + "shasum": "" + }, + "require": { + "php": "^5.3|^7.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "1.3.3", + "phpunit/phpunit": "~4.0", + "satooshi/php-coveralls": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ], + "files": [ + "hamcrest/Hamcrest.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "time": "2016-01-19T12:08:55+00:00" + }, + { + "name": "mockery/mockery", + "version": "0.9.x-dev", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "80ac8060487db317c133ee00154f0862bd4d4ec7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/80ac8060487db317c133ee00154f0862bd4d4ec7", + "reference": "80ac8060487db317c133ee00154f0862bd4d4ec7", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "~1.1", + "lib-pcre": ">=7.0", + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.9.x-dev" + } + }, + "autoload": { + "psr-0": { + "Mockery": "library/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "http://blog.astrumfutura.com" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "http://davedevelopment.co.uk" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework for use in unit testing with PHPUnit, PHPSpec or any other testing framework. Its core goal is to offer a test double framework with a succinct API capable of clearly defining all possible object operations and interactions using a human readable Domain Specific Language (DSL). Designed as a drop in alternative to PHPUnit's phpunit-mock-objects library, Mockery is easy to integrate with PHPUnit and can operate alongside phpunit-mock-objects without the World ending.", + "homepage": "http://github.com/padraic/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "time": "2017-06-07T08:41:50+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8e6e04167378abf1ddb4d3522d8755c5fd90d102", + "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "doctrine/collections": "1.*", + "phpunit/phpunit": "~4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "homepage": "https://github.com/myclabs/DeepCopy", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "time": "2017-04-12T18:52:22+00:00" + }, + { + "name": "phar-io/manifest", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "014feadb268809af7c8e2f7ccd396b8494901f58" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/014feadb268809af7c8e2f7ccd396b8494901f58", + "reference": "014feadb268809af7c8e2f7ccd396b8494901f58", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^1.0.1", + "php": "^5.6 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "time": "2017-04-07T07:07:10+00:00" + }, + { + "name": "phar-io/version", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df", + "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "time": "2017-03-05T17:38:23+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "a046af61c36e9162372f205de091a1cab7340f1c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/a046af61c36e9162372f205de091a1cab7340f1c", + "reference": "a046af61c36e9162372f205de091a1cab7340f1c", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2017-04-30T11:58:12+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "4.1.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "2d3d238c433cf69caeb4842e97a3223a116f94b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/2d3d238c433cf69caeb4842e97a3223a116f94b2", + "reference": "2d3d238c433cf69caeb4842e97a3223a116f94b2", + "shasum": "" + }, + "require": { + "php": "^7.0", + "phpdocumentor/reflection-common": "^1.0@dev", + "phpdocumentor/type-resolver": "^0.4.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2017-08-30T18:51:59+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "0.4.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", + "shasum": "" + }, + "require": { + "php": "^5.5 || ^7.0", + "phpdocumentor/reflection-common": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^5.2||^4.8.24" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "time": "2017-07-14T14:27:02+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6", + "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", + "sebastian/comparator": "^1.1|^2.0", + "sebastian/recursion-context": "^1.0|^2.0|^3.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.5|^3.2", + "phpunit/phpunit": "^4.8 || ^5.6.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2017-09-04T11:05:03+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "77a1ba8076365f943e2a3d75573b6c9822840ac6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/77a1ba8076365f943e2a3d75573b6c9822840ac6", + "reference": "77a1ba8076365f943e2a3d75573b6c9822840ac6", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^7.0", + "phpunit/php-file-iterator": "^1.4.2", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^2.0", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^3.0", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1" + }, + "require-dev": { + "ext-xdebug": "^2.5", + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-xdebug": "^2.5.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2017-08-25T06:32:04+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2016-10-03T07:40:28+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21T13:50:34+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "d107f347d368dd8a384601398280c7c608390ab7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/d107f347d368dd8a384601398280c7c608390ab7", + "reference": "d107f347d368dd8a384601398280c7c608390ab7", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2017-03-07T15:42:04+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "9a02332089ac48e704c70f6cefed30c224e3c0b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/9a02332089ac48e704c70f6cefed30c224e3c0b0", + "reference": "9a02332089ac48e704c70f6cefed30c224e3c0b0", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.2.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2017-08-20T05:47:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "e6e7085fbbd2e25f4ca128ac30c1b0d3dd4ef827" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e6e7085fbbd2e25f4ca128ac30c1b0d3dd4ef827", + "reference": "e6e7085fbbd2e25f4ca128ac30c1b0d3dd4ef827", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "^1.6.1", + "phar-io/manifest": "^1.0.1", + "phar-io/version": "^1.0", + "php": "^7.0", + "phpspec/prophecy": "^1.7", + "phpunit/php-code-coverage": "^5.2.2", + "phpunit/php-file-iterator": "^1.4.2", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^1.0.9", + "phpunit/phpunit-mock-objects": "^4.0.3", + "sebastian/comparator": "^2.0.2", + "sebastian/diff": "^2.0", + "sebastian/environment": "^3.1", + "sebastian/exporter": "^3.1", + "sebastian/global-state": "^2.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^1.0", + "sebastian/version": "^2.0.1" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "3.0.2", + "phpunit/dbunit": "<3.0" + }, + "require-dev": { + "ext-pdo": "*" + }, + "suggest": { + "ext-xdebug": "*", + "phpunit/php-invoker": "^1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2017-09-01T08:39:38+00:00" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "2f789b59ab89669015ad984afa350c4ec577ade0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/2f789b59ab89669015ad984afa350c4ec577ade0", + "reference": "2f789b59ab89669015ad984afa350c4ec577ade0", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.5", + "php": "^7.0", + "phpunit/php-text-template": "^1.2.1", + "sebastian/exporter": "^3.0" + }, + "conflict": { + "phpunit/phpunit": "<6.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2017-08-03T14:08:16+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "3488be0a7b346cd6e5361510ed07e88f9bea2e88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/3488be0a7b346cd6e5361510ed07e88f9bea2e88", + "reference": "3488be0a7b346cd6e5361510ed07e88f9bea2e88", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2017-03-04T10:23:55+00:00" + }, + { + "name": "sebastian/comparator", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fb3213355da37bf91569ca7a944af19bc57b80e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fb3213355da37bf91569ca7a944af19bc57b80e9", + "reference": "fb3213355da37bf91569ca7a944af19bc57b80e9", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/diff": "^2.0", + "sebastian/exporter": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2017-08-20T14:03:32+00:00" + }, + { + "name": "sebastian/diff", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", + "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2017-08-03 08:09:46" + }, + { + "name": "sebastian/environment", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5", + "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2017-07-01T08:51:00+00:00" + }, + { + "name": "sebastian/exporter", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "234199f4528de6d12aaa58b612e98f7d36adb937" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937", + "reference": "234199f4528de6d12aaa58b612e98f7d36adb937", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2017-04-03T13:19:02+00:00" + }, + { + "name": "sebastian/global-state", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2017-04-27T15:39:26+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "time": "2017-08-03T12:35:26+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "773f97c67f28de00d397be301821b06708fca0be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", + "reference": "773f97c67f28de00d397be301821b06708fca0be", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "time": "2017-03-29T09:07:27+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "a0e54bc9bf04e2c5b302236984cebc277631f0f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/a0e54bc9bf04e2c5b302236984cebc277631f0f1", + "reference": "a0e54bc9bf04e2c5b302236984cebc277631f0f1", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2017-03-07T15:09:59+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "fadc83f7c41fb2924e542635fea47ae546816ece" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/fadc83f7c41fb2924e542635fea47ae546816ece", + "reference": "fadc83f7c41fb2924e542635fea47ae546816ece", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2016-10-03T07:43:09+00:00" + }, + { + "name": "sebastian/version", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2016-10-03T07:35:21+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b", + "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "time": "2017-04-07T12:08:54+00:00" + }, + { + "name": "webmozart/assert", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "4a8bf11547e139e77b651365113fc12850c43d9a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/4a8bf11547e139e77b651365113fc12850c43d9a", + "reference": "4a8bf11547e139e77b651365113fc12850c43d9a", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2016-11-23T20:04:41+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=7.0.0" + }, + "platform-dev": [] +} diff --git a/config/app.php b/config/app.php new file mode 100644 index 00000000..519fdecd --- /dev/null +++ b/config/app.php @@ -0,0 +1,235 @@ + 'Laravel', + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services your application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | your application so that it is used when running Artisan tasks. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. We have gone + | ahead and set this to a sensible default for you out of the box. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by the translation service provider. You are free to set this value + | to any of the locales which will be supported by the application. + | + */ + + 'locale' => 'en', + + /* + |-------------------------------------------------------------------------- + | Application Fallback Locale + |-------------------------------------------------------------------------- + | + | The fallback locale determines the locale to use when the current one + | is not available. You may change the value to correspond to any of + | the language folders that are provided through your application. + | + */ + + 'fallback_locale' => 'en', + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is used by the Illuminate encrypter service and should be set + | to a random, 32 character string, otherwise these encrypted strings + | will not be safe. Please do this before deploying an application! + | + */ + + 'key' => env('APP_KEY'), + + 'cipher' => 'AES-256-CBC', + + /* + |-------------------------------------------------------------------------- + | Logging Configuration + |-------------------------------------------------------------------------- + | + | Here you may configure the log settings for your application. Out of + | the box, Laravel uses the Monolog PHP logging library. This gives + | you a variety of powerful log handlers / formatters to utilize. + | + | Available Settings: "single", "daily", "syslog", "errorlog" + | + */ + + 'log' => env('APP_LOG', 'single'), + + 'log_level' => env('APP_LOG_LEVEL', 'debug'), + + /* + |-------------------------------------------------------------------------- + | Autoloaded Service Providers + |-------------------------------------------------------------------------- + | + | The service providers listed here will be automatically loaded on the + | request to your application. Feel free to add your own services to + | this array to grant expanded functionality to your applications. + | + */ + + 'providers' => [ + + /* + * Laravel Framework Service Providers... + */ + Illuminate\Auth\AuthServiceProvider::class, + Illuminate\Broadcasting\BroadcastServiceProvider::class, + Illuminate\Bus\BusServiceProvider::class, + Illuminate\Cache\CacheServiceProvider::class, + Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class, + Illuminate\Cookie\CookieServiceProvider::class, + Illuminate\Database\DatabaseServiceProvider::class, + Illuminate\Encryption\EncryptionServiceProvider::class, + Illuminate\Filesystem\FilesystemServiceProvider::class, + Illuminate\Foundation\Providers\FoundationServiceProvider::class, + Illuminate\Hashing\HashServiceProvider::class, + Illuminate\Mail\MailServiceProvider::class, + Illuminate\Notifications\NotificationServiceProvider::class, + Illuminate\Pagination\PaginationServiceProvider::class, + Illuminate\Pipeline\PipelineServiceProvider::class, + Illuminate\Queue\QueueServiceProvider::class, + Illuminate\Redis\RedisServiceProvider::class, + Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, + Illuminate\Session\SessionServiceProvider::class, + Illuminate\Translation\TranslationServiceProvider::class, + Illuminate\Validation\ValidationServiceProvider::class, + Illuminate\View\ViewServiceProvider::class, + + /* + * Package Service Providers... + */ + + Laravel\Passport\PassportServiceProvider::class, + Laravel\Tinker\TinkerServiceProvider::class, + + /* + * Application Service Providers... + */ + + App\Providers\AppServiceProvider::class, + App\Providers\AuthServiceProvider::class, + App\Providers\BroadcastServiceProvider::class, + App\Providers\EventServiceProvider::class, + App\Providers\RouteServiceProvider::class, + + + ], + + /* + |-------------------------------------------------------------------------- + | Class Aliases + |-------------------------------------------------------------------------- + | + | This array of class aliases will be registered when this application + | is started. However, feel free to register as many as you wish as + | the aliases are "lazy" loaded so they don't hinder performance. + | + */ + + 'aliases' => [ + + 'App' => Illuminate\Support\Facades\App::class, + 'Artisan' => Illuminate\Support\Facades\Artisan::class, + 'Auth' => Illuminate\Support\Facades\Auth::class, + 'Blade' => Illuminate\Support\Facades\Blade::class, + 'Broadcast' => Illuminate\Support\Facades\Broadcast::class, + 'Bus' => Illuminate\Support\Facades\Bus::class, + 'Cache' => Illuminate\Support\Facades\Cache::class, + 'Config' => Illuminate\Support\Facades\Config::class, + 'Cookie' => Illuminate\Support\Facades\Cookie::class, + 'Crypt' => Illuminate\Support\Facades\Crypt::class, + 'DB' => Illuminate\Support\Facades\DB::class, + 'Eloquent' => Illuminate\Database\Eloquent\Model::class, + 'Event' => Illuminate\Support\Facades\Event::class, + 'File' => Illuminate\Support\Facades\File::class, + 'Gate' => Illuminate\Support\Facades\Gate::class, + 'Hash' => Illuminate\Support\Facades\Hash::class, + 'Lang' => Illuminate\Support\Facades\Lang::class, + 'Log' => Illuminate\Support\Facades\Log::class, + 'Mail' => Illuminate\Support\Facades\Mail::class, + 'Notification' => Illuminate\Support\Facades\Notification::class, + 'Password' => Illuminate\Support\Facades\Password::class, + 'Queue' => Illuminate\Support\Facades\Queue::class, + 'Redirect' => Illuminate\Support\Facades\Redirect::class, + 'Redis' => Illuminate\Support\Facades\Redis::class, + 'Request' => Illuminate\Support\Facades\Request::class, + 'Response' => Illuminate\Support\Facades\Response::class, + 'Route' => Illuminate\Support\Facades\Route::class, + 'Schema' => Illuminate\Support\Facades\Schema::class, + 'Session' => Illuminate\Support\Facades\Session::class, + 'Storage' => Illuminate\Support\Facades\Storage::class, + 'URL' => Illuminate\Support\Facades\URL::class, + 'Validator' => Illuminate\Support\Facades\Validator::class, + 'View' => Illuminate\Support\Facades\View::class, + + ], + +]; diff --git a/config/auth.php b/config/auth.php new file mode 100644 index 00000000..f8a1194b --- /dev/null +++ b/config/auth.php @@ -0,0 +1,102 @@ + [ + 'guard' => 'web', + 'passwords' => 'users', + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | here which uses session storage and the Eloquent user provider. + | + | All authentication drivers have a user provider. This defines how the + | users are actually retrieved out of your database or other storage + | mechanisms used by this application to persist your user's data. + | + | Supported: "session", "token" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + + 'api' => [ + 'driver' => 'passport', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication drivers have a user provider. This defines how the + | users are actually retrieved out of your database or other storage + | mechanisms used by this application to persist your user's data. + | + | If you have multiple user tables or models you may configure multiple + | sources which represent each model / table. These sources may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => App\User::class, + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | You may specify multiple password reset configurations if you have more + | than one user table or model in the application and you want to have + | separate password reset settings based on the specific user types. + | + | The expire time is the number of minutes that the reset token should be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => 'password_resets', + 'expire' => 60, + ], + ], + +]; diff --git a/config/broadcasting.php b/config/broadcasting.php new file mode 100644 index 00000000..13682e3c --- /dev/null +++ b/config/broadcasting.php @@ -0,0 +1,58 @@ + env('BROADCAST_DRIVER', 'null'), + + /* + |-------------------------------------------------------------------------- + | Broadcast Connections + |-------------------------------------------------------------------------- + | + | Here you may define all of the broadcast connections that will be used + | to broadcast events to other systems or over websockets. Samples of + | each available type of connection are provided inside this array. + | + */ + + 'connections' => [ + + 'pusher' => [ + 'driver' => 'pusher', + 'key' => env('PUSHER_APP_KEY'), + 'secret' => env('PUSHER_APP_SECRET'), + 'app_id' => env('PUSHER_APP_ID'), + 'options' => [ + 'cluster' => 'us2', + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'default', + ], + + 'log' => [ + 'driver' => 'log', + ], + + 'null' => [ + 'driver' => 'null', + ], + + ], + +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 00000000..e87f0320 --- /dev/null +++ b/config/cache.php @@ -0,0 +1,91 @@ + env('CACHE_DRIVER', 'file'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + */ + + 'stores' => [ + + 'apc' => [ + 'driver' => 'apc', + ], + + 'array' => [ + 'driver' => 'array', + ], + + 'database' => [ + 'driver' => 'database', + 'table' => 'cache', + 'connection' => null, + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'default', + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing a RAM based store such as APC or Memcached, there might + | be other applications utilizing the same cache. So, we'll specify a + | value to get prefixed to all our keys so we can avoid collisions. + | + */ + + 'prefix' => 'laravel', + +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 00000000..a196943f --- /dev/null +++ b/config/database.php @@ -0,0 +1,109 @@ + env('DB_CONNECTION', 'mysql'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Here are each of the database connections setup for your application. + | Of course, examples of configuring each database platform that is + | supported by Laravel is shown below to make development simple. + | + | + | All database work in Laravel is done through the PHP PDO facilities + | so make sure you have the driver for your particular database of + | choice installed on your machine before you begin development. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'strict' => true, + 'engine' => null, + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8', + 'prefix' => '', + 'schema' => 'public', + 'sslmode' => 'prefer', + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run in the database. + | + */ + + 'migrations' => 'migrations', + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer set of commands than a typical key-value systems + | such as APC or Memcached. Laravel makes it easy to dig right in. + | + */ + + 'redis' => [ + + 'client' => 'predis', + + 'default' => [ + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'password' => env('REDIS_PASSWORD', null), + 'port' => env('REDIS_PORT', 6379), + 'database' => 0, + ], + + ], + +]; diff --git a/config/filesystems.php b/config/filesystems.php new file mode 100644 index 00000000..f59cf9e9 --- /dev/null +++ b/config/filesystems.php @@ -0,0 +1,68 @@ + 'local', + + /* + |-------------------------------------------------------------------------- + | Default Cloud Filesystem Disk + |-------------------------------------------------------------------------- + | + | Many applications store files both locally and in the cloud. For this + | reason, you may specify a default "cloud" driver here. This driver + | will be bound as the Cloud disk implementation in the container. + | + */ + + 'cloud' => 's3', + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Here you may configure as many filesystem "disks" as you wish, and you + | may even configure multiple disks of the same driver. Defaults have + | been setup for each driver as an example of the required options. + | + | Supported Drivers: "local", "ftp", "s3", "rackspace" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app'), + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', + 'visibility' => 'public', + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_KEY'), + 'secret' => env('AWS_SECRET'), + 'region' => env('AWS_REGION'), + 'bucket' => env('AWS_BUCKET'), + ], + + ], + +]; diff --git a/config/mail.php b/config/mail.php new file mode 100644 index 00000000..bb92224c --- /dev/null +++ b/config/mail.php @@ -0,0 +1,123 @@ + env('MAIL_DRIVER', 'smtp'), + + /* + |-------------------------------------------------------------------------- + | SMTP Host Address + |-------------------------------------------------------------------------- + | + | Here you may provide the host address of the SMTP server used by your + | applications. A default option is provided that is compatible with + | the Mailgun mail service which will provide reliable deliveries. + | + */ + + 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), + + /* + |-------------------------------------------------------------------------- + | SMTP Host Port + |-------------------------------------------------------------------------- + | + | This is the SMTP port used by your application to deliver e-mails to + | users of the application. Like the host we have set this value to + | stay compatible with the Mailgun e-mail application by default. + | + */ + + 'port' => env('MAIL_PORT', 587), + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all e-mails sent by your application to be sent from + | the same address. Here, you may specify a name and address that is + | used globally for all e-mails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', 'Example'), + ], + + /* + |-------------------------------------------------------------------------- + | E-Mail Encryption Protocol + |-------------------------------------------------------------------------- + | + | Here you may specify the encryption protocol that should be used when + | the application send e-mail messages. A sensible default using the + | transport layer security protocol should provide great security. + | + */ + + 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + + /* + |-------------------------------------------------------------------------- + | SMTP Server Username + |-------------------------------------------------------------------------- + | + | If your SMTP server requires a username for authentication, you should + | set it here. This will get used to authenticate with your server on + | connection. You may also set the "password" value below this one. + | + */ + + 'username' => env('MAIL_USERNAME'), + + 'password' => env('MAIL_PASSWORD'), + + /* + |-------------------------------------------------------------------------- + | Sendmail System Path + |-------------------------------------------------------------------------- + | + | When using the "sendmail" driver to send e-mails, we will need to know + | the path to where Sendmail lives on this server. A default path has + | been provided here, which will work well on most of your systems. + | + */ + + 'sendmail' => '/usr/sbin/sendmail -bs', + + /* + |-------------------------------------------------------------------------- + | Markdown Mail Settings + |-------------------------------------------------------------------------- + | + | If you are using Markdown based email rendering, you may configure your + | theme and component paths here, allowing you to customize the design + | of the emails. Or, you may simply stick with the Laravel defaults! + | + */ + + 'markdown' => [ + 'theme' => 'default', + + 'paths' => [ + resource_path('views/vendor/mail'), + ], + ], + +]; diff --git a/config/queue.php b/config/queue.php new file mode 100644 index 00000000..4d83ebd0 --- /dev/null +++ b/config/queue.php @@ -0,0 +1,85 @@ + env('QUEUE_DRIVER', 'sync'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection information for each server that + | is used by your application. A default configuration has been added + | for each back-end shipped with Laravel. You are free to add more. + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'table' => 'jobs', + 'queue' => 'default', + 'retry_after' => 90, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => 'localhost', + 'queue' => 'default', + 'retry_after' => 90, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => 'your-public-key', + 'secret' => 'your-secret-key', + 'prefix' => 'https://sqs.us-east-1.amazonaws.com/your-account-id', + 'queue' => 'your-queue-name', + 'region' => 'us-east-1', + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'default', + 'queue' => 'default', + 'retry_after' => 90, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control which database and table are used to store the jobs that + | have failed. You may change them to any database / table you wish. + | + */ + + 'failed' => [ + 'database' => env('DB_CONNECTION', 'mysql'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/config/services.php b/config/services.php new file mode 100644 index 00000000..d0047a95 --- /dev/null +++ b/config/services.php @@ -0,0 +1,54 @@ + [ + 'secret' => env('CLOUDFLARE_SECRET'), + ], + + 'mailgun' => [ + 'domain' => env('MAILGUN_DOMAIN'), + 'secret' => env('MAILGUN_SECRET'), + ], + + 'route53' => [ + 'key' => env('ROUTE_53_KEY'), + 'secret' => env('ROUTE_53_SECRET'), + ], + + 'ses' => [ + 'key' => env('SES_KEY'), + 'secret' => env('SES_SECRET'), + 'region' => 'us-east-1', + ], + + 'sparkpost' => [ + 'secret' => env('SPARKPOST_SECRET'), + ], + + 'stripe' => [ + 'model' => App\User::class, + 'key' => env('STRIPE_KEY'), + 'secret' => env('STRIPE_SECRET'), + ], + + 'testing' => [ + 'digital_ocean' => env('DIGITAL_OCEAN_TEST_KEY'), + 'github' => env('GITHUB_TEST_KEY'), + 's3_key' => env('S3_KEY'), + 's3_secret' => env('S3_SECRET'), + ], + +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 00000000..e2779ad8 --- /dev/null +++ b/config/session.php @@ -0,0 +1,179 @@ + env('SESSION_DRIVER', 'file'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to immediately expire on the browser closing, set that option. + | + */ + + 'lifetime' => 120, + + 'expire_on_close' => false, + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it is stored. All encryption will be run + | automatically by Laravel and you can use the Session like normal. + | + */ + + 'encrypt' => false, + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When using the native session driver, we need a location where session + | files may be stored. A default has been set for you but a different + | location may be specified. This is only needed for file sessions. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => null, + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table we + | should use to manage the sessions. Of course, a sensible default is + | provided for you; however, you are free to change this as needed. + | + */ + + 'table' => 'sessions', + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using the "apc" or "memcached" session drivers, you may specify a + | cache store that should be used for these sessions. This value must + | correspond with one of the application's configured cache stores. + | + */ + + 'store' => null, + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the cookie used to identify a session + | instance by ID. The name specified here will get used every time a + | new session cookie is created by the framework for every driver. + | + */ + + 'cookie' => 'laravel_session', + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application but you are free to change this when necessary. + | + */ + + 'path' => '/', + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | Here you may change the domain of the cookie used to identify a session + | in your application. This will determine which domains the cookie is + | available to in your application. A sensible default has been set. + | + */ + + 'domain' => env('SESSION_DOMAIN', null), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you if it can not be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE', false), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. You are free to modify this option if needed. + | + */ + + 'http_only' => true, + +]; diff --git a/config/view.php b/config/view.php new file mode 100644 index 00000000..2acfd9cc --- /dev/null +++ b/config/view.php @@ -0,0 +1,33 @@ + [ + resource_path('views'), + ], + + /* + |-------------------------------------------------------------------------- + | Compiled View Path + |-------------------------------------------------------------------------- + | + | This option determines where all the compiled Blade templates will be + | stored for your application. Typically, this is within the storage + | directory. However, as usual, you are free to change this value. + | + */ + + 'compiled' => realpath(storage_path('framework/views')), + +]; diff --git a/database/.gitignore b/database/.gitignore new file mode 100644 index 00000000..9b1dffd9 --- /dev/null +++ b/database/.gitignore @@ -0,0 +1 @@ +*.sqlite diff --git a/database/factories/AppServerFactory.php b/database/factories/AppServerFactory.php new file mode 100644 index 00000000..54941910 --- /dev/null +++ b/database/factories/AppServerFactory.php @@ -0,0 +1,28 @@ +define(App\AppServer::class, function () { + return [ + 'project_id' => factory(App\Project::class), + 'stack_id' => factory(App\Stack::class), + 'name' => str_random(6), + 'size' => '2GB', + 'provider_server_id' => 1, + 'port' => 22, + 'sudo_password' => str_random(40), + 'database_username' => 'cloud', + 'database_password' => str_random(40), + 'meta' => [], + 'status' => 'provisioned', + ]; +}); diff --git a/database/factories/BalancerFactory.php b/database/factories/BalancerFactory.php new file mode 100644 index 00000000..84ce7290 --- /dev/null +++ b/database/factories/BalancerFactory.php @@ -0,0 +1,24 @@ +define(App\Balancer::class, function () { + return [ + 'project_id' => factory(App\Project::class), + 'name' => str_random(6), + 'size' => '2GB', + 'provider_server_id' => 1, + 'port' => 22, + 'sudo_password' => str_random(40), + 'status' => 'provisioned', + ]; +}); diff --git a/database/factories/CertificateFactory.php b/database/factories/CertificateFactory.php new file mode 100644 index 00000000..dd49b57f --- /dev/null +++ b/database/factories/CertificateFactory.php @@ -0,0 +1,21 @@ +define(App\Certificate::class, function () { + return [ + 'project_id' => factory(App\Project::class), + 'name' => 'Test Certificate', + 'private_key' => 'private-key', + 'certificate' => 'certificate', + ]; +}); diff --git a/database/factories/DatabaseBackupFactory.php b/database/factories/DatabaseBackupFactory.php new file mode 100644 index 00000000..112d1412 --- /dev/null +++ b/database/factories/DatabaseBackupFactory.php @@ -0,0 +1,23 @@ +define(App\DatabaseBackup::class, function () { + return [ + 'database_id' => factory(App\Database::class), + 'storage_provider_id' => factory(App\StorageProvider::class), + 'database_name' => 'Test Database', + 'backup_path' => 'laravel-cloud-test/backups/laravel/2017-01-01-12-01-01.sql.gz', + 'status' => 'running', + 'output' => '', + ]; +}); diff --git a/database/factories/DatabaseFactory.php b/database/factories/DatabaseFactory.php new file mode 100644 index 00000000..4d3ae0c8 --- /dev/null +++ b/database/factories/DatabaseFactory.php @@ -0,0 +1,27 @@ +define(App\Database::class, function () { + return [ + 'project_id' => factory(App\Project::class), + 'name' => str_random(6), + 'size' => '2gb', + 'provider_server_id' => 1, + 'port' => 22, + 'sudo_password' => str_random(40), + 'username' => str_random(40), + 'password' => str_random(40), + 'allows_access_from' => [], + 'status' => 'provisioned', + ]; +}); diff --git a/database/factories/DatabaseRestoreFactory.php b/database/factories/DatabaseRestoreFactory.php new file mode 100644 index 00000000..0519c1ad --- /dev/null +++ b/database/factories/DatabaseRestoreFactory.php @@ -0,0 +1,22 @@ +define(App\DatabaseRestore::class, function () { + return [ + 'database_id' => factory(App\Database::class), + 'database_backup_id' => factory(App\DatabaseBackup::class), + 'database_name' => 'Test Database', + 'status' => 'running', + 'output' => '', + ]; +}); diff --git a/database/factories/DeploymentFactory.php b/database/factories/DeploymentFactory.php new file mode 100644 index 00000000..645fc163 --- /dev/null +++ b/database/factories/DeploymentFactory.php @@ -0,0 +1,26 @@ +define(App\Deployment::class, function () { + return [ + 'stack_id' => factory(App\Stack::class), + 'branch' => 'master', + 'commit_hash' => str_random(20), + 'build_commands' => ['first'], + 'activation_commands' => ['second'], + 'directories' => ['storage'], + 'daemons' => [], + 'schedule' => [], + 'status' => 'pending', + ]; +}); diff --git a/database/factories/EnvironmentFactory.php b/database/factories/EnvironmentFactory.php new file mode 100644 index 00000000..b4cd3103 --- /dev/null +++ b/database/factories/EnvironmentFactory.php @@ -0,0 +1,22 @@ +define(App\Environment::class, function () { + return [ + 'project_id' => factory(App\Project::class), + 'creator_id' => factory(App\User::class), + 'name' => 'production', + 'encryption_key' => '', + 'variables' => '', + ]; +}); diff --git a/database/factories/HookFactory.php b/database/factories/HookFactory.php new file mode 100644 index 00000000..dbde9eb6 --- /dev/null +++ b/database/factories/HookFactory.php @@ -0,0 +1,23 @@ +define(App\Hook::class, function () { + return [ + 'stack_id' => factory(App\Stack::class), + 'name' => 'Test Hook', + 'branch' => 'master', + 'token' => str_random(40), + 'meta' => [], + 'published' => false, + ]; +}); diff --git a/database/factories/IpAddressFactory.php b/database/factories/IpAddressFactory.php new file mode 100644 index 00000000..dd3f31e3 --- /dev/null +++ b/database/factories/IpAddressFactory.php @@ -0,0 +1,21 @@ +define(App\IpAddress::class, function () { + return [ + 'addressable_id' => factory(App\Database::class), + 'addressable_type' => App\Database::class, + 'public_address' => '127.0.0.1', + 'private_address' => '127.0.0.2', + ]; +}); diff --git a/database/factories/ProjectFactory.php b/database/factories/ProjectFactory.php new file mode 100644 index 00000000..b890bfda --- /dev/null +++ b/database/factories/ProjectFactory.php @@ -0,0 +1,23 @@ +define(App\Project::class, function () { + return [ + 'user_id' => factory(App\User::class), + 'server_provider_id' => factory(App\ServerProvider::class), + 'source_provider_id' => factory(App\SourceProvider::class), + 'repository' => 'taylorotwell/hello-world', + 'name' => 'Laravel', + 'region' => 'nyc3', + ]; +}); diff --git a/database/factories/ServerDeploymentFactory.php b/database/factories/ServerDeploymentFactory.php new file mode 100644 index 00000000..76c9034e --- /dev/null +++ b/database/factories/ServerDeploymentFactory.php @@ -0,0 +1,23 @@ +define(App\ServerDeployment::class, function () { + return [ + 'deployment_id' => factory(App\Deployment::class), + 'deployable_id' => factory(App\AppServer::class), + 'deployable_type' => App\AppServer::class, + 'build_commands' => [], + 'activation_commands' => [], + 'status' => 'running', + ]; +}); diff --git a/database/factories/ServerProviderFactory.php b/database/factories/ServerProviderFactory.php new file mode 100644 index 00000000..98312504 --- /dev/null +++ b/database/factories/ServerProviderFactory.php @@ -0,0 +1,23 @@ +define(App\ServerProvider::class, function () { + return [ + 'user_id' => function () { + return factory(App\User::class)->create(); + }, + 'name' => 'DigitalOcean', + 'type' => 'DigitalOcean', + 'meta' => ['token' => config('services.testing.digital_ocean')], + ]; +}); diff --git a/database/factories/ServerTaskFactory.php b/database/factories/ServerTaskFactory.php new file mode 100644 index 00000000..7a615eff --- /dev/null +++ b/database/factories/ServerTaskFactory.php @@ -0,0 +1,24 @@ +define(App\ServerTask::class, function () { + return [ + 'stack_task_id' => factory(App\StackTask::class), + 'taskable_id' => factory(App\WebServer::class), + 'taskable_type' => 'App\WebServer', + 'task_id' => factory(App\Task::class), + 'commands' => [ + 'exit 1', + ], + ]; +}); diff --git a/database/factories/SourceProviderFactory.php b/database/factories/SourceProviderFactory.php new file mode 100644 index 00000000..5aee0297 --- /dev/null +++ b/database/factories/SourceProviderFactory.php @@ -0,0 +1,22 @@ +define(App\SourceProvider::class, function () { + return [ + 'user_id' => factory(App\User::class), + 'name' => 'GitHub', + 'type' => 'GitHub', + 'meta' => ['token' => config('services.testing.github')], + ]; +}); diff --git a/database/factories/StackFactory.php b/database/factories/StackFactory.php new file mode 100644 index 00000000..2d718138 --- /dev/null +++ b/database/factories/StackFactory.php @@ -0,0 +1,30 @@ +define(App\Stack::class, function () { + return [ + 'environment_id' => factory(App\Environment::class), + 'creator_id' => factory(App\User::class), + 'name' => 'test-stack', + 'url' => App\Haiku::withToken(), + 'balanced' => false, + 'status' => 'pending', + 'pending_deployment' => [], + 'meta' => [ + 'php' => '7.1', + 'initial_branch' => 'master', + 'initial_build_commands' => [], + 'initial_activation_commands' => [], + ], + ]; +}); diff --git a/database/factories/StackTaskFactory.php b/database/factories/StackTaskFactory.php new file mode 100644 index 00000000..c216cb12 --- /dev/null +++ b/database/factories/StackTaskFactory.php @@ -0,0 +1,23 @@ +define(App\StackTask::class, function () { + return [ + 'stack_id' => factory(App\Stack::class), + 'name' => 'Test Name', + 'user' => 'cloud', + 'commands' => [ + 'exit 1', + ], + ]; +}); diff --git a/database/factories/StorageProviderFactory.php b/database/factories/StorageProviderFactory.php new file mode 100644 index 00000000..250855f5 --- /dev/null +++ b/database/factories/StorageProviderFactory.php @@ -0,0 +1,27 @@ +define(App\StorageProvider::class, function () { + return [ + 'user_id' => factory(App\User::class), + 'name' => 'S3', + 'type' => 'S3', + 'meta' => [ + 'key' => config('services.testing.s3_key'), + 'secret' => config('services.testing.s3_secret'), + 'region' => 'us-east-1', + 'bucket' => 'laravel-cloud-test', + ], + ]; +}); diff --git a/database/factories/TaskFactory.php b/database/factories/TaskFactory.php new file mode 100644 index 00000000..e2dcac6e --- /dev/null +++ b/database/factories/TaskFactory.php @@ -0,0 +1,27 @@ +define(App\Task::class, function () { + return [ + 'project_id' => factory(App\Project::class), + 'provisionable_id' => factory(App\Database::class), + 'provisionable_type' => 'App\Database', + 'name' => 'Task Name', + 'user' => 'root', + 'status' => 'finished', + 'exit_code' => 0, + 'script' => '', + 'output' => '', + 'options' => [], + ]; +}); diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php new file mode 100644 index 00000000..4b100074 --- /dev/null +++ b/database/factories/UserFactory.php @@ -0,0 +1,27 @@ +define(App\User::class, function ($faker) { + static $key; + static $password; + static $workerKey; + + return [ + 'name' => $faker->name, + 'email' => $faker->unique()->safeEmail, + 'password' => $password ?: $password = bcrypt('secret'), + 'remember_token' => str_random(10), + 'keypair' => $key = $key ?: App\SecureShellKey::forNewUser(), + 'worker_keypair' => $workerKey = $workerKey ?: App\SecureShellKey::forNewUser(), + ]; +}); diff --git a/database/factories/WebServerFactory.php b/database/factories/WebServerFactory.php new file mode 100644 index 00000000..b1beeaaa --- /dev/null +++ b/database/factories/WebServerFactory.php @@ -0,0 +1,26 @@ +define(App\WebServer::class, function () { + return [ + 'project_id' => factory(App\Project::class), + 'stack_id' => factory(App\Stack::class), + 'name' => str_random(6), + 'size' => '2gb', + 'provider_server_id' => 1, + 'port' => 22, + 'sudo_password' => str_random(40), + 'meta' => [], + 'status' => 'provisioned', + ]; +}); diff --git a/database/factories/WorkerServerFactory.php b/database/factories/WorkerServerFactory.php new file mode 100644 index 00000000..01d9200f --- /dev/null +++ b/database/factories/WorkerServerFactory.php @@ -0,0 +1,27 @@ +define(App\WorkerServer::class, function () { + return [ + 'project_id' => factory(App\Project::class), + 'stack_id' => factory(App\Stack::class), + 'name' => str_random(6), + 'size' => '2gb', + 'provider_server_id' => 1, + 'port' => 22, + 'sudo_password' => str_random(40), + 'meta' => [], + 'status' => 'provisioned', + 'daemon_status' => 'pending', + ]; +}); diff --git a/database/migrations/2014_10_12_000000_create_users_table.php b/database/migrations/2014_10_12_000000_create_users_table.php new file mode 100644 index 00000000..1bc752bc --- /dev/null +++ b/database/migrations/2014_10_12_000000_create_users_table.php @@ -0,0 +1,31 @@ +increments('id'); + $table->string('name'); + $table->string('email')->unique(); + $table->string('password'); + $table->rememberToken(); + $table->text('public_key'); + $table->text('private_key'); + $table->text('public_worker_key'); + $table->text('private_worker_key'); + $table->string('provider_key_id')->nullable(); + $table->timestamp('last_alert_received_at')->nullable(); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2014_10_12_100000_create_password_resets_table.php b/database/migrations/2014_10_12_100000_create_password_resets_table.php new file mode 100644 index 00000000..fa3ae284 --- /dev/null +++ b/database/migrations/2014_10_12_100000_create_password_resets_table.php @@ -0,0 +1,22 @@ +string('email')->index(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + } +} diff --git a/database/migrations/2017_03_31_170538_create_projects_table.php b/database/migrations/2017_03_31_170538_create_projects_table.php new file mode 100644 index 00000000..4962b723 --- /dev/null +++ b/database/migrations/2017_03_31_170538_create_projects_table.php @@ -0,0 +1,28 @@ +increments('id'); + $table->unsignedInteger('user_id')->index(); + $table->string('name'); + $table->unsignedInteger('server_provider_id')->index(); + $table->string('region', 25); + $table->unsignedInteger('source_provider_id')->index(); + $table->string('repository'); + $table->tinyInteger('archived')->default(0); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2017_03_31_172929_create_environments_table.php b/database/migrations/2017_03_31_172929_create_environments_table.php new file mode 100644 index 00000000..503ef54b --- /dev/null +++ b/database/migrations/2017_03_31_172929_create_environments_table.php @@ -0,0 +1,26 @@ +increments('id'); + $table->unsignedInteger('project_id')->index(); + $table->unsignedInteger('creator_id'); + $table->string('name'); + $table->text('encryption_key'); + $table->text('variables'); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2017_04_03_152027_create_stacks_table.php b/database/migrations/2017_04_03_152027_create_stacks_table.php new file mode 100644 index 00000000..52eead9d --- /dev/null +++ b/database/migrations/2017_04_03_152027_create_stacks_table.php @@ -0,0 +1,37 @@ +bigIncrements('id'); + $table->unsignedInteger('environment_id')->index(); + $table->unsignedInteger('creator_id'); + $table->string('name'); + $table->string('url')->unique(); + $table->string('dns_record_id')->nullable(); + $table->string('dns_address')->nullable(); + $table->integer('initial_server_count')->nullable(); + $table->tinyInteger('balanced')->default(0); + $table->unsignedInteger('certificate_id')->nullable(); + $table->tinyInteger('promoted')->default(0); + $table->string('status', 25)->default('pending'); + $table->string('deployment_status', 25)->nullable(); + $table->timestamp('deployment_started_at')->nullable(); + $table->text('pending_deployment'); + $table->tinyInteger('under_maintenance')->default(0); + $table->longText('meta'); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2017_04_03_193949_create_server_providers_table.php b/database/migrations/2017_04_03_193949_create_server_providers_table.php new file mode 100644 index 00000000..cc5e5926 --- /dev/null +++ b/database/migrations/2017_04_03_193949_create_server_providers_table.php @@ -0,0 +1,25 @@ +increments('id'); + $table->unsignedInteger('user_id')->index(); + $table->string('name'); + $table->string('type', 25); + $table->text('meta'); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2017_04_07_195655_create_databases_table.php b/database/migrations/2017_04_07_195655_create_databases_table.php new file mode 100644 index 00000000..70f18933 --- /dev/null +++ b/database/migrations/2017_04_07_195655_create_databases_table.php @@ -0,0 +1,34 @@ +increments('id'); + $table->unsignedInteger('project_id')->index(); + $table->string('name'); + $table->string('size', 25); + $table->string('provider_server_id')->nullable(); + $table->integer('port')->default(22); + $table->string('sudo_password'); + $table->string('username'); + $table->string('password'); + $table->text('allows_access_from'); + $table->timestamp('provisioning_job_dispatched_at')->nullable(); + $table->string('status', 25); + $table->timestamps(); + + $table->unique(['project_id', 'name']); + }); + } +} diff --git a/database/migrations/2017_04_10_190135_create_balancers_table.php b/database/migrations/2017_04_10_190135_create_balancers_table.php new file mode 100644 index 00000000..4639ea0d --- /dev/null +++ b/database/migrations/2017_04_10_190135_create_balancers_table.php @@ -0,0 +1,32 @@ +increments('id'); + $table->unsignedInteger('project_id')->index(); + $table->string('name'); + $table->string('size', 25); + $table->string('provider_server_id')->nullable(); + $table->integer('port')->default(22); + $table->string('sudo_password'); + $table->timestamp('provisioning_job_dispatched_at')->nullable(); + $table->string('tls', 25)->nullable(); + $table->string('status', 25); + $table->timestamps(); + + $table->unique(['project_id', 'name']); + }); + } +} diff --git a/database/migrations/2017_04_18_203351_create_tasks_table.php b/database/migrations/2017_04_18_203351_create_tasks_table.php new file mode 100644 index 00000000..37c32a19 --- /dev/null +++ b/database/migrations/2017_04_18_203351_create_tasks_table.php @@ -0,0 +1,34 @@ +bigIncrements('id'); + $table->unsignedInteger('project_id')->index(); + $table->unsignedInteger('provisionable_id'); + $table->string('provisionable_type'); + $table->string('name'); + $table->string('user', 25); + $table->string('status', 25)->default('pending'); + $table->integer('exit_code')->nullable(); + $table->longText('script'); + $table->longText('output'); + $table->text('options'); + $table->timestamps(); + + $table->index(['provisionable_id', 'provisionable_type']); + $table->index('created_at'); + }); + } +} diff --git a/database/migrations/2017_04_26_163353_create_source_providers_table.php b/database/migrations/2017_04_26_163353_create_source_providers_table.php new file mode 100644 index 00000000..3f359cfa --- /dev/null +++ b/database/migrations/2017_04_26_163353_create_source_providers_table.php @@ -0,0 +1,25 @@ +increments('id'); + $table->unsignedInteger('user_id')->index(); + $table->string('name'); + $table->string('type', 25); + $table->text('meta'); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2017_04_27_193631_create_jobs_table.php b/database/migrations/2017_04_27_193631_create_jobs_table.php new file mode 100644 index 00000000..10ff6944 --- /dev/null +++ b/database/migrations/2017_04_27_193631_create_jobs_table.php @@ -0,0 +1,28 @@ +bigIncrements('id'); + $table->string('queue'); + $table->longText('payload'); + $table->tinyInteger('attempts')->unsigned(); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + + $table->index(['queue', 'reserved_at']); + }); + } +} diff --git a/database/migrations/2017_04_28_160656_create_project_users_table.php b/database/migrations/2017_04_28_160656_create_project_users_table.php new file mode 100644 index 00000000..3c992ebf --- /dev/null +++ b/database/migrations/2017_04_28_160656_create_project_users_table.php @@ -0,0 +1,24 @@ +unsignedInteger('project_id'); + $table->unsignedInteger('user_id'); + $table->text('permissions'); + + $table->unique(['project_id', 'user_id']); + }); + } +} diff --git a/database/migrations/2017_05_08_175021_create_ip_addresses_table.php b/database/migrations/2017_05_08_175021_create_ip_addresses_table.php new file mode 100644 index 00000000..a4459264 --- /dev/null +++ b/database/migrations/2017_05_08_175021_create_ip_addresses_table.php @@ -0,0 +1,28 @@ +increments('id'); + $table->unsignedInteger('addressable_id'); + $table->string('addressable_type'); + $table->string('public_address'); + $table->string('private_address'); + $table->timestamps(); + + $table->index(['addressable_id', 'addressable_type']); + $table->index('public_address'); + }); + } +} diff --git a/database/migrations/2017_05_10_150545_create_failed_jobs_table.php b/database/migrations/2017_05_10_150545_create_failed_jobs_table.php new file mode 100644 index 00000000..d432dff0 --- /dev/null +++ b/database/migrations/2017_05_10_150545_create_failed_jobs_table.php @@ -0,0 +1,35 @@ +bigIncrements('id'); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('failed_jobs'); + } +} diff --git a/database/migrations/2017_05_10_203142_create_alerts_table.php b/database/migrations/2017_05_10_203142_create_alerts_table.php new file mode 100644 index 00000000..89a73fc1 --- /dev/null +++ b/database/migrations/2017_05_10_203142_create_alerts_table.php @@ -0,0 +1,27 @@ +bigIncrements('id'); + $table->unsignedInteger('project_id')->index(); + $table->unsignedInteger('stack_id')->nullable()->index(); + $table->string('level', 15)->default('error'); + $table->string('type'); + $table->text('exception'); + $table->text('meta'); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2017_05_11_213227_create_app_servers_table.php b/database/migrations/2017_05_11_213227_create_app_servers_table.php new file mode 100644 index 00000000..e7fc0c93 --- /dev/null +++ b/database/migrations/2017_05_11_213227_create_app_servers_table.php @@ -0,0 +1,34 @@ +bigIncrements('id'); + $table->unsignedInteger('project_id')->index(); + $table->unsignedInteger('stack_id')->index(); + $table->string('name'); + $table->string('size', 25); + $table->string('provider_server_id')->nullable(); + $table->integer('port')->default(22); + $table->string('sudo_password'); + $table->string('database_username'); + $table->string('database_password'); + $table->text('meta'); + $table->string('status', 25)->default('pending'); + $table->string('daemon_status', 25)->default('pending'); + $table->timestamp('provisioning_job_dispatched_at')->nullable(); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2017_05_11_213349_create_web_servers_table.php b/database/migrations/2017_05_11_213349_create_web_servers_table.php new file mode 100644 index 00000000..d78e24ea --- /dev/null +++ b/database/migrations/2017_05_11_213349_create_web_servers_table.php @@ -0,0 +1,32 @@ +bigIncrements('id'); + $table->unsignedInteger('project_id')->index(); + $table->unsignedInteger('stack_id')->index(); + $table->string('name'); + $table->string('size', 25); + $table->string('provider_server_id')->nullable(); + $table->integer('port')->default(22); + $table->string('sudo_password'); + $table->text('meta'); + $table->timestamp('provisioning_job_dispatched_at')->nullable(); + $table->string('status', 25)->default('pending'); + $table->string('daemon_status', 25)->default('pending'); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2017_05_11_213407_create_worker_servers_table.php b/database/migrations/2017_05_11_213407_create_worker_servers_table.php new file mode 100644 index 00000000..9fe214b2 --- /dev/null +++ b/database/migrations/2017_05_11_213407_create_worker_servers_table.php @@ -0,0 +1,32 @@ +bigIncrements('id'); + $table->unsignedInteger('project_id')->index(); + $table->unsignedInteger('stack_id')->index(); + $table->string('name'); + $table->string('size', 25); + $table->string('provider_server_id')->nullable(); + $table->integer('port')->default(22); + $table->string('sudo_password'); + $table->text('meta'); + $table->timestamp('provisioning_job_dispatched_at')->nullable(); + $table->string('status', 25)->default('pending'); + $table->string('daemon_status', 25)->default('pending'); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2017_05_25_194654_create_deployments_table.php b/database/migrations/2017_05_25_194654_create_deployments_table.php new file mode 100644 index 00000000..6b907cd1 --- /dev/null +++ b/database/migrations/2017_05_25_194654_create_deployments_table.php @@ -0,0 +1,34 @@ +increments('id'); + $table->unsignedInteger('stack_id')->index(); + $table->unsignedInteger('initiator_id')->nullable(); + $table->string('branch')->nullable(); + $table->string('commit_hash'); + $table->text('build_commands'); + $table->text('activation_commands'); + $table->text('directories'); + $table->text('daemons'); + $table->text('schedule'); + $table->tinyInteger('activated')->default(0); + $table->string('status', 25)->default('pending'); + $table->timestamps(); + + $table->index('created_at'); + }); + } +} diff --git a/database/migrations/2017_05_25_195216_create_server_deployments_table.php b/database/migrations/2017_05_25_195216_create_server_deployments_table.php new file mode 100644 index 00000000..f1bf4d5d --- /dev/null +++ b/database/migrations/2017_05_25_195216_create_server_deployments_table.php @@ -0,0 +1,36 @@ +increments('id'); + $table->unsignedInteger('deployment_id')->index(); + $table->unsignedInteger('deployable_id'); + $table->string('deployable_type'); + $table->unsignedInteger('build_task_id')->nullable(); + $table->unsignedInteger('activation_task_id')->nullable(); + $table->text('build_commands'); + $table->text('activation_commands'); + $table->string('status', 25)->default('running'); + $table->timestamps(); + + $table->index(['deployable_id', 'deployable_type']); + + $table->foreign('deployment_id') + ->references('id') + ->on('deployments') + ->onDelete('cascade'); + }); + } +} diff --git a/database/migrations/2017_06_11_154558_create_daemon_generations_table.php b/database/migrations/2017_06_11_154558_create_daemon_generations_table.php new file mode 100644 index 00000000..11be0d9d --- /dev/null +++ b/database/migrations/2017_06_11_154558_create_daemon_generations_table.php @@ -0,0 +1,35 @@ +increments('id'); + $table->unsignedInteger('generationable_id'); + $table->string('generationable_type'); + $table->timestamps(); + + $table->index(['generationable_id', 'generationable_type'], 'generationable_morphs'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('daemon_generations'); + } +} diff --git a/database/migrations/2017_06_14_151436_create_stack_databases_table.php b/database/migrations/2017_06_14_151436_create_stack_databases_table.php new file mode 100644 index 00000000..f478f493 --- /dev/null +++ b/database/migrations/2017_06_14_151436_create_stack_databases_table.php @@ -0,0 +1,33 @@ +unsignedInteger('stack_id'); + $table->unsignedInteger('database_id'); + + $table->unique(['stack_id', 'database_id']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('stack_databases'); + } +} diff --git a/database/migrations/2017_07_29_024811_create_certificates_table.php b/database/migrations/2017_07_29_024811_create_certificates_table.php new file mode 100644 index 00000000..d3cd9658 --- /dev/null +++ b/database/migrations/2017_07_29_024811_create_certificates_table.php @@ -0,0 +1,35 @@ +increments('id'); + $table->unsignedInteger('project_id')->index(); + $table->string('name'); + $table->text('private_key'); + $table->text('certificate'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('certificates'); + } +} diff --git a/database/migrations/2017_07_31_191305_create_stack_tasks_table.php b/database/migrations/2017_07_31_191305_create_stack_tasks_table.php new file mode 100644 index 00000000..b5959ba9 --- /dev/null +++ b/database/migrations/2017_07_31_191305_create_stack_tasks_table.php @@ -0,0 +1,38 @@ +increments('id'); + $table->unsignedInteger('stack_id')->index(); + $table->string('name'); + $table->string('user'); + $table->longText('commands'); + $table->string('status')->default('pending'); + $table->timestamps(); + + $table->index('created_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('stack_tasks'); + } +} diff --git a/database/migrations/2017_07_31_191310_create_server_tasks_table.php b/database/migrations/2017_07_31_191310_create_server_tasks_table.php new file mode 100644 index 00000000..c5ab7ea8 --- /dev/null +++ b/database/migrations/2017_07_31_191310_create_server_tasks_table.php @@ -0,0 +1,42 @@ +increments('id'); + $table->unsignedInteger('stack_task_id')->index(); + $table->unsignedInteger('taskable_id'); + $table->string('taskable_type'); + $table->unsignedInteger('task_id')->nullable(); + $table->longText('commands'); + $table->string('status')->default('pending'); + $table->timestamps(); + + $table->foreign('stack_task_id') + ->references('id') + ->on('stack_tasks') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('server_tasks'); + } +} diff --git a/database/migrations/2017_08_02_155914_create_storage_providers_table.php b/database/migrations/2017_08_02_155914_create_storage_providers_table.php new file mode 100644 index 00000000..77b6117a --- /dev/null +++ b/database/migrations/2017_08_02_155914_create_storage_providers_table.php @@ -0,0 +1,35 @@ +increments('id'); + $table->unsignedInteger('user_id')->index(); + $table->string('name'); + $table->string('type', 25); + $table->text('meta'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('storage_providers'); + } +} diff --git a/database/migrations/2017_08_03_145343_create_database_backups_table.php b/database/migrations/2017_08_03_145343_create_database_backups_table.php new file mode 100644 index 00000000..6db61a88 --- /dev/null +++ b/database/migrations/2017_08_03_145343_create_database_backups_table.php @@ -0,0 +1,49 @@ +increments('id'); + $table->unsignedInteger('database_id')->index(); + $table->unsignedInteger('storage_provider_id'); + $table->string('database_name'); + $table->text('backup_path'); + $table->integer('size')->nullable(); + $table->string('status', 25)->default('pending'); + $table->integer('exit_code')->nullable(); + $table->longText('output'); + $table->timestamps(); + + $table->foreign('database_id') + ->references('id') + ->on('databases') + ->onDelete('cascade'); + + $table->foreign('storage_provider_id') + ->references('id') + ->on('storage_providers') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('database_backups'); + } +} diff --git a/database/migrations/2017_08_07_181907_create_database_restores_table.php b/database/migrations/2017_08_07_181907_create_database_restores_table.php new file mode 100644 index 00000000..3a71e65b --- /dev/null +++ b/database/migrations/2017_08_07_181907_create_database_restores_table.php @@ -0,0 +1,42 @@ +increments('id'); + $table->unsignedInteger('database_id')->index(); + $table->unsignedInteger('database_backup_id')->index(); + $table->string('database_name'); + $table->string('status', 25)->default('pending'); + $table->integer('exit_code')->nullable(); + $table->longText('output'); + $table->timestamps(); + + $table->foreign('database_backup_id') + ->references('id') + ->on('database_backups') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('database_restores'); + } +} diff --git a/database/migrations/2017_08_10_151936_create_hooks_table.php b/database/migrations/2017_08_10_151936_create_hooks_table.php new file mode 100644 index 00000000..f021e8d5 --- /dev/null +++ b/database/migrations/2017_08_10_151936_create_hooks_table.php @@ -0,0 +1,37 @@ +increments('id'); + $table->unsignedInteger('stack_id')->index(); + $table->string('name'); + $table->string('token', 40); + $table->string('branch'); + $table->tinyInteger('published')->default(0); + $table->text('meta'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('hooks'); + } +} diff --git a/database/seeds/DatabaseSeeder.php b/database/seeds/DatabaseSeeder.php new file mode 100644 index 00000000..2b898154 --- /dev/null +++ b/database/seeds/DatabaseSeeder.php @@ -0,0 +1,33 @@ +create([ + 'name' => 'Taylor Otwell', + 'email' => 'taylor@laravel.com', + 'api_token' => 'laravel', + ]); + + $provider = factory(ServerProvider::class)->create([ + 'user_id' => $user->id, + ]); + + $project = factory(Project::class)->create([ + 'user_id' => $user->id, + 'server_provider_id' => $provider->id, + 'region' => 'nyc3', + ]); + } +} diff --git a/helpers.php b/helpers.php new file mode 100644 index 00000000..052fffe0 --- /dev/null +++ b/helpers.php @@ -0,0 +1,43 @@ +encode($value); +} + +/** + * Decode a hashid. + * + * @param string $value + * @return int + */ +function hashid_decode($value) +{ + $hashids = new Hashids\Hashids(config('app.key'), 36); + + return $hashids->decode($value)[0]; +} + +/** + * Get a new UUID. + * + * @var string + */ +function uuid() +{ + $orderedTimeFactory = new UuidFactory; + $orderedTimeFactory->setCodec(new OrderedTimeCodec($orderedTimeFactory->getUuidBuilder())); + $orderedTimeUuid = $orderedTimeFactory->uuid1(); + return (string) $orderedTimeUuid; +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..ba4b9c7b --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "private": true, + "scripts": { + "dev": "npm run development", + "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", + "watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", + "watch-poll": "npm run watch -- --watch-poll", + "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", + "prod": "npm run production", + "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" + }, + "devDependencies": { + "axios": "^0.16.2", + "bootstrap-sass": "^3.3.7", + "cross-env": "^5.0.1", + "laravel-echo": "^1.3.0", + "jquery": "^3.1.1", + "laravel-mix": "^1.0", + "lodash": "^4.17.4", + "pusher-js": "^4.1.0", + "vue": "^2.1.10" + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..86f0d95b --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,38 @@ + + + + + ./tests/Feature + + + + ./tests/Unit + + + + + ./app + + + + + ngrok + + + + + + + + + + + diff --git a/public/css/.gitignore b/public/css/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/public/css/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 00000000..e69de29b diff --git a/public/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.eot b/public/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.eot new file mode 100644 index 0000000000000000000000000000000000000000..b93a4953fff68df523aa7656497ee339d6026d64 GIT binary patch literal 20127 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.ttf b/public/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1413fc609ab6f21774de0cb7e01360095584f65b GIT binary patch literal 45404 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 G`u_lkUM?>H literal 0 HcmV?d00001 diff --git a/public/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.woff2 b/public/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..64539b54c3751a6d9adb44c8e3a45ba5a73b77f0 GIT binary patch literal 18028 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 literal 0 HcmV?d00001 diff --git a/public/index.php b/public/index.php new file mode 100644 index 00000000..1e1d775f --- /dev/null +++ b/public/index.php @@ -0,0 +1,58 @@ + + */ + +/* +|-------------------------------------------------------------------------- +| Register The Auto Loader +|-------------------------------------------------------------------------- +| +| Composer provides a convenient, automatically generated class loader for +| our application. We just need to utilize it! We'll simply require it +| into the script here so that we don't have to worry about manual +| loading any of our classes later on. It feels great to relax. +| +*/ + +require __DIR__.'/../bootstrap/autoload.php'; + +/* +|-------------------------------------------------------------------------- +| Turn On The Lights +|-------------------------------------------------------------------------- +| +| We need to illuminate PHP development, so let us turn on the lights. +| This bootstraps the framework and gets it ready for use, then it +| will load up this application so that we can run it and send +| the responses back to the browser and delight our users. +| +*/ + +$app = require_once __DIR__.'/../bootstrap/app.php'; + +/* +|-------------------------------------------------------------------------- +| Run The Application +|-------------------------------------------------------------------------- +| +| Once we have the application, we can handle the incoming request +| through the kernel, and send the associated response back to +| the client's browser allowing them to enjoy the creative +| and wonderful application we have prepared for them. +| +*/ + +$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); + +$response = $kernel->handle( + $request = Illuminate\Http\Request::capture() +); + +$response->send(); + +$kernel->terminate($request, $response); diff --git a/public/js/.gitignore b/public/js/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/public/js/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/public/mix-manifest.json b/public/mix-manifest.json new file mode 100644 index 00000000..e2b79fac --- /dev/null +++ b/public/mix-manifest.json @@ -0,0 +1,3 @@ +{ + "/js/app.js": "/js/app.js" +} \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..eb053628 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/resources/assets/js/app.js b/resources/assets/js/app.js new file mode 100644 index 00000000..8b630c24 --- /dev/null +++ b/resources/assets/js/app.js @@ -0,0 +1,25 @@ + +/** + * First we will load all of this project's JavaScript dependencies which + * includes Vue and other libraries. It is a great starting point when + * building robust, powerful web applications using Vue and Laravel. + */ + +require('./bootstrap'); + +window.Vue = require('vue'); + +/** + * Next, we will create a fresh Vue application instance and attach it to + * the page. Then, you may begin adding components to this application + * or customize the JavaScript scaffolding to fit your unique needs. + */ + +Vue.component( + 'passport-personal-access-tokens', + require('./components/passport/PersonalAccessTokens.vue') +); + +Vue.component('project-list', require('./components/ProjectList.vue')); + +const app = new Vue(require('./root')); diff --git a/resources/assets/js/bootstrap.js b/resources/assets/js/bootstrap.js new file mode 100644 index 00000000..d48eebe9 --- /dev/null +++ b/resources/assets/js/bootstrap.js @@ -0,0 +1,54 @@ + +window._ = require('lodash'); + +/** + * We'll load jQuery and the Bootstrap jQuery plugin which provides support + * for JavaScript based Bootstrap features such as modals and tabs. This + * code may be modified to fit the specific needs of your application. + */ + +try { + window.$ = window.jQuery = require('jquery'); + + require('bootstrap-sass'); +} catch (e) {} + +/** + * We'll load the axios HTTP library which allows us to easily issue requests + * to our Laravel back-end. This library automatically handles sending the + * CSRF token as a header based on the value of the "XSRF" token cookie. + */ + +window.axios = require('axios'); + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; + +/** + * Next we will register the CSRF Token as a common header with Axios so that + * all outgoing HTTP requests automatically have it attached. This is just + * a simple convenience so we don't have to attach every token manually. + */ + +let token = document.head.querySelector('meta[name="csrf-token"]'); + +if (token) { + window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; +} else { + console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token'); +} + +/** + * Echo exposes an expressive API for subscribing to channels and listening + * for events that are broadcast by Laravel. Echo and event broadcasting + * allows your team to easily build robust real-time web applications. + */ + +import Echo from 'laravel-echo' + +window.Pusher = require('pusher-js'); + +window.Echo = new Echo({ + broadcaster: 'pusher', + key: 'defd785a73db4fae0479', + cluster: 'us2' +}); diff --git a/resources/assets/js/components/ProjectList.vue b/resources/assets/js/components/ProjectList.vue new file mode 100644 index 00000000..2594655b --- /dev/null +++ b/resources/assets/js/components/ProjectList.vue @@ -0,0 +1,37 @@ + + + diff --git a/resources/assets/js/components/passport/AuthorizedClients.vue b/resources/assets/js/components/passport/AuthorizedClients.vue new file mode 100644 index 00000000..6afd90af --- /dev/null +++ b/resources/assets/js/components/passport/AuthorizedClients.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/resources/assets/js/components/passport/Clients.vue b/resources/assets/js/components/passport/Clients.vue new file mode 100644 index 00000000..068ca2da --- /dev/null +++ b/resources/assets/js/components/passport/Clients.vue @@ -0,0 +1,354 @@ + + + + + diff --git a/resources/assets/js/components/passport/PersonalAccessTokens.vue b/resources/assets/js/components/passport/PersonalAccessTokens.vue new file mode 100644 index 00000000..d2089905 --- /dev/null +++ b/resources/assets/js/components/passport/PersonalAccessTokens.vue @@ -0,0 +1,302 @@ + + + + + diff --git a/resources/assets/js/root.js b/resources/assets/js/root.js new file mode 100644 index 00000000..650ac071 --- /dev/null +++ b/resources/assets/js/root.js @@ -0,0 +1,29 @@ +module.exports = { + el: '#app', + + /** + * The application's data. + */ + data: { + projects: [] + }, + + /** + * Mount the component. + */ + mounted() { + this.getProjects(); + }, + + methods: { + /** + * Get all of the projects for the current user. + */ + getProjects() { + axios.get('/api/projects') + .then(response => { + this.projects = response.data; + }); + } + } +}; diff --git a/resources/assets/sass/_variables.scss b/resources/assets/sass/_variables.scss new file mode 100644 index 00000000..53202ac1 --- /dev/null +++ b/resources/assets/sass/_variables.scss @@ -0,0 +1,38 @@ + +// Body +$body-bg: #f5f8fa; + +// Borders +$laravel-border-color: darken($body-bg, 10%); +$list-group-border: $laravel-border-color; +$navbar-default-border: $laravel-border-color; +$panel-default-border: $laravel-border-color; +$panel-inner-border: $laravel-border-color; + +// Brands +$brand-primary: #3097D1; +$brand-info: #8eb4cb; +$brand-success: #2ab27b; +$brand-warning: #cbb956; +$brand-danger: #bf5329; + +// Typography +$icon-font-path: "~bootstrap-sass/assets/fonts/bootstrap/"; +$font-family-sans-serif: "Raleway", sans-serif; +$font-size-base: 14px; +$line-height-base: 1.6; +$text-color: #636b6f; + +// Navbar +$navbar-default-bg: #fff; + +// Buttons +$btn-default-color: $text-color; + +// Inputs +$input-border: lighten($text-color, 40%); +$input-border-focus: lighten($brand-primary, 25%); +$input-color-placeholder: lighten($text-color, 30%); + +// Panels +$panel-default-heading-bg: #fff; diff --git a/resources/assets/sass/app.scss b/resources/assets/sass/app.scss new file mode 100644 index 00000000..35ce2dc0 --- /dev/null +++ b/resources/assets/sass/app.scss @@ -0,0 +1,9 @@ + +// Fonts +@import url(https://fonts.googleapis.com/css?family=Raleway:300,400,600); + +// Variables +@import "variables"; + +// Bootstrap +@import "node_modules/bootstrap-sass/assets/stylesheets/bootstrap"; diff --git a/resources/lang/en/auth.php b/resources/lang/en/auth.php new file mode 100644 index 00000000..e5506df2 --- /dev/null +++ b/resources/lang/en/auth.php @@ -0,0 +1,19 @@ + 'These credentials do not match our records.', + 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', + +]; diff --git a/resources/lang/en/pagination.php b/resources/lang/en/pagination.php new file mode 100644 index 00000000..d4814118 --- /dev/null +++ b/resources/lang/en/pagination.php @@ -0,0 +1,19 @@ + '« Previous', + 'next' => 'Next »', + +]; diff --git a/resources/lang/en/passwords.php b/resources/lang/en/passwords.php new file mode 100644 index 00000000..e5544d20 --- /dev/null +++ b/resources/lang/en/passwords.php @@ -0,0 +1,22 @@ + 'Passwords must be at least six characters and match the confirmation.', + 'reset' => 'Your password has been reset!', + 'sent' => 'We have e-mailed your password reset link!', + 'token' => 'This password reset token is invalid.', + 'user' => "We can't find a user with that e-mail address.", + +]; diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php new file mode 100644 index 00000000..2a5c7434 --- /dev/null +++ b/resources/lang/en/validation.php @@ -0,0 +1,119 @@ + 'The :attribute must be accepted.', + 'active_url' => 'The :attribute is not a valid URL.', + 'after' => 'The :attribute must be a date after :date.', + 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', + 'alpha' => 'The :attribute may only contain letters.', + 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', + 'alpha_num' => 'The :attribute may only contain letters and numbers.', + 'array' => 'The :attribute must be an array.', + 'before' => 'The :attribute must be a date before :date.', + 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', + 'between' => [ + 'numeric' => 'The :attribute must be between :min and :max.', + 'file' => 'The :attribute must be between :min and :max kilobytes.', + 'string' => 'The :attribute must be between :min and :max characters.', + 'array' => 'The :attribute must have between :min and :max items.', + ], + 'boolean' => 'The :attribute field must be true or false.', + 'confirmed' => 'The :attribute confirmation does not match.', + 'date' => 'The :attribute is not a valid date.', + 'date_format' => 'The :attribute does not match the format :format.', + 'different' => 'The :attribute and :other must be different.', + 'digits' => 'The :attribute must be :digits digits.', + 'digits_between' => 'The :attribute must be between :min and :max digits.', + 'dimensions' => 'The :attribute has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'email' => 'The :attribute must be a valid email address.', + 'exists' => 'The selected :attribute is invalid.', + 'file' => 'The :attribute must be a file.', + 'filled' => 'The :attribute field must have a value.', + 'image' => 'The :attribute must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'in_array' => 'The :attribute field does not exist in :other.', + 'integer' => 'The :attribute must be an integer.', + 'ip' => 'The :attribute must be a valid IP address.', + 'json' => 'The :attribute must be a valid JSON string.', + 'max' => [ + 'numeric' => 'The :attribute may not be greater than :max.', + 'file' => 'The :attribute may not be greater than :max kilobytes.', + 'string' => 'The :attribute may not be greater than :max characters.', + 'array' => 'The :attribute may not have more than :max items.', + ], + 'mimes' => 'The :attribute must be a file of type: :values.', + 'mimetypes' => 'The :attribute must be a file of type: :values.', + 'min' => [ + 'numeric' => 'The :attribute must be at least :min.', + 'file' => 'The :attribute must be at least :min kilobytes.', + 'string' => 'The :attribute must be at least :min characters.', + 'array' => 'The :attribute must have at least :min items.', + ], + 'not_in' => 'The selected :attribute is invalid.', + 'numeric' => 'The :attribute must be a number.', + 'present' => 'The :attribute field must be present.', + 'regex' => 'The :attribute format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_with_all' => 'The :attribute field is required when :values is present.', + 'required_without' => 'The :attribute field is required when :values is not present.', + 'required_without_all' => 'The :attribute field is required when none of :values are present.', + 'same' => 'The :attribute and :other must match.', + 'size' => [ + 'numeric' => 'The :attribute must be :size.', + 'file' => 'The :attribute must be :size kilobytes.', + 'string' => 'The :attribute must be :size characters.', + 'array' => 'The :attribute must contain :size items.', + ], + 'string' => 'The :attribute must be a string.', + 'timezone' => 'The :attribute must be a valid zone.', + 'unique' => 'The :attribute has already been taken.', + 'uploaded' => 'The :attribute failed to upload.', + 'url' => 'The :attribute format is invalid.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap attribute place-holders + | with something more reader friendly such as E-Mail Address instead + | of "email". This simply helps us make messages a little cleaner. + | + */ + + 'attributes' => [], + +]; diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php new file mode 100644 index 00000000..757d821d --- /dev/null +++ b/resources/views/auth/login.blade.php @@ -0,0 +1,68 @@ +@extends('layouts.app') + +@section('content') +
+
+
+
+
Login
+
+
+ {{ csrf_field() }} + +
+ + +
+ + + @if ($errors->has('email')) + + {{ $errors->first('email') }} + + @endif +
+
+ +
+ + +
+ + + @if ($errors->has('password')) + + {{ $errors->first('password') }} + + @endif +
+
+ +
+
+
+ +
+
+
+ +
+
+ + + + Forgot Your Password? + +
+
+
+
+
+
+
+
+@endsection diff --git a/resources/views/auth/passwords/email.blade.php b/resources/views/auth/passwords/email.blade.php new file mode 100644 index 00000000..e566cfb0 --- /dev/null +++ b/resources/views/auth/passwords/email.blade.php @@ -0,0 +1,46 @@ +@extends('layouts.app') + +@section('content') +
+
+
+
+
Reset Password
+
+ @if (session('status')) +
+ {{ session('status') }} +
+ @endif + +
+ {{ csrf_field() }} + +
+ + +
+ + + @if ($errors->has('email')) + + {{ $errors->first('email') }} + + @endif +
+
+ +
+
+ +
+
+
+
+
+
+
+
+@endsection diff --git a/resources/views/auth/passwords/reset.blade.php b/resources/views/auth/passwords/reset.blade.php new file mode 100644 index 00000000..6ed9298a --- /dev/null +++ b/resources/views/auth/passwords/reset.blade.php @@ -0,0 +1,76 @@ +@extends('layouts.app') + +@section('content') +
+
+
+
+
Reset Password
+ +
+ @if (session('status')) +
+ {{ session('status') }} +
+ @endif + +
+ {{ csrf_field() }} + + + +
+ + +
+ + + @if ($errors->has('email')) + + {{ $errors->first('email') }} + + @endif +
+
+ +
+ + +
+ + + @if ($errors->has('password')) + + {{ $errors->first('password') }} + + @endif +
+
+ +
+ +
+ + + @if ($errors->has('password_confirmation')) + + {{ $errors->first('password_confirmation') }} + + @endif +
+
+ +
+
+ +
+
+
+
+
+
+
+
+@endsection diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php new file mode 100644 index 00000000..83b9f0d9 --- /dev/null +++ b/resources/views/auth/register.blade.php @@ -0,0 +1,76 @@ +@extends('layouts.app') + +@section('content') +
+
+
+
+
Register
+
+
+ {{ csrf_field() }} + +
+ + +
+ + + @if ($errors->has('name')) + + {{ $errors->first('name') }} + + @endif +
+
+ +
+ + +
+ + + @if ($errors->has('email')) + + {{ $errors->first('email') }} + + @endif +
+
+ +
+ + +
+ + + @if ($errors->has('password')) + + {{ $errors->first('password') }} + + @endif +
+
+ +
+ + +
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+
+@endsection diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php new file mode 100644 index 00000000..de73a980 --- /dev/null +++ b/resources/views/home.blade.php @@ -0,0 +1,17 @@ +@extends('layouts.app') + +@section('content') +
+
+
+
+
Dashboard
+ +
+ You are logged in! +
+
+
+
+
+@endsection diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php new file mode 100644 index 00000000..6fec5f64 --- /dev/null +++ b/resources/views/layouts/app.blade.php @@ -0,0 +1,87 @@ + + + + + + + + + + + {{ config('app.name', 'Laravel') }} + + + + + + + + +
+ + + @yield('content') +
+ + + + + diff --git a/resources/views/mail/balancer/provisioned.blade.php b/resources/views/mail/balancer/provisioned.blade.php new file mode 100644 index 00000000..44974e3b --- /dev/null +++ b/resources/views/mail/balancer/provisioned.blade.php @@ -0,0 +1,13 @@ +@component('mail::message') +# Balancer Created + +A new balancer server has been created for the "{{ $balancer->project->name }}" project. +The server's credentials are: + +**Server Name:** {{ $balancer->name }} + +**Sudo Password:** {{ $balancer->sudo_password }} + +Thanks,
+{{ config('app.name') }} +@endcomponent diff --git a/resources/views/mail/database/provisioned.blade.php b/resources/views/mail/database/provisioned.blade.php new file mode 100644 index 00000000..63726b03 --- /dev/null +++ b/resources/views/mail/database/provisioned.blade.php @@ -0,0 +1,17 @@ +@component('mail::message') +# Database Created + +A new database server has been created for the "{{ $database->project->name }}" project. +The server's credentials are: + +**Server Name:** {{ $database->name }} + +**Database Username:** {{ $database->username }} + +**Database Password:** {{ $database->password }} + +**Server Sudo Password:** {{ $database->sudo_password }} + +Thanks,
+{{ config('app.name') }} +@endcomponent diff --git a/resources/views/mail/stack/provisioned.blade.php b/resources/views/mail/stack/provisioned.blade.php new file mode 100644 index 00000000..e76cad4b --- /dev/null +++ b/resources/views/mail/stack/provisioned.blade.php @@ -0,0 +1,16 @@ +@component('mail::message') +# Stack Created + +A new stack has been created for the "{{ $stack->environment->project->name }}" project. +The stack's credentials are: + +**Stack Name:** {{ $stack->name }} + +@foreach ($stack->allServers() as $server) +**{{ $server->name }} Sudo Password:** {{ $server->sudo_password }} + +@endforeach + +Thanks,
+{{ config('app.name') }} +@endcomponent diff --git a/resources/views/projects/index.blade.php b/resources/views/projects/index.blade.php new file mode 100644 index 00000000..a47d698b --- /dev/null +++ b/resources/views/projects/index.blade.php @@ -0,0 +1,17 @@ +@extends('layouts.app') + +@section('content') +
+
+
+ +
+
+ +
+
+ +
+
+
+@endsection diff --git a/resources/views/scripts/app/provision.blade.php b/resources/views/scripts/app/provision.blade.php new file mode 100644 index 00000000..ba588411 --- /dev/null +++ b/resources/views/scripts/app/provision.blade.php @@ -0,0 +1,65 @@ + +export DEBIAN_FRONTEND=noninteractive + +# Run Base Script + +@include('scripts.provisionable.base') + +# Run Caddy Installation Script + +@include('scripts.caddy.install') + +# Run PHP Installation Script + +@include('scripts.php.install') + +# Run Node Installation Script + +@include('scripts.node.install') + +# Run Database Installation Script + +@include('scripts.database.install') + +# Create Dummy App + +mkdir -p /home/cloud/app/public +mkdir -p /home/cloud/maintenance/public + +cat > /home/cloud/app/public/index.php << EOF + + +EOF + +cat > /home/cloud/maintenance/public/index.php << EOF + +Site under maintenance. + +EOF + +# Write Caddyfile For Server + +cat > /home/cloud/Caddyfile << EOF +{!! $script->actualDomainConfiguration() !!} +{!! $script->vanityDomainConfiguration() !!} +EOF + +# Make Sure Directories Have Correct Permissions + +@include('scripts.tools.chown') + +# Update The Supervisor Configuration + +supervisorctl reread +supervisorctl update + +# Start Caddy + +supervisorctl start caddy + +# Run The Custom Scripts + +@foreach ($customScripts as $customScript) +{!! $customScript !!} + +@endforeach diff --git a/resources/views/scripts/balancer/provision.blade.php b/resources/views/scripts/balancer/provision.blade.php new file mode 100644 index 00000000..1cf16eff --- /dev/null +++ b/resources/views/scripts/balancer/provision.blade.php @@ -0,0 +1,37 @@ +# Run Base Script + +@include('scripts.provisionable.base') + +# Install Caddy + +@include('scripts.caddy.install') + +# Create Caddy Directories + +mkdir /home/cloud/status + +# Create Base Caddy Configuration + +cat > /home/cloud/status/index.html << EOF +OK +EOF + +cat > /home/cloud/Caddyfile << EOF +:80 { + root /home/cloud/status + tls off +} +EOF + +# Make Sure Directories Have Correct Permissions + +@include('scripts.tools.chown') + +# Update The Supervisor Configuration + +supervisorctl reread +supervisorctl update + +# Start Caddy + +supervisorctl start caddy diff --git a/resources/views/scripts/balancer/sync.blade.php b/resources/views/scripts/balancer/sync.blade.php new file mode 100644 index 00000000..972edc7d --- /dev/null +++ b/resources/views/scripts/balancer/sync.blade.php @@ -0,0 +1,24 @@ + +# Rewrite Caddyfile + +@if (count($script->balancer->project->allStacks()) > 0) +cat > /home/cloud/Caddyfile << EOF +{!! $script->actualDomainConfiguration() !!} +{!! $script->vanityDomainConfiguration() !!} +EOF +@else +cat > /home/cloud/Caddyfile << EOF +:80 { + root /home/cloud/status + tls off +} +EOF +@endif + +# Make Sure Directories Have Correct Permissions + +@include('scripts.tools.chown') + +# Restart Caddy + +supervisorctl signal USR1 caddy diff --git a/resources/views/scripts/caddy-configuration/app.blade.php b/resources/views/scripts/caddy-configuration/app.blade.php new file mode 100644 index 00000000..756392f3 --- /dev/null +++ b/resources/views/scripts/caddy-configuration/app.blade.php @@ -0,0 +1,25 @@ +{!! $domain !!} { + {!! $tls !!} + + redir 301 { + if {scheme} is http + / https://{!! str_replace([':80', ':443'], '', $domain) !!}{uri} + } + + root {!! $root !!} + +@if (! $index) + header / X-Robots-Tag "noindex" +@endif + + gzip + fastcgi / 127.0.0.1:9000 php + + limits { + body 50mb + } + + rewrite { + to {path} {path}/ /index.php?{query} + } +} diff --git a/resources/views/scripts/caddy-configuration/proxy.blade.php b/resources/views/scripts/caddy-configuration/proxy.blade.php new file mode 100644 index 00000000..5ba1bb5f --- /dev/null +++ b/resources/views/scripts/caddy-configuration/proxy.blade.php @@ -0,0 +1,13 @@ +{!! $domain !!} { + {!! $tls !!} + + redir 301 { + if {scheme} is http + / https://{host}{uri} + } + + proxy / {!! implode(' ', $proxyTo) !!} { + transparent + insecure_skip_verify + } +} diff --git a/resources/views/scripts/caddy-configuration/redirect.blade.php b/resources/views/scripts/caddy-configuration/redirect.blade.php new file mode 100644 index 00000000..8c39feb6 --- /dev/null +++ b/resources/views/scripts/caddy-configuration/redirect.blade.php @@ -0,0 +1,7 @@ +{!! $domain !!} { + {!! $tls !!} + + redir 301 { + / https://{!! $canonicalDomain !!}{uri} + } +} diff --git a/resources/views/scripts/caddy/install.blade.php b/resources/views/scripts/caddy/install.blade.php new file mode 100644 index 00000000..0650afd5 --- /dev/null +++ b/resources/views/scripts/caddy/install.blade.php @@ -0,0 +1,40 @@ + +# Install Packages + +apt-get install -y --force-yes supervisor +curl https://getcaddy.com | bash + +# Configure Supervisor Autostart + +systemctl enable supervisor.service +service supervisor start + +# Allow Caddy To Bind To Root Privileged Ports + +setcap cap_net_bind_service=+ep $(which caddy) + +# Create Caddy Directories + +mkdir /home/cloud/.caddy + +# Write The Caddy Supervisor Configuration + +# -ca "https://acme-staging.api.letsencrypt.org/directory" + +cat > /etc/supervisor/conf.d/caddy.conf << EOF +[program:caddy] +command=/usr/local/bin/caddy -conf="/home/cloud/Caddyfile" -pidfile="/home/cloud/caddy.pid" -log="/home/cloud/caddy.log" -agree -email="letsencrypt@laravel.com" +user=cloud +environment=HOME="/home/cloud",CADDYPATH="/home/cloud/.caddy" +autostart=true +autorestart=unexpected +exitcodes=0,2 +startsecs=1 +startretries=3 +stopsignal=QUIT +stopwaitsecs=10 +stopasgroup=false +redirect_stderr=true +stdout_logfile=/home/cloud/caddy.stdout +stderr_logfile=/home/cloud/caddy.stderr +EOF diff --git a/resources/views/scripts/daemon/activate.blade.php b/resources/views/scripts/daemon/activate.blade.php new file mode 100644 index 00000000..9a21ae1e --- /dev/null +++ b/resources/views/scripts/daemon/activate.blade.php @@ -0,0 +1,19 @@ + +# Stop The Previous Daemon Generations + +@foreach ($previousGenerations as $previousGeneration) + if [ -f /etc/supervisor/conf.d/daemon-{!! $previousGeneration->id !!}.conf ] + then + echo "Stopping Supervisor Group: daemon-{!! $previousGeneration->id !!}" + + nohup bash -c "sudo supervisorctl stop daemon-{!! $previousGeneration->id !!}:* && \ + sudo supervisorctl remove daemon-{!! $previousGeneration->id !!} && \ + rm /etc/supervisor/conf.d/daemon-{!! $previousGeneration->id !!}.conf" > /dev/null 2>&1 & + fi +@endforeach + +# Activate The Daemon Configuration + +sleep 3 + +sudo supervisorctl add daemon-{!! $generation->id !!} diff --git a/resources/views/scripts/daemon/build.blade.php b/resources/views/scripts/daemon/build.blade.php new file mode 100644 index 00000000..f1a63050 --- /dev/null +++ b/resources/views/scripts/daemon/build.blade.php @@ -0,0 +1,51 @@ + +# Build All Of The Daemon Configurations + + + +@foreach ($deployment->daemons() as $name => $daemon) +echo "Writing Daemon Supervisor Configuration" + +# Define Some Variables + +id; ?> + +# Write The Supervisor Configuration + +cat >> /tmp/daemon-programs-{!! $generation->id !!}.conf << EOF +[program:{!! $program !!}] +command={!! $daemon['command'] !!} +directory={!! $daemon['directory'] ?? '/home/cloud/app' !!} +numprocs={!! $daemon['processes'] ?? 1 !!} +process_name=%(program_name)s_%(process_num)02d +user=cloud +autostart=true +autorestart=true +startsecs=3 +startretries=3 +stopsignal=TERM +stopwaitsecs={!! $daemon['wait'] ?? 60 !!} +stopasgroup=true +stdout_logfile=/home/cloud/daemon-{!! $name !!}.stdout +stderr_logfile=/home/cloud/daemon-{!! $name !!}.stderr +stdout_logfile_maxbytes=10MB +stderr_logfile_maxbytes=10MB + +EOF + +@endforeach + +# Prepend Group To The Supervisor Configuration + +cat > /tmp/daemon-group-{!! $generation->id !!} << EOF +[group:daemon-{!! $generation->id !!}] +programs={!! implode(',', $programs) !!} + +EOF + +# Generate Final Supervisor Configuration + +cat /tmp/daemon-group-{!! $generation->id !!} /tmp/daemon-programs-{!! $generation->id !!}.conf \ + > /etc/supervisor/conf.d/daemon-{!! $generation->id !!}.conf + +sudo supervisorctl reread diff --git a/resources/views/scripts/daemon/pause.blade.php b/resources/views/scripts/daemon/pause.blade.php new file mode 100644 index 00000000..68afb47a --- /dev/null +++ b/resources/views/scripts/daemon/pause.blade.php @@ -0,0 +1,5 @@ + +# Pause The Daemons + +echo "Pausing Supervisor Group: daemon-{!! $generation->id !!}" +sudo supervisorctl signal USR2 daemon-{!! $generation->id !!}:* diff --git a/resources/views/scripts/daemon/restart.blade.php b/resources/views/scripts/daemon/restart.blade.php new file mode 100644 index 00000000..8880948b --- /dev/null +++ b/resources/views/scripts/daemon/restart.blade.php @@ -0,0 +1,8 @@ + +# Write Fresh Supervisor Configuration + +{!! $script->daemonConfiguration() !!} + +# Reload Daemons & Stop & Remove Old Ones + +{!! $script->activateDaemons() !!} diff --git a/resources/views/scripts/daemon/start.blade.php b/resources/views/scripts/daemon/start.blade.php new file mode 100644 index 00000000..1ec6eaf1 --- /dev/null +++ b/resources/views/scripts/daemon/start.blade.php @@ -0,0 +1,5 @@ + +# Start The Daemons + +echo "Starting Supervisor Group: daemon-{!! $generation->id !!}" +sudo supervisorctl add daemon-{!! $generation->id !!} diff --git a/resources/views/scripts/daemon/stop.blade.php b/resources/views/scripts/daemon/stop.blade.php new file mode 100644 index 00000000..c66a198d --- /dev/null +++ b/resources/views/scripts/daemon/stop.blade.php @@ -0,0 +1,8 @@ + +# Stop The Daemons + +echo "Stopping Supervisor Group: daemon-{!! $generation->id !!}" + +nohup bash -c "sudo supervisorctl stop daemon-{!! $generation->id !!}:* && \ + sudo supervisorctl remove daemon-{!! $generation->id !!} && \ + rm /etc/supervisor/conf.d/daemon-{!! $generation->id !!}.conf" > /dev/null 2>&1 & diff --git a/resources/views/scripts/daemon/unpause.blade.php b/resources/views/scripts/daemon/unpause.blade.php new file mode 100644 index 00000000..6c692151 --- /dev/null +++ b/resources/views/scripts/daemon/unpause.blade.php @@ -0,0 +1,5 @@ + +# Unpause The Daemons + +echo "Unpausing Supervisor Group: daemon-{!! $generation->id !!}" +sudo supervisorctl signal CONT daemon-{!! $generation->id !!}:* diff --git a/resources/views/scripts/database/backup.blade.php b/resources/views/scripts/database/backup.blade.php new file mode 100644 index 00000000..5bb56196 --- /dev/null +++ b/resources/views/scripts/database/backup.blade.php @@ -0,0 +1,44 @@ + +set -e + +# Set Variables + +BACKUP="{!! basename($backup->backup_path) !!}" + +# Create Backup Directory + +mkdir -p /home/cloud/backups + +# Create Provider Credentials File + +{!! $backup->configurationScript() !!} + +# Create Backup + +mysqldump --single-transaction --skip-lock-tables --quick \ + -u cloud -p{!! $backup->database->password !!} \ + {!! $backup->database_name !!} | gzip > /home/cloud/backups/${BACKUP} + +# Verify The Backup File Was Created + +if [ ! -e /home/cloud/backups/${BACKUP} ]; then + echo "The backup was not created." + + exit 1 +fi + +# Upload The Backup To The Provider + +{!! $backup->uploadScript() !!} + +# Test Result Of Upload + +if [ "$?" -ne "0" ]; then + echo "Failed to upload backup to storage provider." + + exit 1 +fi + +# Remove The Backup File + +rm -f /home/cloud/backups/${BACKUP} diff --git a/resources/views/scripts/database/install.blade.php b/resources/views/scripts/database/install.blade.php new file mode 100644 index 00000000..36fb9319 --- /dev/null +++ b/resources/views/scripts/database/install.blade.php @@ -0,0 +1,61 @@ + +# Install MySQL + +debconf-set-selections <<< "mysql-community-server mysql-community-server/data-dir select ''" +debconf-set-selections <<< "mysql-community-server mysql-community-server/root-pass password {!! $databasePassword !!}" +debconf-set-selections <<< "mysql-community-server mysql-community-server/re-root-pass password {!! $databasePassword !!}" + +apt-get install -y mysql-server + +# Configure Password Expiration + +echo "default_password_lifetime = 0" >> /etc/mysql/mysql.conf.d/mysqld.cnf + +# Configure Access Permissions For Root & Cloud Users + +sed -i '/^bind-address/s/bind-address.*=.*/bind-address = */' /etc/mysql/mysql.conf.d/mysqld.cnf + +mysql --user="root" --password="{!! $databasePassword !!}" -e "GRANT ALL ON *.* TO root@'localhost' IDENTIFIED BY '{!! $databasePassword !!}';" +mysql --user="root" --password="{!! $databasePassword !!}" -e "GRANT ALL ON *.* TO root@'%' IDENTIFIED BY '{!! $databasePassword !!}';" + +service mysql restart + +mysql --user="root" --password="{!! $databasePassword !!}" -e "CREATE USER 'cloud'@'localhost' IDENTIFIED BY '{!! $databasePassword !!}';" +mysql --user="root" --password="{!! $databasePassword !!}" -e "CREATE USER 'cloud'@'%' IDENTIFIED BY '{!! $databasePassword !!}';" +mysql --user="root" --password="{!! $databasePassword !!}" -e "GRANT ALL ON *.* TO 'cloud'@'localhost' IDENTIFIED BY '{!! $databasePassword !!}' WITH GRANT OPTION;" +mysql --user="root" --password="{!! $databasePassword !!}" -e "GRANT ALL ON *.* TO 'cloud'@'%' IDENTIFIED BY '{!! $databasePassword !!}' WITH GRANT OPTION;" +mysql --user="root" --password="{!! $databasePassword !!}" -e "FLUSH PRIVILEGES;" + +# Create The Initial Database + +mysql --user="root" --password="{!! $databasePassword !!}" -e "CREATE DATABASE cloud CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" + +# Install & Configure Redis Server + +apt-get install -y --force-yes redis-server +sed -i 's/bind 127.0.0.1/bind 0.0.0.0/' /etc/redis/redis.conf +service redis-server restart + +# Install & Configure Memcached + +apt-get install -y --force-yes memcached +sed -i 's/-l 127.0.0.1/-l 0.0.0.0/' /etc/memcached.conf +service memcached restart + +# Install & Configure Beanstalk + +apt-get install -y --force-yes beanstalkd +sed -i "s/BEANSTALKD_LISTEN_ADDR.*/BEANSTALKD_LISTEN_ADDR=0.0.0.0/" /etc/default/beanstalkd +sed -i "s/#START=yes/START=yes/" /etc/default/beanstalkd + +# Reload Beanstalk To Pull In New Configuration + +service beanstalkd start +sleep 5 +service beanstalkd restart + +# Install AWS CLI (For S3 Backups) + +apt-get install -y --force-yes python-pip + +pip install awscli diff --git a/resources/views/scripts/database/network.blade.php b/resources/views/scripts/database/network.blade.php new file mode 100644 index 00000000..559c4a38 --- /dev/null +++ b/resources/views/scripts/database/network.blade.php @@ -0,0 +1,16 @@ + +# Rebuild UFW Rules + +@foreach ($previousIpAddresses as $ipAddress) +ufw delete allow from {!! $ipAddress !!} to any port 3306 +ufw delete allow from {!! $ipAddress !!} to any port 6379 +ufw delete allow from {!! $ipAddress !!} to any port 11211 +ufw delete allow from {!! $ipAddress !!} to any port 11300 +@endforeach + +@foreach ($ipAddresses as $ipAddress) +ufw allow from {!! $ipAddress !!} to any port 3306 +ufw allow from {!! $ipAddress !!} to any port 6379 +ufw allow from {!! $ipAddress !!} to any port 11211 +ufw allow from {!! $ipAddress !!} to any port 11300 +@endforeach diff --git a/resources/views/scripts/database/provision.blade.php b/resources/views/scripts/database/provision.blade.php new file mode 100644 index 00000000..75926dea --- /dev/null +++ b/resources/views/scripts/database/provision.blade.php @@ -0,0 +1,14 @@ + +export DEBIAN_FRONTEND=noninteractive + +# Run Base Script + +@include('scripts.provisionable.base') + +# Run Database Installation Script + +@include('scripts.database.install') + +# Make Sure Directories Have Correct Permissions + +@include('scripts.tools.chown') diff --git a/resources/views/scripts/database/restore.blade.php b/resources/views/scripts/database/restore.blade.php new file mode 100644 index 00000000..bc2b7594 --- /dev/null +++ b/resources/views/scripts/database/restore.blade.php @@ -0,0 +1,43 @@ + +set -e + +# Set Variables + +BACKUP="{!! basename($backup->backup_path) !!}" +UNZIPPED_BACKUP="{!! basename($backup->backup_path, '.gz') !!}" + +# Create Restore Directory + +mkdir -p /home/cloud/restores + +# Create Provider Credentials File + +{!! $backup->configurationScript() !!} + +# Download The Backup From The Provider + +rm -f /home/cloud/restores/${BACKUP} +rm -f /home/cloud/restores/${UNZIPPED_BACKUP} + +{!! $backup->downloadScript() !!} + +# Test Result Of Download + +if [ "$?" -ne "0" ]; then + echo "Failed to download backup from storage provider." + + exit 1 +fi + +gunzip /home/cloud/restores/${BACKUP} + +# Create The Database + +mysql --user="cloud" --password="{!! $backup->database->password !!}" -e "CREATE DATABASE IF NOT EXISTS {!! $backup->database_name !!} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" + +mysql -u cloud -p{!! $backup->database->password !!} {!! $backup->database_name !!} < /home/cloud/restores/${UNZIPPED_BACKUP} + +# Remove The Restore Files + +rm -f /home/cloud/restores/${BACKUP} +rm -f /home/cloud/restores/${UNZIPPED_BACKUP} diff --git a/resources/views/scripts/deployment/activate.blade.php b/resources/views/scripts/deployment/activate.blade.php new file mode 100644 index 00000000..b8db42a9 --- /dev/null +++ b/resources/views/scripts/deployment/activate.blade.php @@ -0,0 +1,40 @@ + +set -e + +APP_PATH="/home/cloud/app" +DEPLOYMENTS_PATH="/home/cloud/deployments" +DEPLOYMENT_PATH="$DEPLOYMENTS_PATH/{!! $deployment->timestamp() !!}" + +# Activate New Deployment + +echo "Activating Deployment" + +ln -s $DEPLOYMENT_PATH /home/cloud/energize +mv -Tf /home/cloud/energize $APP_PATH + +# Reload PHP-FPM + +echo "Reloading FPM" + +@if ($script->shouldRestartFpm()) +sudo service php{{ $deployment->phpVersion() }}-fpm reload +@endif + +# Run User Defined Activation Commands + +cd $DEPLOYMENT_PATH + +echo "Running User Activation Commands" + +@foreach ($deployment->activation_commands as $command) +{!! $command !!} + +@endforeach + +# Delete Old Deployments + +echo "Purging Old Deployments" + +cd $DEPLOYMENTS_PATH + +rm -rf `ls -t | tail -n +{{ 2 + 1 }}` diff --git a/resources/views/scripts/deployment/build.blade.php b/resources/views/scripts/deployment/build.blade.php new file mode 100644 index 00000000..0d9745c7 --- /dev/null +++ b/resources/views/scripts/deployment/build.blade.php @@ -0,0 +1,110 @@ + +set -e + +# Define Some Variables + +TARBALL_PATH="/home/cloud/tarballs" +STORAGE_PATH="/home/cloud/directories" +DEPLOYMENTS_PATH="/home/cloud/deployments" +TARBALL="$TARBALL_PATH/{!! $deployment->hash() !!}" +DEPLOYMENT_PATH="/home/cloud/deployments/{!! $deployment->timestamp() !!}" +ENVIRONMENT_ENV="$DEPLOYMENT_PATH/.env.{!! $deployment->stack()->environment->name !!}" + +# Remove The App Directory If It's Not A Symlink + +if [ ! -h /home/cloud/app ] +then + echo "Removing App Directory" + rm -rf /home/cloud/app +fi + +# Ensure Necessary Directories Exist + +mkdir -p $TARBALL_PATH +mkdir -p $DEPLOYMENT_PATH + +# Unpack Tarball To Deployment Directory + +echo "Downloading Project Tarball" + +wget {!! $deployment->tarballUrl() !!} -O "$TARBALL" --progress=dot:mega +tar -xvf $TARBALL -C "$DEPLOYMENT_PATH" --strip-components=1 > /dev/null +rm -f $TARBALL + +# Create Environment File + +touch $DEPLOYMENT_PATH/.env + +if [ -f $ENVIRONMENT_ENV ] +then + $ENVIRONMENT_ENV $DEPLOYMENT_PATH/.env +fi + +@if ($deployment->environmentVariables()) +echo "Writing Environment File" + +cat > $DEPLOYMENT_PATH/.env << EOF +{!! $deployment->environmentVariables() !!} + +EOF +@endif + +# Add Stack Variables To Environment File + +echo "Appending To Environment File" + +cat >> $DEPLOYMENT_PATH/.env << EOF + +APP_KEY={!! $deployable->stack->environment->encryption_key !!} + +CLOUD_MASTER={!! $deployable->isMaster() ? 'true' : 'false' !!} +CLOUD_WORKER={!! $deployable->isWorker() ? 'true' : 'false' !!} +CLOUD_MASTER_WORKER={!! $deployable->isMasterWorker() ? 'true' : 'false' !!} + +DB_CONNECTION=mysql +DB_HOST={!! $deployment->databaseHost() !!} +DB_DATABASE=cloud +DB_USERNAME=cloud +DB_PASSWORD={!! $deployment->databasePassword() !!} +DB_PORT=3306 + +@foreach ($deployment->stack()->databases as $database) +{!! $database->variableName() !!}_HOST={!! $database->address->private_address !!} +{!! $database->variableName() !!}_PASSWORD={!! $database->password !!} +@endforeach + +REDIS_HOST={!! $deployment->databaseHost() !!} +BEANSTALKD_HOST={!! $deployment->databaseHost() !!} + +EOF + +# Ensure Storage Directory Exists + +mkdir -p $STORAGE_PATH + +# Link Directories + +@foreach ($directories as $directory) +if [ -d $DEPLOYMENT_PATH/{{ $directory }} ] +then + echo "Linking Directory ({{ $directory }})" + + STORAGE_DIRECTORY_PATH="${STORAGE_PATH}/{{ str_replace('/', '-', $directory) }}" + + mkdir -p ${STORAGE_DIRECTORY_PATH} + cp -ar ${DEPLOYMENT_PATH}/{{ $directory }}/* ${STORAGE_DIRECTORY_PATH} + rm -rf "${DEPLOYMENT_PATH}/{{ $directory }}" + ln -s ${STORAGE_DIRECTORY_PATH} "${DEPLOYMENT_PATH}/{{ $directory }}" +fi +@endforeach + +# Run User Defined Build Commands + +echo "Running User Build Commands" + +cd $DEPLOYMENT_PATH + +@foreach ($deployment->build_commands as $command) +{!! $command !!} + +@endforeach diff --git a/resources/views/scripts/node/install.blade.php b/resources/views/scripts/node/install.blade.php new file mode 100644 index 00000000..152dad9c --- /dev/null +++ b/resources/views/scripts/node/install.blade.php @@ -0,0 +1,12 @@ + +# Install NodeJS + +curl --silent --location https://deb.nodesource.com/setup_8.x | bash - +apt-get update +sudo apt-get install -y --force-yes nodejs + +# Install Yarn + +curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update && sudo apt-get install yarn diff --git a/resources/views/scripts/php/cli.ini b/resources/views/scripts/php/cli.ini new file mode 100644 index 00000000..6a9278a5 --- /dev/null +++ b/resources/views/scripts/php/cli.ini @@ -0,0 +1,22 @@ +[PHP] +engine = On +error_reporting = E_ALL +expose_php = On +log_errors = On +max_execution_time = 0 +max_input_time = -1 +memory_limit = 512M +output_buffering = 4096 +register_argc_argv = Off +request_order = "GP" +short_open_tag = Off +variables_order = "GPCS" + +[CLI Server] +cli_server.color = On + +[Date] +date.timezone = UTC + +[Assertion] +zend.assertions = -1 diff --git a/resources/views/scripts/php/fpm.ini b/resources/views/scripts/php/fpm.ini new file mode 100644 index 00000000..047314eb --- /dev/null +++ b/resources/views/scripts/php/fpm.ini @@ -0,0 +1,30 @@ +[PHP] +engine = On +error_reporting = E_ALL +expose_php = Off +log_errors = On +memory_limit = 512M +output_buffering = 4096 +post_max_size = 50M +register_argc_argv = Off +request_order = "GP" +short_open_tag = Off +upload_max_filesize = 50M +variables_order = "GPCS" + +[CLI Server] +cli_server.color = On + +[Date] +date.timezone = UTC + +[Assertion] +zend.assertions = -1 + +[opcache] +opcache.enable = 1 +opcache.interned_strings_buffer = 64 +opcache.max_accelerated_files = 30000 +opcache.memory_consumption = 512 +opcache.save_comments = 1 +opcache.validate_timestamps = 0 diff --git a/resources/views/scripts/php/install.blade.php b/resources/views/scripts/php/install.blade.php new file mode 100644 index 00000000..e5272fc3 --- /dev/null +++ b/resources/views/scripts/php/install.blade.php @@ -0,0 +1,58 @@ +# Install Base PHP Packages + +apt-add-repository ppa:ondrej/php -y + +apt-get update + +apt-get install -y --force-yes php7.1-bcmath \ + php7.1-cli \ + php7.1-curl \ + php7.1.dev \ + php7.1-fpm \ + php7.1-gd \ + php7.1-imap \ + php7.1-intl \ + php7.1-mbstring \ + php7.1-memcached \ + php7.1-mcrypt \ + php7.1-mysql \ + php7.1-pgsql \ + php7.1-readline \ + php7.1-soap \ + php7.1-sqlite3 \ + php7.1-xml \ + php7.1-zip + +# Install Composer Package Manager + +curl -sS https://getcomposer.org/installer | php +mv composer.phar /usr/local/bin/composer + +# Configure PHP CLI + +cat > /etc/php/7.1/cli/php.ini << EOF +{!! file_get_contents(resource_path('views/scripts/php/cli.ini')) !!} + +EOF + +# Configure PHP FPM + +cat > /etc/php/7.1/fpm/php.ini << EOF +{!! file_get_contents(resource_path('views/scripts/php/fpm.ini')) !!} + +EOF + +# Configure FPM Pool + +cat > /etc/php/7.1/fpm/pool.d/www.conf << EOF +{!! file_get_contents(resource_path('views/scripts/php/www.conf')) !!} + +EOF + +# Restart FPM + +service php7.1-fpm restart > /dev/null 2>&1 + +# Configure Sudoers Entries + +echo "cloud ALL=NOPASSWD: /usr/sbin/service php7.1-fpm reload" > /etc/sudoers.d/php-fpm diff --git a/resources/views/scripts/php/www.conf b/resources/views/scripts/php/www.conf new file mode 100644 index 00000000..35163e6f --- /dev/null +++ b/resources/views/scripts/php/www.conf @@ -0,0 +1,16 @@ +[www] +user = cloud +group = cloud +listen = 127.0.0.1:9000 + +listen.owner = cloud +listen.group = cloud +listen.mode = 0666 + +pm = dynamic +pm.max_children = 24 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 + +request_terminate_timeout = 60 diff --git a/resources/views/scripts/provisionable/addKey.blade.php b/resources/views/scripts/provisionable/addKey.blade.php new file mode 100644 index 00000000..239e7623 --- /dev/null +++ b/resources/views/scripts/provisionable/addKey.blade.php @@ -0,0 +1,10 @@ + +# Write Key & Regenerate Keys File + +cat > /home/cloud/.ssh/authorized_keys.d/{{ $name }} << EOF +# {{ $name }} +{{ $key }} + +EOF + +cat /home/cloud/.ssh/authorized_keys.d/* > /home/cloud/.ssh/authorized_keys diff --git a/resources/views/scripts/provisionable/base.blade.php b/resources/views/scripts/provisionable/base.blade.php new file mode 100644 index 00000000..56ac1092 --- /dev/null +++ b/resources/views/scripts/provisionable/base.blade.php @@ -0,0 +1,192 @@ + +export DEBIAN_FRONTEND=noninteractive + +# Wait For Apt To Unlock + +while fuser /var/lib/dpkg/lock >/dev/null 2>&1 ; do + echo "Waiting for other software managers to finish..." + + sleep 1 +done + +# Update & Install Packages + +apt-get update +apt-get upgrade -y + +apt-get install -y --force-yes build-essential \ + curl \ + fail2ban \ + ufw \ + software-properties-common \ + supervisor \ + whois + +# Disable Password Authentication Over SSH + +sed -i "/PasswordAuthentication yes/d" /etc/ssh/sshd_config +echo "" | sudo tee -a /etc/ssh/sshd_config +echo "" | sudo tee -a /etc/ssh/sshd_config +echo "PasswordAuthentication no" | sudo tee -a /etc/ssh/sshd_config + +# Restart SSH + +ssh-keygen -A +service ssh restart + +# Set The Hostname + +echo "{!! $script->provisionable->name !!}" > /etc/hostname +sed -i 's/127\.0\.0\.1.*localhost/127.0.0.1 {!! $script->provisionable->name !!} localhost/' /etc/hosts +hostname {!! $script->provisionable->name !!} + +# Set The Timezone + +ln -sf /usr/share/zoneinfo/UTC /etc/localtime + +# Create The Root SSH Directory If Necessary + +if [ ! -d /root/.ssh ] +then + mkdir -p /root/.ssh + touch /root/.ssh/authorized_keys +fi + +# Setup Cloud User + +useradd cloud +mkdir -p /home/cloud/.ssh +mkdir -p /home/cloud/.cloud +adduser cloud sudo + +# Setup Bash For The Cloud User + +chsh -s /bin/bash cloud +cp /root/.profile /home/cloud/.profile +cp /root/.bashrc /home/cloud/.bashrc + +# Set The Sudo Password For The Cloud User + +PASSWORD=$(mkpasswd {!! $script->provisionable->sudo_password !!}) +usermod --password $PASSWORD cloud + +# Build SSH Key Directories + +mkdir -p /root/.ssh/authorized_keys.d +mkdir -p /home/cloud/.ssh/authorized_keys.d + +# Write Local Key If Necessary + +@if (app()->environment('local')) +cat > /root/.ssh/authorized_keys.d/local << EOF +# Local +{!! file_get_contents(env('TEST_SSH_CONTAINER_PUBLIC_KEY')) !!} +EOF + +cp /root/.ssh/authorized_keys.d/local /home/cloud/.ssh/authorized_keys.d/local +@endif + +# Write Owner Key + +cat > /root/.ssh/authorized_keys.d/owner << EOF +# Owner +{{ $script->provisionable->project->user->public_worker_key }} +EOF + +cp /root/.ssh/authorized_keys.d/owner /home/cloud/.ssh/authorized_keys.d/owner + +# Generate Authorized Keys File + +cat /root/.ssh/authorized_keys.d/* > /root/.ssh/authorized_keys +cat /home/cloud/.ssh/authorized_keys.d/* > /home/cloud/.ssh/authorized_keys + +# Build Key Generation Cron + +cat > /etc/cron.d/authorized_keys << EOF +SHELL=/bin/sh +PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin + +* * * * * cloud cat /home/cloud/.ssh/authorized_keys.d/* > /home/cloud/.ssh/authorized_keys +EOF + +# Create The Server SSH Key + +ssh-keygen -f /home/cloud/.ssh/id_rsa -t rsa -N '' + +# Configure Supervisor + +systemctl enable supervisor.service +service supervisor start + +chmod 777 /etc/supervisor/conf.d + +echo "cloud ALL=NOPASSWD: /usr/bin/supervisorctl *" > /etc/sudoers.d/supervisorctl + +# Setup UFW Firewall + +ufw allow 22 +ufw allow 80 +ufw allow 443 +ufw --force enable + +# Configure Logrotate + +cat > /etc/logrotate.d/cloud-app << EOF +/home/cloud/app/storage/logs/*.log { + su cloud cloud + missingok + size 10M + rotate 5 + compress + notifempty + create 755 cloud cloud +} +EOF + +cat > /etc/logrotate.d/cloud-scheduler << EOF +/home/cloud/scheduler.log { + su cloud cloud + missingok + size 10M + rotate 5 + compress + notifempty + create 755 cloud cloud +} +EOF + +# Configure Swap Disk + +if [ -f /swapfile ]; then + echo "Swap exists." +else + fallocate -l 1G /swapfile + chmod 600 /swapfile + mkswap /swapfile + swapon /swapfile + echo "/swapfile none swap sw 0 0" >> /etc/fstab + echo "vm.swappiness=30" >> /etc/sysctl.conf + echo "vm.vfs_cache_pressure=50" >> /etc/sysctl.conf +fi + +# Setup Unattended Security Upgrades + +cat > /etc/apt/apt.conf.d/50unattended-upgrades << EOF +Unattended-Upgrade::Allowed-Origins { + "Ubuntu zesty-security"; +}; +Unattended-Upgrade::Package-Blacklist { + // +}; +EOF + +cat > /etc/apt/apt.conf.d/10periodic << EOF +APT::Periodic::Update-Package-Lists "1"; +APT::Periodic::Download-Upgradeable-Packages "1"; +APT::Periodic::AutocleanInterval "7"; +APT::Periodic::Unattended-Upgrade "1"; +EOF + +# Make Sure Directories Have Correct Permissions + +@include('scripts.tools.chown') diff --git a/resources/views/scripts/provisionable/removeKey.blade.php b/resources/views/scripts/provisionable/removeKey.blade.php new file mode 100644 index 00000000..35c2c9f6 --- /dev/null +++ b/resources/views/scripts/provisionable/removeKey.blade.php @@ -0,0 +1,6 @@ + +# Remove Key & Regenerate Keys File + +rm -f /home/cloud/.ssh/authorized_keys.d/{{ $name }} + +cat /home/cloud/.ssh/authorized_keys.d/* > /home/cloud/.ssh/authorized_keys diff --git a/resources/views/scripts/scheduler/start.blade.php b/resources/views/scripts/scheduler/start.blade.php new file mode 100644 index 00000000..c450984f --- /dev/null +++ b/resources/views/scripts/scheduler/start.blade.php @@ -0,0 +1,13 @@ + +# Start The Scheduled Tasks + +rm -f /etc/cron.d/schedule-* + +@foreach ($deployment->schedule() as $name => $options) +cat > /etc/cron.d/schedule-{{ $name }} << EOF +SHELL=/bin/sh +PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin + +{{ $options['frequency'] }} {{ $options['user'] ?? 'cloud' }} {{ $options['command'] }} >> /home/cloud/schedule-{{ $name }}.log 2>&1 +EOF +@endforeach diff --git a/resources/views/scripts/scheduler/stop.blade.php b/resources/views/scripts/scheduler/stop.blade.php new file mode 100644 index 00000000..7221bb69 --- /dev/null +++ b/resources/views/scripts/scheduler/stop.blade.php @@ -0,0 +1,4 @@ + +# Stop The Scheduler + +rm -f /etc/cron.d/schedule-* diff --git a/resources/views/scripts/server/sync.blade.php b/resources/views/scripts/server/sync.blade.php new file mode 100644 index 00000000..c9c6a46b --- /dev/null +++ b/resources/views/scripts/server/sync.blade.php @@ -0,0 +1,15 @@ + +# Write Caddyfile For Server + +cat > /home/cloud/Caddyfile << EOF +{!! $script->actualDomainConfiguration() !!} +{!! $script->vanityDomainConfiguration() !!} +EOF + +# Make Sure Directories Have Correct Permissions + +@include('scripts.tools.chown') + +# Restart Caddy + +supervisorctl signal USR1 caddy diff --git a/resources/views/scripts/storage-provider-configuration/s3.blade.php b/resources/views/scripts/storage-provider-configuration/s3.blade.php new file mode 100644 index 00000000..19124705 --- /dev/null +++ b/resources/views/scripts/storage-provider-configuration/s3.blade.php @@ -0,0 +1,18 @@ + +mkdir -p /home/cloud/.aws + +# Write The Credentials File + +cat > /home/cloud/.aws/credentials << EOF +[default] +aws_access_key_id = {!! $provider->meta['key'] !!} +aws_secret_access_key = {!! $provider->meta['secret'] !!} +EOF + +# Write The Configuration File + +cat > /home/cloud/.aws/config << EOF +[default] +output = json +region = {!! $provider->meta['region'] !!} +EOF diff --git a/resources/views/scripts/tools/callback.blade.php b/resources/views/scripts/tools/callback.blade.php new file mode 100644 index 00000000..4f9c379c --- /dev/null +++ b/resources/views/scripts/tools/callback.blade.php @@ -0,0 +1,21 @@ + +# Rewrite Script Into Another File + +cat > {!! $path !!} << '{!! $token !!}' +{!! $task->script !!} + +{!! $token !!} + +# Invoke Script File + +@if ($task->timeout() > 0) +timeout {!! $task->timeout() !!}s bash {!! $path !!} +@else +bash {!! $path !!} +@endif + +# Call Home With ID & Status Code + +STATUS=$? + +curl --insecure {!! url('/api/callback/'.hashid_encode($task->id)) !!}?exit_code=$STATUS > /dev/null 2>&1 diff --git a/resources/views/scripts/tools/chown.blade.php b/resources/views/scripts/tools/chown.blade.php new file mode 100644 index 00000000..45748d3f --- /dev/null +++ b/resources/views/scripts/tools/chown.blade.php @@ -0,0 +1,13 @@ + +# Set The Proper Directory Permissions + +chown -R cloud:cloud /home/cloud +chmod -R 755 /home/cloud + +chmod 700 /home/cloud/.ssh +chmod 700 /home/cloud/.ssh/authorized_keys.d + +chmod 644 /home/cloud/.ssh/authorized_keys.d/* +chmod 644 /home/cloud/.ssh/authorized_keys +chmod 644 /home/cloud/.ssh/id_rsa.pub +chmod 600 /home/cloud/.ssh/id_rsa diff --git a/resources/views/scripts/web/provision.blade.php b/resources/views/scripts/web/provision.blade.php new file mode 100644 index 00000000..6c43af28 --- /dev/null +++ b/resources/views/scripts/web/provision.blade.php @@ -0,0 +1,61 @@ + +export DEBIAN_FRONTEND=noninteractive + +# Run Base Script + +@include('scripts.provisionable.base') + +# Run Caddy Installation Script + +@include('scripts.caddy.install') + +# Run PHP Installation Script + +@include('scripts.php.install') + +# Run Node Installation Script + +@include('scripts.node.install') + +# Create Dummy App + +mkdir -p /home/cloud/app/public +mkdir -p /home/cloud/maintenance/public + +cat > /home/cloud/app/public/index.php << EOF + + +EOF + +cat > /home/cloud/maintenance/public/index.php << EOF + +Site under maintenance. + +EOF + +# Write Caddyfile For Server + +cat > /home/cloud/Caddyfile << EOF +{!! $script->actualDomainConfiguration() !!} +{!! $script->vanityDomainConfiguration() !!} +EOF + +# Make Sure Directories Have Correct Permissions + +@include('scripts.tools.chown') + +# Update The Supervisor Configuration + +supervisorctl reread +supervisorctl update + +# Start Caddy + +supervisorctl start caddy + +# Run The Custom Scripts + +@foreach ($customScripts as $customScript) +{!! $customScript !!} + +@endforeach diff --git a/resources/views/scripts/worker/provision.blade.php b/resources/views/scripts/worker/provision.blade.php new file mode 100644 index 00000000..af7fc9cf --- /dev/null +++ b/resources/views/scripts/worker/provision.blade.php @@ -0,0 +1,21 @@ + +export DEBIAN_FRONTEND=noninteractive + +# Run Base Script + +@include('scripts.provisionable.base') + +# Run PHP Installation Script + +@include('scripts.php.install') + +# Make Sure Directories Have Correct Permissions + +@include('scripts.tools.chown') + +# Run The Custom Scripts + +@foreach ($customScripts as $customScript) +{!! $customScript !!} + +@endforeach diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php new file mode 100644 index 00000000..44b7e721 --- /dev/null +++ b/resources/views/welcome.blade.php @@ -0,0 +1,95 @@ + + + + + + + + Laravel + + + + + + + + +
+ @if (Route::has('login')) + + @endif + +
+
+ Laravel +
+ + +
+
+ + diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 00000000..247dba64 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,132 @@ + 'auth:api', +], function () { + // Providers... + Route::get('/server-providers', 'API\ServerProviderController@index'); + Route::post('/server-provider', 'API\ServerProviderController@store'); + + Route::get('/server-provider/{provider}/regions', 'API\ServerProviderRegionController@index'); + Route::get('/server-provider/{provider}/sizes', 'API\ServerProviderSizeController@index'); + + // Source Control Providers... + Route::get('/source-providers', 'API\SourceProviderController@index'); + Route::post('/source-provider', 'API\SourceProviderController@store'); + Route::delete('/source-provider/{sourceProvider}', 'API\SourceProviderController@destroy'); + + // Storage Providers... + Route::get('/storage-providers', 'API\StorageProviderController@index'); + Route::post('/storage-provider', 'API\StorageProviderController@store'); + Route::delete('/storage-provider/{storageProvider}', 'API\StorageProviderController@destroy'); + + // Projects... + Route::get('/projects', 'API\ProjectController@index'); + Route::get('/owned-projects', 'API\OwnedProjectsController@index'); + Route::get('/project/{project}', 'API\ProjectController@show'); + Route::post('/project', 'API\ProjectController@store'); + Route::get('/project/{project}/sizes', 'API\ProjectSizeController@index'); + Route::delete('/project/{project}', 'API\ProjectController@destroy'); + + // Project Collaborators... + Route::get('/collaborators', 'API\CollaboratorController@index'); + Route::delete('/collaborator', 'API\CollaboratorController@destroy'); + + Route::get('/project/{project}/collaborators', 'API\ProjectCollaboratorController@index'); + Route::post('/project/{project}/collaborator', 'API\ProjectCollaboratorController@store'); + Route::delete('/project/{project}/collaborator', 'API\ProjectCollaboratorController@destroy'); + + // Databases... + Route::get('/project/{project}/databases', 'API\DatabaseController@index'); + Route::get('/project/{project}/ssh-databases', 'API\SshDatabaseController@index'); + Route::post('/project/{project}/database', 'API\DatabaseController@store'); + Route::post('/database/{database}/transfers', 'API\DatabaseTransferController@store'); + Route::delete('/database/{database}', 'API\DatabaseController@destroy'); + + // Database Backups... + Route::get('/database/{database}/backups', 'API\DatabaseBackupController@index'); + Route::post('/database/{database}/backup', 'API\DatabaseBackupController@store'); + Route::delete('/backup/{backup}', 'API\DatabaseBackupController@destroy'); + + // Database Restores... + Route::get('/database/{database}/restores', 'API\DatabaseRestoreController@index'); + Route::post('/backup/{backup}/restore', 'API\DatabaseRestoreController@store'); + + // Balancers... + Route::get('/project/{project}/balancers', 'API\BalancerController@index'); + Route::get('/project/{project}/ssh-balancers', 'API\SshBalancerController@index'); + Route::post('/project/{project}/balancer', 'API\BalancerController@store'); + Route::delete('/balancer/{balancer}', 'API\BalancerController@destroy'); + + // Environments... + Route::get('/project/{project}/environments', 'API\EnvironmentController@index'); + Route::get('/environment/{environment}', 'API\EnvironmentController@show'); + Route::post('/project/{project}/environment', 'API\EnvironmentController@store'); + Route::put('/environment/{environment}', 'API\EnvironmentController@update'); + Route::delete('/environment/{environment}', 'API\EnvironmentController@destroy'); + + // Stacks... + Route::get('/project/{project}/stacks', 'API\StackController@index'); + Route::post('/environment/{environment}/stack', 'API\StackController@store'); + Route::get('/environment/{environment}/promoted-stack', 'API\PromotedStackController@show'); + Route::put('/environment/{environment}/promoted-stack', 'API\PromotedStackController@update'); + Route::delete('/stacks/{stack}', 'API\StackController@destroy'); + + Route::put('/stack/{stack}/server-configuration', 'API\ServerConfigurationController@update'); + Route::put('/stack/{stack}/databases', 'API\StackDatabaseController@update'); + + // Stack Servers... + Route::get('/stack/{stack}/servers', 'API\StackServerController@index'); + Route::get('/stack/{stack}/ssh-servers', 'API\StackSshServerController@index'); + + // Deployments... + Route::get('/stack/{stack}/deployments', 'API\DeploymentController@index'); + Route::get('/deployment/{deployment}', 'API\DeploymentController@show'); + Route::post('/stack/{stack}/deployment', 'API\DeploymentController@store'); + Route::delete('/stack/{stack}/deployment', 'API\LastDeploymentController@destroy'); + Route::delete('/deployment/{deployment}', 'API\DeploymentController@destroy'); + + // Hooks... + Route::get('/environment/{environment}/hooks', 'API\EnvironmentHookController@index'); + Route::get('/stack/{stack}/hooks', 'API\HookController@index'); + Route::post('/stack/{stack}/hook', 'API\HookController@store'); + Route::delete('/hook/{hook}', 'API\HookController@destroy'); + + // Daemons... + Route::put('/stack/{stack}/daemons', 'API\DaemonController@update'); + + // Scheduler... + Route::post('/stack/{stack}/scheduler', 'API\SchedulerController@store'); + Route::delete('/stack/{stack}/scheduler', 'API\SchedulerController@destroy'); + + // Keys... + Route::post('/key/{ip_address}', 'API\KeyController@store'); + + // Stack Tasks... + Route::post('/stack/{stack}/stack-tasks', 'API\StackTaskController@store'); + + // Maintenance Mode... + Route::post('/maintenanced-stacks', 'API\MaintenancedStackController@store'); + Route::delete('/maintenanced-stack/{stack}', 'API\MaintenancedStackController@destroy'); +}); diff --git a/routes/channels.php b/routes/channels.php new file mode 100644 index 00000000..25ab121b --- /dev/null +++ b/routes/channels.php @@ -0,0 +1,22 @@ +id === (int) $id; +}); + +Broadcast::channel('stack.{stack}', function ($user, Stack $stack) { + return $user->can('view', $stack); +}); diff --git a/routes/console.php b/routes/console.php new file mode 100644 index 00000000..c44e27d7 --- /dev/null +++ b/routes/console.php @@ -0,0 +1,25 @@ +comment(Inspiring::quote()); +})->describe('Display an inspiring quote'); + + +Artisan::command('cloud', function () { + $backup = App\DatabaseBackup::find(1); + + $backup->restore(); +}); diff --git a/routes/schedule.php b/routes/schedule.php new file mode 100644 index 00000000..d3e59f41 --- /dev/null +++ b/routes/schedule.php @@ -0,0 +1,3 @@ +make(Kernel::class)->bootstrap(); + + return $app; + } +} diff --git a/tests/Fakes/FakeTask.php b/tests/Fakes/FakeTask.php new file mode 100644 index 00000000..faa5bd2f --- /dev/null +++ b/tests/Fakes/FakeTask.php @@ -0,0 +1,22 @@ +ranInBackground = true; + + return $this; + } +} diff --git a/tests/Feature/ActivateJobTest.php b/tests/Feature/ActivateJobTest.php new file mode 100644 index 00000000..fedf16e0 --- /dev/null +++ b/tests/Feature/ActivateJobTest.php @@ -0,0 +1,65 @@ +withoutExceptionHandling(); + } + + + public function test_task_id_is_stored() + { + $serverDeployment = factory(ServerDeployment::class)->create(); + $serverDeployment->setRelation('deployable', $deployable = new ActivateJobTestFakeDeployable); + $serverDeployment->stack()->environment->update([ + 'name' => 'workbench', + ]); + + $job = new Activate($serverDeployment); + + ShellProcessRunner::mock([ + (object) ['exitCode' => 0, 'output' => '', 'timedOut' => false], + ]); + + $job->handle(); + + $this->assertEquals(123, $serverDeployment->fresh()->activation_task_id); + $this->assertInstanceOf(ActivateScript::class, $deployable->script); + $this->assertInstanceOf(CheckActivation::class, $deployable->options['then'][0]); + $this->assertInstanceOf(StartBackgroundServices::class, $deployable->options['then'][1]); + } +} + + +class ActivateJobTestFakeDeployable extends AppServer +{ + public $script; + public $options; + + public function runInBackground(Script $script, array $options = []) + { + $this->script = $script; + $this->options = $options; + + return (object) ['id' => 123]; + } +} diff --git a/tests/Feature/ActivateScriptTest.php b/tests/Feature/ActivateScriptTest.php new file mode 100644 index 00000000..2b190c0a --- /dev/null +++ b/tests/Feature/ActivateScriptTest.php @@ -0,0 +1,33 @@ +withoutExceptionHandling(); + } + + + public function test_script_can_be_rendered() + { + $deployment = factory(ServerDeployment::class)->create(); + + $deployment->deployable->createDaemonGeneration(); + + $script = new Activate($deployment); + + $this->assertNotNull($script->script()); + } +} diff --git a/tests/Feature/BalancerControllerTest.php b/tests/Feature/BalancerControllerTest.php new file mode 100644 index 00000000..61e37e1f --- /dev/null +++ b/tests/Feature/BalancerControllerTest.php @@ -0,0 +1,133 @@ +withoutExceptionHandling(); + } + + + public function test_balancers_can_be_listed() + { + $balancer = factory(Balancer::class)->create(); + + $response = $this->actingAs($balancer->project->user, 'api') + ->get('/api/project/'.$balancer->project->id.'/balancers'); + + $response->assertStatus(200); + $this->assertCount(1, $response->original); + $this->assertEquals($balancer->id, $response->original[0]->id); + } + + + public function test_duplicate_balancer_names_cant_be_created() + { + $balancer = factory(Balancer::class)->create(['name' => 'main']); + + $response = $this->withExceptionHandling() + ->actingAs($balancer->project->user, 'api') + ->json('POST', '/api/project/'.$balancer->project->id.'/balancer', [ + 'name' => 'main', + 'size' => '2GB', + ]); + + $response->assertStatus(422); + } + + + public function test_balancers_can_be_created() + { + Bus::fake(); + + $project = factory(Project::class)->create(); + + ServerProviderClientFactory::shouldReceive('make->sizes')->andReturn([ + '2GB' => [], + ]); + + ServerProviderClientFactory::shouldReceive('make->createServer') + ->with('main', '2GB', 'nyc3') + ->andReturn('123'); + + $response = $this->actingAs($project->user, 'api')->json('POST', '/api/project/'.$project->id.'/balancer', [ + 'name' => 'main', + 'size' => '2GB', + ]); + + Bus::assertDispatched(ProvisionBalancer::class); + + $this->assertCount(1, $project->balancers); + $this->assertEquals('main', $project->balancers[0]->name); + $this->assertEquals('2GB', $project->balancers[0]->size); + } + + + public function test_balancers_can_be_deleted() + { + Bus::fake(); + + $balancer = factory(Balancer::class)->create(); + $balancer->address()->save($address = factory(IpAddress::class)->make()); + + $balancer->project->balancers()->save(factory(Balancer::class)->make()); + + $response = $this->actingAs($balancer->project->user, 'api') + ->delete('/api/balancer/'.$balancer->id); + + $response->assertStatus(200); + $this->assertNull(Balancer::find($balancer->id)); + $this->assertNull(IpAddress::where('public_address', $address->public_address)->first()); + + Bus::assertDispatched(UpdateStackDnsRecords::class); + + Bus::assertDispatched(DeleteServerOnProvider::class, function ($job) use ($balancer) { + return $job->project->id == $balancer->project->id && + $job->providerServerId == $balancer->providerServerId(); + }); + } + + + public function test_balancers_cant_be_deleted_when_they_are_last_balancer_and_have_balanced_stacks() + { + Bus::fake(); + + $balancer = factory(Balancer::class)->create(); + $balancer->address()->save($address = factory(IpAddress::class)->make()); + + $project = $balancer->project; + $project->environments()->save($environment = factory(Environment::class)->make()); + + $environment->stacks()->save($stack = factory(Stack::class)->make(['balanced' => true])); + $stack->webServers()->save(factory(WebServer::class)->make()); + $stack->webServers()->save(factory(WebServer::class)->make()); + + $response = $this->withExceptionHandling()->actingAs($balancer->project->user, 'api') + ->json('DELETE', '/api/balancer/'.$balancer->id); + + $response->assertStatus(422); + + Bus::assertNotDispatched(DeleteServerOnProvider::class); + } +} diff --git a/tests/Feature/BuildJobTest.php b/tests/Feature/BuildJobTest.php new file mode 100644 index 00000000..dc4fe3c7 --- /dev/null +++ b/tests/Feature/BuildJobTest.php @@ -0,0 +1,58 @@ +withoutExceptionHandling(); + } + + + public function test_task_id_is_stored() + { + $serverDeployment = factory(ServerDeployment::class)->create(); + $serverDeployment->setRelation('deployable', $deployable = new BuildJobTestFakeDeployable); + + $job = new Build($serverDeployment); + + ShellProcessRunner::mock([ + (object) ['exitCode' => 0, 'output' => '', 'timedOut' => false], + ]); + + $job->handle(); + + $this->assertEquals(123, $serverDeployment->fresh()->build_task_id); + $this->assertInstanceOf(BuildScript::class, $deployable->script); + $this->assertInstanceOf(CheckBuild::class, $deployable->options['then'][0]); + } +} + + +class BuildJobTestFakeDeployable +{ + public $script; + public $options; + + public function runInBackground($script, $options) + { + $this->script = $script; + $this->options = $options; + + return (object) ['id' => 123]; + } +} diff --git a/tests/Feature/BuildScriptTest.php b/tests/Feature/BuildScriptTest.php new file mode 100644 index 00000000..7aebacf8 --- /dev/null +++ b/tests/Feature/BuildScriptTest.php @@ -0,0 +1,33 @@ +withoutExceptionHandling(); + } + + + public function test_script_can_be_rendered() + { + $deployment = factory(ServerDeployment::class)->create(); + + $deployment->deployable->createDaemonGeneration(); + + $script = new Build($deployment); + + $this->assertNotNull($script->script()); + } +} diff --git a/tests/Feature/CallbackControllerTest.php b/tests/Feature/CallbackControllerTest.php new file mode 100644 index 00000000..68cafe61 --- /dev/null +++ b/tests/Feature/CallbackControllerTest.php @@ -0,0 +1,107 @@ +withoutExceptionHandling(); + } + + + public function test_task_status_is_updated() + { + $task = factory(Task::class)->create(['status' => 'running']); + + ShellProcessRunner::shouldReceive('run')->andReturn((object) [ + 'exitCode' => 0, + 'output' => 'output', + 'timedOut' => false, + ]); + + $response = $this->get('/api/callback/'.hashid_encode($task->id)); + + $response->assertStatus(200); + $task = $task->fresh(); + $this->assertEquals(0, $task->exit_code); + $this->assertEquals('finished', $task->status); + } + + + public function test_404_is_returned_for_tasks_that_dont_exist() + { + $response = $this->withExceptionHandling()->get('/api/callback/no'); + $response->assertStatus(404); + + $response = $this->withExceptionHandling()->get('/api/callback/-1'); + $response->assertStatus(404); + + $response = $this->withExceptionHandling()->get('/api/callback/alsdkjadf10390'); + $response->assertStatus(404); + } + + + public function test_task_is_updated_with_exit_code_from_query_string() + { + $task = factory(Task::class)->create(['status' => 'running']); + + ShellProcessRunner::shouldReceive('run')->andReturn((object) [ + 'exitCode' => 0, + 'output' => 'output', + 'timedOut' => false, + ]); + + $response = $this->get('/api/callback/'.hashid_encode($task->id).'?exit_code=1'); + + $response->assertStatus(200); + $task = $task->fresh(); + $this->assertEquals(1, $task->exit_code); + $this->assertEquals('finished', $task->status); + } + + + public function test_callbacks_are_executed() + { + TestCallbackHandler::$called = false; + + $task = factory(Task::class)->create([ + 'status' => 'running', + 'options' => ['then' => [TestCallbackHandler::class]], + ]); + + ShellProcessRunner::shouldReceive('run')->andReturn((object) [ + 'exitCode' => 0, + 'output' => 'output', + 'timedOut' => false, + ]); + + $response = $this->get('/api/callback/'.hashid_encode($task->id)); + + $response->assertStatus(200); + $task = $task->fresh(); + $this->assertEquals('finished', $task->status); + $this->assertTrue(TestCallbackHandler::$called); + } +} + + +class TestCallbackHandler +{ + public static $called = false; + + public function handle(Task $task) + { + static::$called = true; + } +} diff --git a/tests/Feature/CheckActivationCallbackTest.php b/tests/Feature/CheckActivationCallbackTest.php new file mode 100644 index 00000000..24e61320 --- /dev/null +++ b/tests/Feature/CheckActivationCallbackTest.php @@ -0,0 +1,52 @@ +withoutExceptionHandling(); + } + + + public function test_deployment_status_is_properly_updated_if_successful() + { + $deployment = factory(ServerDeployment::class)->create(); + + $task = factory(Task::class)->create([ + 'exit_code' => 0, + ]); + + $handler = new CheckActivation($deployment->id); + $handler->handle($task); + + $this->assertEquals('activated', $deployment->fresh()->status); + } + + + public function test_deployment_status_is_properly_updated_if_failed() + { + $deployment = factory(ServerDeployment::class)->create(); + + $task = factory(Task::class)->create([ + 'exit_code' => 1, + ]); + + $handler = new CheckActivation($deployment->id); + $handler->handle($task); + + $this->assertEquals('failed', $deployment->fresh()->status); + } +} diff --git a/tests/Feature/CheckBuildCallbackTest.php b/tests/Feature/CheckBuildCallbackTest.php new file mode 100644 index 00000000..a34e690e --- /dev/null +++ b/tests/Feature/CheckBuildCallbackTest.php @@ -0,0 +1,52 @@ +withoutExceptionHandling(); + } + + + public function test_deployment_status_is_properly_updated_if_successful() + { + $deployment = factory(ServerDeployment::class)->create(); + + $task = factory(Task::class)->create([ + 'exit_code' => 0, + ]); + + $handler = new CheckBuild($deployment->id); + $handler->handle($task); + + $this->assertEquals('built', $deployment->fresh()->status); + } + + + public function test_deployment_status_is_properly_updated_if_failed() + { + $deployment = factory(ServerDeployment::class)->create(); + + $task = factory(Task::class)->create([ + 'exit_code' => 1, + ]); + + $handler = new CheckBuild($deployment->id); + $handler->handle($task); + + $this->assertEquals('failed', $deployment->fresh()->status); + } +} diff --git a/tests/Feature/CreateLoadBalancerIfNecessaryJobTest.php b/tests/Feature/CreateLoadBalancerIfNecessaryJobTest.php new file mode 100644 index 00000000..0989c1fd --- /dev/null +++ b/tests/Feature/CreateLoadBalancerIfNecessaryJobTest.php @@ -0,0 +1,109 @@ +withoutExceptionHandling(); + } + + + public function test_balancer_is_provisioned_if_multiple_servers_are_present_and_balancer_doesnt_exist() + { + $stack = factory(Stack::class)->create(); + + $stack->webServers()->save(factory(WebServer::class)->create(['size' => '2GB'])); + $stack->webServers()->save(factory(WebServer::class)->create(['size' => '2GB'])); + $stack->setRelation('environment', (object) ['project' => $fake = new CreateLoadBalancerIfNecessaryJobTestFakeProject]); + + $job = new CreateLoadBalancerIfNecessary($stack); + $job->handle(); + + $this->assertEquals('balancer', $fake->name); + $this->assertEquals('1GB', $fake->size); + $this->assertTrue($stack->balanced); + } + + + public function test_balancer_is_not_provisioned_if_a_balancer_already_exists() + { + $stack = factory(Stack::class)->create(); + + $stack->webServers()->save(factory(WebServer::class)->create(['size' => '2GB'])); + $fake = new CreateLoadBalancerIfNecessaryJobTestFakeProject; + $fake->balancers = collect([(object) []]); + $stack->setRelation('environment', (object) ['project' => $fake]); + + $job = new CreateLoadBalancerIfNecessary($stack); + $job->handle(); + + $this->assertNull($fake->name); + $this->assertNull($fake->size); + $this->assertTrue($stack->balanced); + } + + + public function test_balancer_is_not_provisioned_if_only_single_web_server() + { + $stack = factory(Stack::class)->create(); + + $stack->webServers()->save(factory(WebServer::class)->create(['size' => '2GB'])); + $stack->setRelation('environment', (object) ['project' => $fake = new CreateLoadBalancerIfNecessaryJobTestFakeProject]); + + $job = new CreateLoadBalancerIfNecessary($stack); + $job->handle(); + + $this->assertNull($fake->name); + $this->assertNull($fake->size); + $this->assertFalse($stack->balanced); + } + + + public function test_balancer_is_not_provisioned_if_only_single_app_server() + { + $stack = factory(Stack::class)->create(); + + $stack->appServers()->save(factory(AppServer::class)->create(['size' => '2GB'])); + $stack->setRelation('environment', (object) ['project' => $fake = new CreateLoadBalancerIfNecessaryJobTestFakeProject]); + + $job = new CreateLoadBalancerIfNecessary($stack); + $job->handle(); + + $this->assertNull($fake->name); + $this->assertNull($fake->size); + $this->assertFalse($stack->balanced); + } +} + + +class CreateLoadBalancerIfNecessaryJobTestFakeProject +{ + public $balancers; + public $name; + public $size; + + public function __construct() + { + $this->balancers = collect(); + } + + public function provisionBalancer($name, $size) + { + $this->name = $name; + $this->size = $size; + } +} diff --git a/tests/Feature/DaemonControllerTest.php b/tests/Feature/DaemonControllerTest.php new file mode 100644 index 00000000..224d9363 --- /dev/null +++ b/tests/Feature/DaemonControllerTest.php @@ -0,0 +1,110 @@ +withoutExceptionHandling(); + } + + + /** + * @dataProvider modifierProvider + */ + public function test_daemons_can_be_modified($action, $job, $status) + { + Bus::fake(); + + $stack = factory(Stack::class)->create(); + $stack->deployments()->save(factory(Deployment::class)->make()); + + $stack->deployments()->save($lastDeployment = factory(Deployment::class)->make([ + 'daemons' => ['first'] + ])); + + $lastDeployment->serverDeployments()->save( + $serverDeployment = factory(ServerDeployment::class)->make() + ); + + $response = $this->actingAs($stack->project()->user, 'api') + ->json('put', '/api/stack/'.$stack->id.'/daemons', [ + 'action' => $action, + ]); + + $response->assertStatus(200); + + $this->assertEquals($status, $serverDeployment->deployable->daemon_status); + + Bus::assertDispatched($job, function ($job) use ($serverDeployment) { + return $job->deployment->id === $serverDeployment->id; + }); + } + + + /** + * @dataProvider modifierProvider + */ + public function test_nothing_dispatched_if_no_daemons($action, $job) + { + Bus::fake(); + + $stack = factory(Stack::class)->create(); + $stack->deployments()->save(factory(Deployment::class)->make()); + $stack->deployments()->save($lastDeployment = factory(Deployment::class)->make()); + + $response = $this->actingAs($stack->project()->user, 'api') + ->json('put', '/api/stack/'.$stack->id.'/daemons', [ + 'action' => $action, + ]); + + $response->assertStatus(200); + + Bus::assertNotDispatched($job, function ($job) use ($lastDeployment) { + return $job->deployment->id === $lastDeployment->id; + }); + } + + + public function test_cant_modify_daemons_if_the_stack_has_no_deployments() + { + Bus::fake(); + + $stack = factory(Stack::class)->create(); + + $response = $this->withExceptionHandling()->actingAs($stack->project()->user, 'api') + ->json('put', '/api/stack/'.$stack->id.'/daemons', [ + 'action' => 'start', + ]); + + $response->assertStatus(422); + } + + + public function modifierProvider() + { + return [ + ['start', RestartDaemons::class, 'running'], + ['restart', RestartDaemons::class, 'running'], + ['pause', PauseDaemons::class, 'pending'], + ['unpause', UnpauseDaemons::class, 'running'], + ]; + } +} diff --git a/tests/Feature/DatabaseBackupControllerTest.php b/tests/Feature/DatabaseBackupControllerTest.php new file mode 100644 index 00000000..c8d3b73f --- /dev/null +++ b/tests/Feature/DatabaseBackupControllerTest.php @@ -0,0 +1,165 @@ +withoutExceptionHandling(); + } + + + public function test_backups_can_be_listed() + { + $backup = factory(DatabaseBackup::class)->create(); + + $response = $this->actingAs($backup->database->project->user, 'api')->json( + 'GET', "/api/database/{$backup->database->id}/backups" + ); + + $response->assertStatus(200); + $this->assertEquals($backup->id, $response->original['Test Database'][0]['id']); + + + // Filter by database... + $response = $this->actingAs($backup->database->project->user, 'api')->json( + 'GET', "/api/database/{$backup->database->id}/backups?database_name=Test+Database" + ); + + $response->assertStatus(200); + $this->assertEquals($backup->id, $response->original['Test Database'][0]['id']); + + + // Filter that doesn't exist... + $response = $this->actingAs($backup->database->project->user, 'api')->json( + 'GET', "/api/database/{$backup->database->id}/backups?database_name=doesnt-exist" + ); + + $response->assertStatus(200); + $this->assertCount(0, $response->original); + } + + + public function test_backup_can_be_created() + { + Bus::fake(); + + $database = factory(Database::class)->create(); + $database->project->user->storageProviders()->save( + $provider = factory(StorageProvider::class)->make() + ); + + $response = $this->actingAs($database->project->user, 'api')->json('POST', "/api/database/{$database->id}/backup", [ + 'storage_provider_id' => $provider->id, + 'database_name' => 'cloud', + ]); + + $response->assertStatus(201); + $this->assertCount(1, $database->backups); + $this->assertEquals('cloud', $database->backups[0]->database_name); + $this->assertEquals($provider->id, $database->backups[0]->storage_provider_id); + + Bus::assertDispatched(StoreDatabaseBackup::class, function ($job) use ($response) { + return $job->backup->id === $response->original->id; + }); + } + + + public function test_backup_cant_be_started_if_database_is_not_finished_provisioning() + { + Bus::fake(); + + $database = factory(Database::class)->create([ + 'status' => 'provisioning', + ]); + $database->project->user->storageProviders()->save( + $provider = factory(StorageProvider::class)->make() + ); + + $response = $this->withExceptionHandling()->actingAs($database->project->user, 'api') + ->json('POST', "/api/database/{$database->id}/backup", [ + 'storage_provider_id' => $provider->id, + 'database_name' => 'cloud', + ]); + + $response->assertStatus(422); + $this->assertCount(0, $database->backups); + + Bus::assertNotDispatched(StoreDatabaseBackup::class); + } + + + public function test_collaborators_can_manually_start_database_backups() + { + Bus::fake(); + + $database = factory(Database::class)->create(); + + $database->project->user->storageProviders()->save( + $provider = factory(StorageProvider::class)->make() + ); + + $user = $this->user(); + $user->storageProviders()->save($otherProvider = factory(StorageProvider::class)->make()); + + $database->project->shareWith($user); + + $response = $this->withExceptionHandling()->actingAs($user, 'api') + ->json('POST', "/api/database/{$database->id}/backup", [ + 'storage_provider_id' => $otherProvider->id, + 'database_name' => 'cloud', + ]); + + $response->assertStatus(201); + } + + + public function test_backups_can_be_deleted() + { + Bus::fake(); + + $backup = factory(DatabaseBackup::class)->create(); + + $response = $this->actingAs($backup->database->project->user, 'api')->json( + 'DELETE', "/api/backup/{$backup->id}" + ); + + $response->assertStatus(200); + + Bus::assertDispatched(DeleteDatabaseBackup::class); + } + + + public function test_only_project_owners_can_delete_backups() + { + Bus::fake(); + + $backup = factory(DatabaseBackup::class)->create(); + + $user = $this->user(); + $backup->database->project->shareWith($user); + + $response = $this->withExceptionHandling()->actingAs($user, 'api')->json( + 'DELETE', "/api/backup/{$backup->id}" + ); + + $response->assertStatus(403); + + Bus::assertNotDispatched(DeleteDatabaseBackup::class); + } +} diff --git a/tests/Feature/DatabaseBackupTest.php b/tests/Feature/DatabaseBackupTest.php new file mode 100644 index 00000000..864628b1 --- /dev/null +++ b/tests/Feature/DatabaseBackupTest.php @@ -0,0 +1,81 @@ +withoutExceptionHandling(); + } + + + public function test_mark_as_running() + { + Event::fake(); + + $backup = factory(DatabaseBackup::class)->create(); + $backup->markAsRunning(); + + $this->assertEquals('running', $backup->status); + + Event::assertDispatched(DatabaseBackupRunning::class); + } + + + public function test_mark_as_finished() + { + Event::fake(); + + $backup = factory(DatabaseBackup::class)->create(); + $backup->markAsFinished('output'); + + $this->assertEquals('finished', $backup->status); + $this->assertEquals('output', $backup->output); + + Event::assertDispatched(DatabaseBackupFinished::class); + } + + + public function test_mark_as_failed() + { + Event::fake(); + + $backup = factory(DatabaseBackup::class)->create(); + $backup->markAsFailed(1, 'output'); + + $this->assertEquals('failed', $backup->status); + $this->assertEquals(1, $backup->exit_code); + $this->assertEquals('output', $backup->output); + + Event::assertDispatched(DatabaseBackupFailed::class); + } + + + public function test_deleting_a_backup_dispatches_the_delete_job() + { + Bus::fake(); + + $backup = factory(DatabaseBackup::class)->create(); + $backup->delete(); + + Bus::assertDispatched(DeleteDatabaseBackup::class, function ($job) use ($backup) { + return $backup->storageProvider->id === $job->provider->id; + }); + } +} diff --git a/tests/Feature/DatabaseControllerTest.php b/tests/Feature/DatabaseControllerTest.php new file mode 100644 index 00000000..bf71a675 --- /dev/null +++ b/tests/Feature/DatabaseControllerTest.php @@ -0,0 +1,117 @@ +withoutExceptionHandling(); + } + + + public function test_databases_can_be_listed() + { + $database = factory(Database::class)->create(); + + $response = $this->actingAs($database->project->user, 'api') + ->get('/api/project/'.$database->project->id.'/databases'); + + $response->assertStatus(200); + $this->assertCount(1, $response->original); + $this->assertEquals($database->id, $response->original[0]->id); + } + + + public function test_duplicate_database_names_cant_be_created() + { + $database = factory(Database::class)->create(['name' => 'mysql']); + + $response = $this->withExceptionHandling() + ->actingAs($database->project->user, 'api') + ->json('POST', '/api/project/'.$database->project->id.'/database', [ + 'name' => 'mysql', + 'size' => '2GB', + ]); + + $response->assertStatus(422); + } + + + public function test_databases_can_be_created() + { + Bus::fake(); + + $project = factory(Project::class)->create(); + + ServerProviderClientFactory::shouldReceive('make->sizes')->andReturn([ + '2GB' => [], + ]); + + ServerProviderClientFactory::shouldReceive('make->createServer') + ->with('mysql', '2GB', 'nyc3') + ->andReturn('123'); + + $response = $this->actingAs($project->user, 'api')->json('POST', '/api/project/'.$project->id.'/database', [ + 'name' => 'mysql', + 'size' => '2GB', + ]); + + Bus::assertDispatched(ProvisionDatabase::class); + + $this->assertCount(1, $project->databases); + $this->assertEquals('mysql', $project->databases[0]->name); + $this->assertEquals('2GB', $project->databases[0]->size); + } + + + public function test_databases_can_be_deleted() + { + Bus::fake(); + + $database = factory(Database::class)->create(); + $database->address()->save($address = factory(IpAddress::class)->make()); + + $response = $this->actingAs($database->project->user, 'api') + ->delete('/api/database/'.$database->id); + + $response->assertStatus(200); + $this->assertNull(Database::find($database->id)); + $this->assertNull(IpAddress::where('public_address', $address->public_address)->first()); + + Bus::assertDispatched(DeleteServerOnProvider::class, function ($job) use ($database) { + return $job->project->id == $database->project->id && + $job->providerServerId == $database->providerServerId(); + }); + } + + + public function test_only_project_owners_can_delete_databases() + { + Bus::fake(); + + $database = factory(Database::class)->create(); + $database->address()->save($address = factory(IpAddress::class)->make()); + + $response = $this->withExceptionHandling()->actingAs($this->user(), 'api') + ->delete('/api/database/'.$database->id); + + $response->assertStatus(403); + Bus::assertNotDispatched(DeleteServerOnProvider::class); + } +} diff --git a/tests/Feature/DatabaseIsProvisionedRuleTest.php b/tests/Feature/DatabaseIsProvisionedRuleTest.php new file mode 100644 index 00000000..31e12038 --- /dev/null +++ b/tests/Feature/DatabaseIsProvisionedRuleTest.php @@ -0,0 +1,39 @@ +create(); + $rule = new DatabaseIsProvisioned($database->project); + $this->assertTrue($rule->passes('database', $database->name)); + } + + + public function test_rule_passes_when_database_doesnt_exist() + { + // Let this pass because valid name will catch this case... + $database = factory(Database::class)->create(); + $rule = new DatabaseIsProvisioned($database->project); + $this->assertTrue($rule->passes('database', 'missing')); + } + + + public function test_rule_fails_when_database_is_not_provisioned() + { + $database = factory(Database::class)->create(['status' => 'pending']); + $rule = new DatabaseIsProvisioned($database->project); + $this->assertFalse($rule->passes('database', $database->name)); + } +} diff --git a/tests/Feature/DatabaseRestoreControllerTest.php b/tests/Feature/DatabaseRestoreControllerTest.php new file mode 100644 index 00000000..93f005e8 --- /dev/null +++ b/tests/Feature/DatabaseRestoreControllerTest.php @@ -0,0 +1,123 @@ +withoutExceptionHandling(); + } + + + public function test_restores_can_be_listed() + { + $backup = factory(DatabaseBackup::class)->create(); + $backup->restores()->save($restore = factory(DatabaseRestore::class)->make([ + 'database_id' => $backup->database->id, + ])); + + $response = $this->actingAs($restore->database->project->user, 'api')->json( + 'GET', "/api/database/{$backup->database->id}/restores" + ); + + $response->assertStatus(200); + $this->assertEquals($restore->id, $response->original['Test Database'][0]['id']); + $this->assertInstanceOf(DatabaseRestore::class, $response->original['Test Database'][0]); + + // Filter by database... + $backup = factory(DatabaseBackup::class)->create(); + $backup->restores()->save($restore = factory(DatabaseRestore::class)->make([ + 'database_id' => $backup->database->id, + ])); + + $response = $this->actingAs($restore->database->project->user, 'api')->json( + 'GET', "/api/database/{$backup->database->id}/restores?database_name=Test+Database" + ); + + $response->assertStatus(200); + $this->assertEquals($restore->id, $response->original['Test Database'][0]['id']); + $this->assertInstanceOf(DatabaseRestore::class, $response->original['Test Database'][0]); + + // Filter that doesn't exist... + $backup = factory(DatabaseBackup::class)->create(); + $backup->restores()->save($restore = factory(DatabaseRestore::class)->make([ + 'database_id' => $backup->database->id, + ])); + + $response = $this->actingAs($restore->database->project->user, 'api')->json( + 'GET', "/api/database/{$backup->database->id}/restores?database_name=Missing" + ); + + $response->assertStatus(200); + $this->assertCount(0, $response->original); + } + + + public function test_restores_can_be_created() + { + Bus::fake(); + + $backup = factory(DatabaseBackup::class)->create(); + + $response = $this->actingAs($backup->database->project->user, 'api')->json('POST', "/api/backup/{$backup->id}/restore", [ + 'database_backup_id' => $backup->id, + ]); + + $response->assertStatus(201); + $this->assertCount(1, $backup->restores); + $this->assertEquals('Test Database', $backup->restores[0]->database_name); + + Bus::assertDispatched(RestoreDatabaseBackup::class, function ($job) use ($response) { + return $job->restore->id === $response->original->id; + }); + } + + + public function test_only_project_owners_can_restore_databases() + { + Bus::fake(); + + $backup = factory(DatabaseBackup::class)->create(); + $user = $this->user(); + $backup->database->project->shareWith($user); + + $response = $this->withExceptionHandling()->actingAs($user, 'api')->json('POST', "/api/backup/{$backup->id}/restore", [ + 'database_backup_id' => $backup->id, + ]); + + $response->assertStatus(403); + } + + + public function test_restores_cant_be_created_if_database_is_not_provisioned() + { + Bus::fake(); + + $backup = factory(DatabaseBackup::class)->create(); + $backup->database->update([ + 'status' => 'pending', + ]); + + $response = $this->withExceptionHandling()->actingAs($backup->database->project->user, 'api')->json('POST', "/api/backup/{$backup->id}/restore", [ + 'database_backup_id' => $backup->id, + ]); + + $response->assertStatus(422); + + Bus::assertNotDispatched(RestoreDatabaseBackup::class); + } +} diff --git a/tests/Feature/DatabaseTest.php b/tests/Feature/DatabaseTest.php new file mode 100644 index 00000000..212a5232 --- /dev/null +++ b/tests/Feature/DatabaseTest.php @@ -0,0 +1,55 @@ +withoutExceptionHandling(); + } + + + public function test_network_is_synced_reports_true_if_allows_access_from_all_stacks() + { + $appServer = factory(AppServer::class)->create(); + $appServer->address()->save(factory(IpAddress::class)->make()); + $database = factory(Database::class)->create(); + + $database->stacks()->sync([$appServer->stack->id]); + + $database->update([ + 'allows_access_from' => [ + $appServer->address->public_address, + $appServer->address->private_address + ], + ]); + + $this->assertTrue($database->networkIsSynced()); + } + + + + public function test_network_is_synced_reports_false_if_doesnt_allow_access_from_all_stacks() + { + $appServer = factory(AppServer::class)->create(); + $appServer->address()->save(factory(IpAddress::class)->make()); + $database = factory(Database::class)->create(); + + $database->stacks()->sync([$appServer->stack->id]); + + $this->assertFalse($database->networkIsSynced()); + } +} diff --git a/tests/Feature/DatabaseTransferControllerTest.php b/tests/Feature/DatabaseTransferControllerTest.php new file mode 100644 index 00000000..0e80aebe --- /dev/null +++ b/tests/Feature/DatabaseTransferControllerTest.php @@ -0,0 +1,91 @@ +withoutExceptionHandling(); + } + + + public function test_databases_can_be_transferred_to_another_project() + { + Bus::fake(); + + $database = factory(Database::class)->create(); + $project = $database->project; + + $anotherProject = factory(Project::class)->create([ + 'user_id' => $project->user->id + ]); + + $response = $this->actingAs( + $database->project->user, 'api' + )->json('post', '/api/database/'.$database->id.'/transfers', [ + 'project_id' => $anotherProject->id, + ]); + + $response->assertStatus(200); + + $this->assertFalse($project->fresh()->databases->contains($database)); + $this->assertTrue($anotherProject->fresh()->databases->contains($database)); + + Bus::assertDispatched(SyncNetwork::class); + } + + + public function test_databases_cant_be_transferred_to_another_users_project() + { + Bus::fake(); + + $database = factory(Database::class)->create(); + $project = $database->project; + + $anotherProject = factory(Project::class)->create(); + + $response = $this->withExceptionHandling()->actingAs( + $database->project->user, 'api' + )->json('post', '/api/database/'.$database->id.'/transfers', [ + 'project_id' => $anotherProject->id, + ]); + + $response->assertStatus(422); + } + + + public function test_databases_cant_be_transferred_without_permission() + { + Bus::fake(); + + $database = factory(Database::class)->create(); + $project = $database->project; + + $anotherProject = factory(Project::class)->create([ + 'user_id' => $project->user->id + ]); + + $project->shareWith($user = $this->user()); + + $response = $this->withExceptionHandling()->actingAs( + $user, 'api' + )->json('post', '/api/database/'.$database->id.'/transfers', [ + 'project_id' => $anotherProject->id, + ]); + + $response->assertStatus(403); + } +} diff --git a/tests/Feature/DeleteServerOnProviderJobTest.php b/tests/Feature/DeleteServerOnProviderJobTest.php new file mode 100644 index 00000000..d431bd69 --- /dev/null +++ b/tests/Feature/DeleteServerOnProviderJobTest.php @@ -0,0 +1,37 @@ +withoutExceptionHandling(); + } + + + public function test_server_is_deleted_using_provider() + { + $project = factory(Project::class)->create(); + + $job = new DeleteServerOnProvider($project, '123'); + + ServerProviderClientFactory::shouldReceive('make->deleteServerById') + ->with('123'); + + $job->handle(); + + $this->assertTrue(true); + } +} diff --git a/tests/Feature/DeploymentControllerTest.php b/tests/Feature/DeploymentControllerTest.php new file mode 100644 index 00000000..a689ebac --- /dev/null +++ b/tests/Feature/DeploymentControllerTest.php @@ -0,0 +1,212 @@ +withoutExceptionHandling(); + } + + + public function test_deployment_can_be_retrieved() + { + $deployment = factory(Deployment::class)->create(); + + $response = $this->actingAs( + $deployment->stack->environment->project->user, 'api' + )->get('/api/deployment/'.$deployment->id); + + $response->assertStatus(200); + + $response->assertJson([ + 'id' => $deployment->id, + ]); + } + + + public function test_deployment_can_be_created() + { + Bus::fake(); + + $stack = factory(Stack::class)->create([ + 'status' => 'provisioned', + ]); + + $response = $this->withExceptionHandling()->actingAs( + $stack->environment->project->user, 'api' + )->json('post', '/api/stack/'.$stack->id.'/deployment', [ + 'branch' => 'master', + 'build' => ['first', 'second'], + 'activate' => ['third', 'fourth'], + ]); + + $stack->deploymentLock()->release(); + + $response->assertStatus(201); + + $stack = $stack->fresh(); + $deployment = $stack->deployments->first(); + + $this->assertEquals($stack->environment->project->user->id, $deployment->initiator->id); + $this->assertNotNull('master', $deployment->commit_hash); + $this->assertNotNull('pending', $deployment->status); + $this->assertEquals(['first', 'second'], $deployment->build_commands); + $this->assertEquals(['third', 'fourth'], $deployment->activation_commands); + + Bus::assertDispatched(MonitorDeployment::class); + } + + + public function test_deployment_can_not_be_created_with_invalid_branch() + { + Bus::fake(); + + $stack = factory(Stack::class)->create([ + 'status' => 'provisioned', + ]); + + $response = $this->withExceptionHandling()->actingAs( + $stack->environment->project->user, 'api' + )->json('post', '/api/stack/'.$stack->id.'/deployment', [ + 'branch' => 'doesnt_exist', + ]); + + $stack->deploymentLock()->release(); + + $response->assertStatus(422); + } + + + public function test_deployment_can_be_created_via_hash() + { + Bus::fake(); + + $stack = factory(Stack::class)->create([ + 'status' => 'provisioned', + ]); + + $response = $this->actingAs( + $stack->environment->project->user, 'api' + )->json('post', '/api/stack/'.$stack->id.'/deployment', [ + 'hash' => '3b478197c05f0bb60ee484e01389bd2fff1d2bfc', + ]); + + $stack->deploymentLock()->release(); + + $response->assertStatus(201); + + $stack = $stack->fresh(); + $deployment = $stack->deployments->first(); + + $this->assertEquals('3b478197c05f0bb60ee484e01389bd2fff1d2bfc', $deployment->commit_hash); + $this->assertNotNull('pending', $deployment->status); + + Bus::assertDispatched(MonitorDeployment::class); + } + + + public function test_deployment_fails_if_stack_is_not_provisioned() + { + Bus::fake(); + + $stack = factory(Stack::class)->create([ + 'status' => 'provisioning', + ]); + + $response = $this->withExceptionHandling()->actingAs( + $stack->environment->project->user, 'api' + )->json('post', '/api/stack/'.$stack->id.'/deployment', [ + 'hash' => '3b478197c05f0bb60ee484e01389bd2fff1d2bfc', + ]); + + $stack->deploymentLock()->release(); + + $response->assertStatus(422); + } + + + public function test_deployment_fails_if_hash_is_invalid() + { + Bus::fake(); + + $stack = factory(Stack::class)->create([ + 'status' => 'provisioned', + ]); + + $response = $this->withExceptionHandling()->actingAs( + $stack->environment->project->user, 'api' + )->json('post', '/api/stack/'.$stack->id.'/deployment', [ + 'hash' => 'foo', + ]); + + $stack->deploymentLock()->release(); + + $response->assertStatus(422); + } + + + public function test_deployment_can_be_cancelled() + { + $deployment = factory(Deployment::class)->create(); + + $response = $this->actingAs( + $deployment->stack->project()->user, 'api' + )->json('delete', '/api/deployment/'.$deployment->id); + + $response->assertStatus(200); + } + + + public function test_latest_deployment_can_be_cancelled() + { + $deployment = factory(Deployment::class)->create(); + + $response = $this->actingAs( + $deployment->stack->project()->user, 'api' + )->json('delete', '/api/stack/'.$deployment->stack->id.'/deployment'); + + $response->assertStatus(200); + } + + + public function test_deployment_cant_be_cancelled_when_its_already_activating() + { + $deployment = factory(Deployment::class)->create([ + 'status' => 'activating', + ]); + + $response = $this->actingAs( + $deployment->stack->project()->user, 'api' + )->json('delete', '/api/deployment/'.$deployment->id); + + $response->assertStatus(400); + } + + + public function test_cant_cancel_deployment_for_stack_with_no_deployments() + { + $stack = factory(Stack::class)->create([ + 'status' => 'provisioned', + ]); + + $response = $this->actingAs( + $stack->project()->user, 'api' + )->json('delete', '/api/stack/'.$stack->id.'/deployment'); + + $response->assertStatus(400); + } +} diff --git a/tests/Feature/DeploymentTest.php b/tests/Feature/DeploymentTest.php new file mode 100644 index 00000000..c877a8b7 --- /dev/null +++ b/tests/Feature/DeploymentTest.php @@ -0,0 +1,266 @@ +withoutExceptionHandling(); + } + + + public function test_deployment_can_determine_if_built() + { + $deployment = factory(Deployment::class)->create(); + $deployment->serverDeployments()->save( + $serverDeployment = factory(ServerDeployment::class)->make() + ); + + $this->assertFalse($deployment->isBuilt()); + + $serverDeployment->update(['status' => 'built']); + + $this->assertTrue($deployment->fresh()->isBuilt()); + } + + + public function test_deployment_can_determine_if_activated() + { + $deployment = factory(Deployment::class)->create(); + $deployment->serverDeployments()->save( + $serverDeployment = factory(ServerDeployment::class)->make() + ); + + $this->assertFalse($deployment->isActivated()); + + $serverDeployment->update(['status' => 'activated']); + + $this->assertTrue($deployment->fresh()->isActivated()); + } + + + public function test_activate_method_dispatches_activate_jobs() + { + Bus::fake(); + + $deployment = factory(Deployment::class)->create(); + $deployment->serverDeployments()->save( + $serverDeployment = factory(ServerDeployment::class)->make() + ); + + $deployment->activate(); + + Bus::assertDispatched(Activate::class, function ($job) use ($serverDeployment) { + return $job->deployment->id === $serverDeployment->id; + }); + } + + + public function test_build_fans_out_deployment_commands_properly() + { + Bus::fake(); + + $deployment = factory(Deployment::class)->create([ + 'build_commands' => [ + 'php artisan all:build', + 'once: php artisan once:build', + 'web: php artisan web:build', + 'worker: php artisan worker:build', + ], + 'activation_commands' => [ + 'php artisan all:activate', + 'once: php artisan once:activate', + 'web: php artisan web:activate', + 'worker: php artisan worker:activate', + ], + ]); + + $deployment->stack->webServers()->save($web1 = factory(WebServer::class)->make()); + $deployment->stack->webServers()->save($web2 = factory(WebServer::class)->make()); + $deployment->stack->workerServers()->save($worker1 = factory(WorkerServer::class)->make()); + + $deployment->build(); + + // Check first web server commands... + $web1Deployment = ServerDeployment::where('deployable_type', WebServer::class) + ->where('deployable_id', $web1->id) + ->first(); + + $this->assertEquals([ + 'php artisan all:build', + 'php artisan once:build', + 'php artisan web:build', + ], $web1Deployment->build_commands); + + $this->assertEquals([ + 'php artisan all:activate', + 'php artisan once:activate', + 'php artisan web:activate', + ], $web1Deployment->activation_commands); + + // Check second web server commands... + $web2Deployment = ServerDeployment::where('deployable_type', WebServer::class) + ->where('deployable_id', $web2->id) + ->first(); + + $this->assertEquals([ + 'php artisan all:build', + 'php artisan web:build', + ], $web2Deployment->build_commands); + + $this->assertEquals([ + 'php artisan all:activate', + 'php artisan web:activate', + ], $web2Deployment->activation_commands); + + // Check worker server commands... + $worker1Deployment = ServerDeployment::where('deployable_type', WorkerServer::class) + ->where('deployable_id', $worker1->id) + ->first(); + + $this->assertEquals([ + 'php artisan all:build', + 'php artisan worker:build', + ], $worker1Deployment->build_commands); + + $this->assertEquals([ + 'php artisan all:activate', + 'php artisan worker:activate', + ], $worker1Deployment->activation_commands); + } + + + public function test_build_fans_out_deployment_commands_properly_with_app_server() + { + Bus::fake(); + + $deployment = factory(Deployment::class)->create([ + 'build_commands' => [ + 'php artisan all:build', + 'once: php artisan once:build', + 'php artisan web:build', + 'php artisan worker:build', + ], + 'activation_commands' => [ + 'php artisan all:activate', + 'once: php artisan once:activate', + 'php artisan web:activate', + 'php artisan worker:activate', + ], + ]); + + $deployment->stack->appServers()->save($app1 = factory(AppServer::class)->make()); + + $deployment->build(); + + // Check app server commands... + $app1Deployment = ServerDeployment::where('deployable_type', AppServer::class) + ->where('deployable_id', $app1->id) + ->first(); + + $this->assertEquals([ + 'php artisan all:build', + 'php artisan once:build', + 'php artisan web:build', + 'php artisan worker:build', + ], $app1Deployment->build_commands); + + $this->assertEquals([ + 'php artisan all:activate', + 'php artisan once:activate', + 'php artisan web:activate', + 'php artisan worker:activate', + ], $app1Deployment->activation_commands); + } + + + public function test_can_be_marked_as_finished() + { + Event::fake(); + + $deployment = factory(Deployment::class)->create(); + $deployment->markAsFinished(); + + $this->assertEquals('finished', $deployment->status); + + Event::assertDispatched(DeploymentFinished::class); + } + + + public function test_can_determine_if_activated() + { + $deployment = factory(Deployment::class)->create(); + + $deployment->serverDeployments()->save($serverDeployment = factory(ServerDeployment::class)->make([ + 'status' => 'activated', + ])); + + $this->assertTrue($deployment->fresh()->isActivated()); + + $deployment->serverDeployments()->save($serverDeployment = factory(ServerDeployment::class)->make([ + 'status' => 'building', + ])); + + $this->assertFalse($deployment->fresh()->isActivated()); + } + + + public function test_can_be_marked_as_timed_out() + { + Event::fake(); + + $deployment = factory(Deployment::class)->create(); + $deployment->markAsTimedOut(); + + $this->assertEquals('timeout', $deployment->status); + + Event::assertDispatched(DeploymentTimedOut::class); + } + + + public function test_can_determine_if_failures_exist() + { + $deployment = factory(Deployment::class)->create(); + + $this->assertFalse($deployment->hasFailures()); + + $deployment->serverDeployments()->save($serverDeployment = factory(ServerDeployment::class)->make([ + 'status' => 'failed', + ])); + + $this->assertTrue($deployment->fresh()->hasFailures()); + } + + + public function test_can_be_marked_as_failed() + { + Event::fake(); + + $deployment = factory(Deployment::class)->create(); + $deployment->markAsFailed(); + + $this->assertEquals('failed', $deployment->status); + + Event::assertDispatched(DeploymentFailed::class); + } +} diff --git a/tests/Feature/DigitalOceanProviderTest.php b/tests/Feature/DigitalOceanProviderTest.php new file mode 100644 index 00000000..b22fdfe5 --- /dev/null +++ b/tests/Feature/DigitalOceanProviderTest.php @@ -0,0 +1,56 @@ +withoutExceptionHandling(); + } + + + public function test_ssh_keys_can_be_added_and_removed() + { + $provider = factory(ServerProvider::class)->create(); + $this->refreshKeys($provider->user); + $ocean = new DigitalOcean($provider); + + $this->assertNull($ocean->findKey()); + + $id = $ocean->addKey(); + + $this->assertNotNull($id); + $this->assertNotNull($ocean->findKey()); + $this->assertEquals($id, $ocean->keyId()); + + $ocean->removeKey(); + + $this->assertNull($ocean->findKey()); + } + + + public function test_can_verify_credentials_are_valid() + { + $provider = factory(ServerProvider::class)->create(); + $this->refreshKeys($provider->user); + $ocean = new DigitalOcean($provider); + + $this->assertTrue($ocean->valid()); + + $provider = factory(ServerProvider::class)->create(['meta' => ['token' => 'foo']]); + $ocean = new DigitalOcean($provider); + + $this->assertFalse($ocean->valid()); + } +} diff --git a/tests/Feature/DispatchCallbackTest.php b/tests/Feature/DispatchCallbackTest.php new file mode 100644 index 00000000..9a938209 --- /dev/null +++ b/tests/Feature/DispatchCallbackTest.php @@ -0,0 +1,55 @@ +withoutExceptionHandling(); + + TestDispatchCallbackJob::$ran = false; + TestDispatchCallbackJob::$database = null; + } + + + public function test_proper_job_is_dispatched() + { + $task = factory(Task::class)->create(); + + $this->assertFalse(TestDispatchCallbackJob::$ran); + + $handler = new Dispatch(TestDispatchCallbackJob::class); + $handler->handle($task); + + $this->assertTrue(TestDispatchCallbackJob::$ran); + $this->assertEquals($task->provisionable->id, TestDispatchCallbackJob::$database->id); + } +} + + +class TestDispatchCallbackJob +{ + public static $database; + public static $ran = false; + + public function __construct($database) + { + static::$database = $database; + } + + public function handle() + { + static::$ran = true; + } +} diff --git a/tests/Feature/EnvironmentControllerTest.php b/tests/Feature/EnvironmentControllerTest.php new file mode 100644 index 00000000..18c35e12 --- /dev/null +++ b/tests/Feature/EnvironmentControllerTest.php @@ -0,0 +1,225 @@ +withoutExceptionHandling(); + } + + + public function test_environments_can_be_listed() + { + $environment = factory(Environment::class)->create(); + + $response = $this->actingAs($environment->project->user, 'api') + ->get('/api/project/'.$environment->project->id.'/environments'); + + $response->assertStatus(200); + $this->assertCount(1, $response->original); + $this->assertEquals($environment->id, $response->original[0]->id); + } + + + public function test_environment_variables_can_be_retrieved() + { + $environment = factory(Environment::class)->create([ + 'variables' => 'APP_DEBUG=true' + ]); + + $response = $this->actingAs($environment->project->user, 'api') + ->get('/api/environment/'.$environment->id); + + $response->assertStatus(200); + $this->assertEquals('APP_DEBUG=true', $response->original->variables); + } + + + public function test_environments_can_be_created() + { + $project = factory(Project::class)->create(); + + $response = $this->actingAs($project->user, 'api') + ->post('/api/project/'.$project->id.'/environment', [ + 'name' => 'Test Environment', + 'variables' => 'APP_DEBUG=true' + ]); + + $response->assertStatus(201); + $this->assertInstanceOf(Environment::class, $response->original); + } + + + public function test_duplicate_environment_names_can_not_be_created() + { + $environment = factory(Environment::class)->create([ + 'name' => 'Test Environment', + ]); + + $response = $this->withExceptionHandling()->actingAs($environment->project->user, 'api') + ->json('POST', '/api/project/'.$environment->project->id.'/environment', [ + 'name' => 'Test Environment', + 'variables' => 'APP_DEBUG=true' + ]); + + $response->assertStatus(422); + } + + + public function test_environments_can_be_updated() + { + $environment = factory(Environment::class)->create([ + 'name' => 'Test Environment', + 'variables' => 'APP_DEBUG=true', + ]); + + $response = $this->actingAs($environment->project->user, 'api') + ->json('PUT', '/api/environment/'.$environment->id, [ + 'variables' => 'APP_DEBUG=false', + ]); + + $response->assertStatus(200); + $this->assertEquals('APP_DEBUG=false', $environment->fresh()->variables); + } + + + public function test_non_collaborator_cant_update_environment() + { + $environment = factory(Environment::class)->create([ + 'name' => 'Test Environment', + 'variables' => 'APP_DEBUG=true', + ]); + + $user = $this->user(); + + $response = $this->withExceptionHandling()->actingAs($user, 'api') + ->json('PUT', '/api/environment/'.$environment->id, [ + 'variables' => 'APP_DEBUG=false', + ]); + + $response->assertStatus(403); + } + + + public function test_collaborator_can_update_environment() + { + $environment = factory(Environment::class)->create([ + 'name' => 'Test Environment', + 'variables' => 'APP_DEBUG=true', + ]); + + $user = $this->user(); + $environment->project->shareWith($user); + + $response = $this->withExceptionHandling()->actingAs($user, 'api') + ->json('PUT', '/api/environment/'.$environment->id, [ + 'variables' => 'APP_DEBUG=false', + ]); + + $response->assertStatus(200); + } + + + public function test_environments_can_be_deleted() + { + Bus::fake(); + + $server = factory(AppServer::class)->create(); + $server->stack->markAsProvisioned(); + $environment = $server->stack->environment; + + $response = $this->actingAs($environment->project->user, 'api')->json( + 'DELETE', '/api/environment/'.$environment->id + ); + + $response->assertStatus(200); + + $this->assertCount(0, Environment::all()); + $this->assertCount(0, Stack::all()); + $this->assertCount(0, AppServer::all()); + } + + + public function test_environments_cant_be_deleted_without_permission() + { + Bus::fake(); + + $server = factory(AppServer::class)->create(); + $server->stack->markAsProvisioned(); + $environment = $server->stack->environment; + + $user = $this->user(); + + $response = $this->withExceptionHandling()->actingAs($user, 'api')->json( + 'DELETE', '/api/environment/'.$environment->id + ); + + $response->assertStatus(403); + } + + + public function test_environments_can_always_be_deleted_by_their_creator() + { + Bus::fake(); + + $server = factory(AppServer::class)->create(); + $server->stack->markAsProvisioned(); + $environment = $server->stack->environment; + + $user = $this->user(); + $environment->update(['creator_id' => $user->id]); + + $response = $this->withExceptionHandling()->actingAs($user, 'api')->json( + 'DELETE', '/api/environment/'.$environment->id + ); + + $response->assertStatus(200); + } + + + public function test_environment_cant_be_deleted_if_their_stacks_are_provisioning() + { + Bus::fake(); + + $server = factory(AppServer::class)->create(); + $server->stack->update(['status' => 'provisioning']); + $environment = $server->stack->environment; + + $response = $this->withExceptionHandling()->actingAs($environment->project->user, 'api')->json( + 'DELETE', '/api/environment/'.$environment->id + ); + + $response->assertStatus(422); + } + + + public function test_environment_cant_be_deleted_if_their_stacks_are_deploying() + { + Bus::fake(); + + $server = factory(AppServer::class)->create(); + $server->stack->markAsDeploying(); + $environment = $server->stack->environment; + + $response = $this->withExceptionHandling()->actingAs($environment->project->user, 'api')->json( + 'DELETE', '/api/environment/'.$environment->id + ); + + $response->assertStatus(422); + } +} diff --git a/tests/Feature/EnvironmentTest.php b/tests/Feature/EnvironmentTest.php new file mode 100644 index 00000000..839f7d24 --- /dev/null +++ b/tests/Feature/EnvironmentTest.php @@ -0,0 +1,31 @@ +create([ + 'project_id' => 1, + ]); + + $environment->stacks()->save($stack = factory(Stack::class)->make([ + 'promoted' => true, + ])); + + $environment->stacks()->save(factory(Stack::class)->make([ + 'promoted' => false, + ])); + + $this->assertEquals($stack->id, $environment->promotedStack()->id); + } +} diff --git a/tests/Feature/GitHubTest.php b/tests/Feature/GitHubTest.php new file mode 100644 index 00000000..b9a1af45 --- /dev/null +++ b/tests/Feature/GitHubTest.php @@ -0,0 +1,115 @@ +withoutExceptionHandling(); + } + + + public function test_can_determine_if_credentials_are_valid() + { + $source = factory(SourceProvider::class)->create(); + $github = new GitHub($source); + + $this->assertTrue($github->valid()); + + $source = factory(SourceProvider::class)->create(['meta' => ['token' => 'foo']]); + $github = new GitHub($source); + + $this->assertFalse($github->valid()); + } + + + public function test_can_determine_if_repository_is_valid() + { + $source = factory(SourceProvider::class)->create(); + + $this->assertTrue($source->client()->validRepository('laravel/laravel', 'master')); + $this->assertFalse($source->client()->validRepository('laravel/laravel', 'fake-branch-that-doesnt-exist')); + $this->assertFalse($source->client()->validRepository('doesnt/exist', 'master')); + } + + + public function test_can_retrieve_latest_commit_hash() + { + $source = factory(SourceProvider::class)->create(); + $hash = $source->client()->latestHashFor('laravel/laravel', 'master'); + + $this->assertNotNull($hash); + } + + + public function test_can_get_deployment_url() + { + $source = factory(SourceProvider::class)->create(); + $url = $source->client()->tarballUrl($deployment = factory(Deployment::class)->create()); + + $this->assertNotNull($url); + } + + + public function test_hooks_can_be_published() + { + $hook = factory(Hook::class)->create([]); + + $source = factory(SourceProvider::class)->create(); + + $source->client()->publishHook($hook); + + $this->assertNotNull($hook->meta['provider_hook_id']); + + $source->client()->unpublishHook($hook); + } + + + public function test_hooks_can_be_added_twice_without_errors() + { + $hook = factory(Hook::class)->create([]); + + $source = factory(SourceProvider::class)->create(); + + $source->client()->publishHook($hook); + $source->client()->publishHook($hook); + $source->client()->unpublishHook($hook); + + $this->assertTrue(true); + } + + + public function test_manifest_can_be_retrieved() + { + $source = factory(SourceProvider::class)->create(); + + $stack = factory(Stack::class)->create(['name' => 'stack-1']); + $stack->environment->update(['name' => 'workbench']); + + $manifest = $source->client()->manifest( + $stack, + 'taylorotwell/hello-world', + 'd8f05f1696032982dd8bf77aa9186d2aea744801' + ); + + $this->assertNotNull($manifest); + $manifest = Yaml::parse($manifest); + $this->assertEquals('Personal', $manifest['source']); + } +} diff --git a/tests/Feature/HandlesStackProvisioningFailuresTest.php b/tests/Feature/HandlesStackProvisioningFailuresTest.php new file mode 100644 index 00000000..e99d759f --- /dev/null +++ b/tests/Feature/HandlesStackProvisioningFailuresTest.php @@ -0,0 +1,45 @@ +withoutExceptionHandling(); + } + + + public function test_stack_is_deleted_and_alert_created() + { + $stack = new HandlesStackProvisioningFailuresTestFakeStack; + $stack->environment()->associate(factory(Environment::class)->create()); + $job = new CreateLoadBalancerIfNecessary($stack); + $job->failed(new Exception); + + $this->assertTrue($stack->wasDeleted); + } +} + + +class HandlesStackProvisioningFailuresTestFakeStack extends Stack +{ + public $wasDeleted = false; + + public function delete() + { + $this->wasDeleted = true; + } +} diff --git a/tests/Feature/HookControllerTest.php b/tests/Feature/HookControllerTest.php new file mode 100644 index 00000000..cb7508c6 --- /dev/null +++ b/tests/Feature/HookControllerTest.php @@ -0,0 +1,142 @@ +withoutExceptionHandling(); + } + + + public function test_hook_can_be_created() + { + Bus::fake(); + + SourceProviderClientFactory::shouldReceive('make')->andReturn( + $client = Mockery::mock() + ); + + $client->shouldReceive('validRepository')->with( + 'taylorotwell/hello-world', 'master' + )->andReturn(true); + + $client->shouldReceive('publishHook')->once(); + + $stack = factory(Stack::class)->create([ + 'status' => 'provisioned', + ]); + + $response = $this->actingAs( + $stack->environment->project->user, 'api' + )->json('post', '/api/stack/'.$stack->id.'/hook', [ + 'name' => 'Test', + 'branch' => 'master', + 'publish' => true, + ]); + + $response->assertStatus(201); + + $stack = $stack->fresh(); + $hook = $stack->hooks->first(); + + $this->assertEquals('taylorotwell/hello-world', $hook->stack->project()->repository); + $this->assertEquals('master', $hook->branch); + } + + + public function test_hook_can_be_created_without_publishing_to_the_source_control_provider() + { + Bus::fake(); + + SourceProviderClientFactory::shouldReceive('make')->andReturn( + $client = Mockery::mock() + ); + + $client->shouldReceive('validRepository')->with( + 'taylorotwell/hello-world', 'master' + )->andReturn(true); + + $client->shouldReceive('publishHook')->never(); + + $stack = factory(Stack::class)->create([ + 'status' => 'provisioned', + ]); + + $response = $this->actingAs( + $stack->environment->project->user, 'api' + )->json('post', '/api/stack/'.$stack->id.'/hook', [ + 'name' => 'Test', + 'branch' => 'master', + 'publish' => false, + ]); + + $response->assertStatus(201); + } + + + public function test_can_not_be_created_with_invalid_branch() + { + Bus::fake(); + + $stack = factory(Stack::class)->create([ + 'status' => 'provisioned', + ]); + + $response = $this->withExceptionHandling()->actingAs( + $stack->environment->project->user, 'api' + )->json('post', '/api/stack/'.$stack->id.'/hook', [ + 'name' => 'Test', + 'branch' => 'does-not-exist', + 'publish' => true, + ]); + + $response->assertStatus(422); + } + + + public function test_hook_can_be_deleted() + { + Bus::fake(); + + $hook = tap(factory(Hook::class)->create([]))->publish(); + + $response = $this->actingAs( + $hook->stack->environment->project->user, 'api' + )->json('delete', '/api/hook/'.$hook->id); + + $response->assertStatus(200); + + $this->assertCount(0, $hook->stack->fresh()->hooks); + } + + + public function test_cant_delete_the_hook_if_not_a_collaborator() + { + Bus::fake(); + + $hook = factory(Hook::class)->create(); + + $response = $this->withExceptionHandling()->actingAs( + $this->user(), 'api' + )->json('delete', '/api/hook/'.$hook->id); + + $response->assertStatus(403); + + $this->assertCount(1, $hook->stack->fresh()->hooks); + } +} diff --git a/tests/Feature/HookDeploymentControllerTest.php b/tests/Feature/HookDeploymentControllerTest.php new file mode 100644 index 00000000..8041ac9f --- /dev/null +++ b/tests/Feature/HookDeploymentControllerTest.php @@ -0,0 +1,239 @@ +withoutExceptionHandling(); + } + + + public function test_deployment_can_be_created_from_hook_payload() + { + Bus::fake(); + + $hook = factory(Hook::class)->create(); + + $hook->stack->deploymentLock()->release(); + + $hook->stack->environment->update([ + 'name' => 'workbench', + ]); + + $hook->stack->update([ + 'name' => 'stack-1', + ]); + + $response = $this->json('post', '/api/hook-deployment/'.$hook->id.'/'.$hook->token, [ + 'ref' => 'refs/heads/'.$hook->branch, + 'head_commit' => [ + 'id' => 'd8f05f1696032982dd8bf77aa9186d2aea744801', + ], + 'repository' => [ + 'full_name' => 'taylorotwell/hello-world', + ], + ]); + + $response->assertStatus(201); + $this->assertInstanceOf(Deployment::class, $response->original); + + $this->assertNull($response->original->branch); + $this->assertEquals('d8f05f1696032982dd8bf77aa9186d2aea744801', $response->original->commit_hash); + } + + + public function test_deployment_can_be_created_from_codeship_payloads() + { + Bus::fake(); + + $hook = factory(Hook::class)->create(['published' => false]); + + $hook->stack->deploymentLock()->release(); + + $hook->stack->environment->update([ + 'name' => 'workbench', + ]); + + $hook->stack->update([ + 'name' => 'stack-1', + ]); + + $response = $this->withHeaders([ + 'User-Agent' => 'Codeship Webhook', + ])->json('post', '/api/hook-deployment/'.$hook->id.'/'.$hook->token, [ + 'build' => [ + 'status' => 'success', + 'commit_id' => 'd8f05f1696032982dd8bf77aa9186d2aea744801', + ], + ]); + + $response->assertStatus(201); + $this->assertInstanceOf(Deployment::class, $response->original); + + $this->assertNull($response->original->branch); + $this->assertEquals('d8f05f1696032982dd8bf77aa9186d2aea744801', $response->original->commit_hash); + } + + + public function test_deployment_can_be_created_for_latest_commit() + { + Bus::fake(); + + $hook = factory(Hook::class)->create(); + + $hook->stack->deploymentLock()->release(); + + $hook->stack->environment->update([ + 'name' => 'workbench', + ]); + + $hook->stack->update([ + 'name' => 'stack-1', + ]); + + $response = $this->json('post', '/api/hook-deployment/'.$hook->id.'/'.$hook->token, [ + 'ref' => 'refs/heads/'.$hook->branch, + 'repository' => [ + 'full_name' => 'taylorotwell/hello-world', + ], + ]); + + $response->assertStatus(201); + $this->assertInstanceOf(Deployment::class, $response->original); + + $this->assertNull($response->original->branch); + + $latestCommit = $hook->sourceProvider()->client()->latestHashFor('taylorotwell/hello-world', 'master'); + $this->assertEquals($latestCommit, $response->original->commit_hash); + } + + + public function test_request_token_must_match_hook_token() + { + Bus::fake(); + + $hook = factory(Hook::class)->create(); + + $hook->stack->deploymentLock()->release(); + + $response = $this->withExceptionHandling()->json('post', '/api/hook-deployment/'.$hook->id.'/token', [ + 'ref' => 'refs/heads/'.$hook->branch, + 'head_commit' => [ + 'id' => '3b478197c05f0bb60ee484e01389bd2fff1d2bfc', + ], + 'repository' => [ + 'full_name' => 'taylorotwell/hello-world', + ], + ]); + + $response->assertStatus(403); + } + + + public function test_branch_must_be_received_by_hook() + { + Bus::fake(); + + $hook = factory(Hook::class)->create(['published' => true]); + + $hook->stack->deploymentLock()->release(); + + $response = $this->withExceptionHandling()->json('post', '/api/hook-deployment/'.$hook->id.'/'.$hook->token, [ + 'ref' => 'refs/heads/something', + 'head_commit' => [ + 'id' => '3b478197c05f0bb60ee484e01389bd2fff1d2bfc', + ], + 'repository' => [ + 'full_name' => 'taylorotwell/hello-world', + ], + ]); + + $response->assertStatus(204); + $this->assertNull($response->original); + } + + + public function test_irrelevant_codeship_hooks_are_discarded() + { + Bus::fake(); + + $hook = factory(Hook::class)->create(['published' => false]); + + $hook->stack->deploymentLock()->release(); + + $response = $this->withExceptionHandling()->withHeaders([ + 'User-Agent' => 'Codeship Webhook', + ])->json('post', '/api/hook-deployment/'.$hook->id.'/'.$hook->token, [ + 'build' => [ + 'status' => 'failed', + 'commit_id' => 'something', + ], + ]); + + $response->assertStatus(204); + $this->assertNull($response->original); + } + + + public function test_hook_always_receives_commits_if_it_is_not_published() + { + Bus::fake(); + + $hook = factory(Hook::class)->create(['published' => false]); + + $hook->stack->deploymentLock()->release(); + + $hook->stack->environment->update([ + 'name' => 'workbench', + ]); + + $hook->stack->update([ + 'name' => 'stack-1', + ]); + + $response = $this->json('post', '/api/hook-deployment/'.$hook->id.'/'.$hook->token, [ + 'ref' => 'refs/heads/something', + 'repository' => [ + 'full_name' => 'taylorotwell/hello-world', + ], + ]); + + $response->assertStatus(201); + } + + + public function test_404_is_returned_if_manifest_is_not_found() + { + Bus::fake(); + + $hook = factory(Hook::class)->create(); + + $hook->stack->deploymentLock()->release(); + + $response = $this->withExceptionHandling()->json('post', '/api/hook-deployment/'.$hook->id.'/'.$hook->token, [ + 'ref' => 'refs/heads/'.$hook->branch, + 'head_commit' => [ + 'id' => '3b478197c05f0bb60ee484e01389bd2fff1d2bfc', + ], + 'repository' => [ + 'full_name' => 'taylorotwell/hello-world', + ], + ]); + + $response->assertStatus(404); + } +} diff --git a/tests/Feature/KeyControllerTest.php b/tests/Feature/KeyControllerTest.php new file mode 100644 index 00000000..b18703a9 --- /dev/null +++ b/tests/Feature/KeyControllerTest.php @@ -0,0 +1,119 @@ +withoutExceptionHandling(); + } + + + public function test_key_can_be_retrieved() + { + Bus::fake(); + + $ipAddress = factory(IpAddress::class)->create(); + + ShellProcessRunner::mock([ + (object) ['exitCode' => 0, 'output' => '', 'timedOut' => false], + ]); + + $response = $this->actingAs($ipAddress->addressable->project->user, 'api') + ->json('post', '/api/key/'.$ipAddress->public_address); + + $response->assertStatus(200); + + Bus::assertDispatched(RemoveKeyFromServer::class); + } + + + public function test_key_is_shared_with_authorized_users() + { + Bus::fake(); + + $user = factory(User::class)->create(); + $ipAddress = factory(IpAddress::class)->create(); + $ipAddress->addressable->project->shareWith($user, ['ssh:database']); + + ShellProcessRunner::mock([ + (object) ['exitCode' => 0, 'output' => '', 'timedOut' => false], + ]); + + $response = $this->withExceptionHandling()->actingAs($user, 'api') + ->json('post', '/api/key/'.$ipAddress->public_address); + + $response->assertStatus(200); + } + + + public function test_key_is_not_shared_with_unauthorized_users() + { + $user = factory(User::class)->create(); + $ipAddress = factory(IpAddress::class)->create(); + + $response = $this->withExceptionHandling()->actingAs($user, 'api') + ->json('post', '/api/key/'.$ipAddress->public_address); + + $response->assertStatus(403); + } + + + public function test_server_keys_can_be_shared_with_collaborators() + { + Bus::fake(); + + $user = factory(User::class)->create(); + $ipAddress = factory(IpAddress::class)->create(); + + $ipAddress->addressable->project->shareWith($user); + + ShellProcessRunner::mock([ + (object) ['exitCode' => 0, 'output' => '', 'timedOut' => false], + ]); + + $response = $this->actingAs($ipAddress->addressable->project->user, 'api') + ->json('post', '/api/key/'.$ipAddress->public_address); + + $response->assertStatus(200); + + Bus::assertDispatched(RemoveKeyFromServer::class); + } + + + public function test_balancer_keys_can_be_shared_with_collaborators() + { + $user = factory(User::class)->create(); + $server = factory(Balancer::class)->create(); + + $server->address()->save($ipAddress = factory(IpAddress::class)->make()); + + $server->project->shareWith($user); + + ShellProcessRunner::mock([ + (object) ['exitCode' => 0, 'output' => '', 'timedOut' => false], + ]); + + $response = $this->withExceptionHandling()->actingAs($user, 'api') + ->json('post', '/api/key/'.$ipAddress->public_address); + + $response->assertStatus(200); + } +} diff --git a/tests/Feature/MarkAsProvisionedCallbackTest.php b/tests/Feature/MarkAsProvisionedCallbackTest.php new file mode 100644 index 00000000..e6bde80f --- /dev/null +++ b/tests/Feature/MarkAsProvisionedCallbackTest.php @@ -0,0 +1,50 @@ +withoutExceptionHandling(); + } + + + public function test_provisionable_is_marked_as_provisioned() + { + $database = factory(Database::class)->create([ + 'status' => 'provisioning' + ]); + + $database->tasks()->save($task = factory(Task::class)->create()); + + $handler = new MarkAsProvisioned; + $handler->handle($task); + + $this->assertEquals('provisioned', $database->fresh()->status); + } + + + public function test_can_be_called_for_models_that_dont_exist_without_errors() + { + $task = factory(Task::class)->create([ + 'options' => ['type' => Database::class, 'id' => 1000], + ]); + + $handler = new MarkAsProvisioned; + $handler->handle($task); + + $this->assertTrue(true); + } +} diff --git a/tests/Feature/MonitorDeploymentJobTest.php b/tests/Feature/MonitorDeploymentJobTest.php new file mode 100644 index 00000000..330588dd --- /dev/null +++ b/tests/Feature/MonitorDeploymentJobTest.php @@ -0,0 +1,139 @@ +withoutExceptionHandling(); + } + + + public function test_marked_as_finished_if_activated() + { + $deployment = factory(Deployment::class)->create(); + + $deployment->serverDeployments()->save($serverDeployment = factory(ServerDeployment::class)->make([ + 'status' => 'activated', + ])); + + $job = new FakeMonitorDeploymentJob($deployment); + + $job->handle(); + + $this->assertTrue($job->deleted); + $this->assertTrue($deployment->isFinished()); + } + + + public function test_marked_as_failed_if_has_failures() + { + $deployment = factory(Deployment::class)->create(); + + $deployment->serverDeployments()->save($serverDeployment = factory(ServerDeployment::class)->make([ + 'status' => 'failed', + ])); + + $job = new FakeMonitorDeploymentJob($deployment); + + $job->handle(); + + $this->assertTrue($job->deleted); + $this->assertEquals('failed', $deployment->status); + } + + + public function test_marked_as_timed_out_if_old() + { + $deployment = factory(Deployment::class)->create([ + 'created_at' => Carbon::now()->subMinutes(40), + ]); + + $deployment->serverDeployments()->save($serverDeployment = factory(ServerDeployment::class)->make([ + 'status' => 'activating', + ])); + + $job = new FakeMonitorDeploymentJob($deployment); + + $job->handle(); + + $this->assertTrue($job->deleted); + $this->assertEquals('timeout', $deployment->status); + } + + + public function test_activated_if_built() + { + Bus::fake(); + + $deployment = factory(Deployment::class)->create(); + + $deployment->serverDeployments()->save($serverDeployment = factory(ServerDeployment::class)->make([ + 'status' => 'built', + ])); + + $job = new FakeMonitorDeploymentJob($deployment); + + $job->handle(); + + $this->assertEquals(5, $job->released); + $this->assertEquals('activating', $deployment->status); + $this->assertTrue($deployment->activated); + + Bus::assertDispatched(Activate::class, function ($job) use ($serverDeployment) { + return $job->deployment->id === $serverDeployment->id; + }); + } + + + public function test_failed_method_marks_as_failed() + { + $deployment = factory(Deployment::class)->create(); + + $job = new FakeMonitorDeploymentJob($deployment); + + $job->failed(new Exception); + + $this->assertEquals('failed', $deployment->status); + $this->assertCount(1, $deployment->project()->alerts); + } +} + + +class FakeMonitorDeploymentJob extends MonitorDeployment +{ + public $released; + public $deleted = false; + public $failed; + + public function release($delay = 0) + { + $this->released = $delay; + } + + public function delete() + { + $this->deleted = true; + } + + public function fail($exception = null) + { + $this->failed = $exception; + } +} diff --git a/tests/Feature/ProjectControllerTest.php b/tests/Feature/ProjectControllerTest.php new file mode 100644 index 00000000..9860bc56 --- /dev/null +++ b/tests/Feature/ProjectControllerTest.php @@ -0,0 +1,170 @@ +withoutExceptionHandling(); + } + + + public function test_projects_can_be_listed() + { + $project = factory(Project::class)->create(); + $project->user->projects()->save(factory(Project::class)->make()); + $project->user->projects()->save(factory(Project::class)->make([ + 'archived' => true, + ])); + + $response = $this->actingAs($project->user, 'api')->json('GET', '/api/projects'); + + $response->assertStatus(200); + $this->assertCount(2, $response->original); + } + + + public function test_no_database_is_created_if_no_database_is_specified() + { + Bus::fake(); + + $provider = factory(ServerProvider::class)->create(); + $provider->user->sourceProviders()->save($source = factory(SourceProvider::class)->make()); + + ServerProviderClientFactory::shouldReceive('make->createServer')->never(); + + ServerProviderClientFactory::shouldReceive('make->regions')->andReturn([ + 'nyc3' => 'New York 3', + ]); + + $response = $this->actingAs($provider->user, 'api')->json('POST', '/api/project', [ + 'name' => 'Laravel', + 'server_provider_id' => $provider->id, + 'region' => 'nyc3', + 'source_provider_id' => $source->id, + 'repository' => 'taylorotwell/hello-world', + ]); + + $response->assertStatus(201); + + Bus::assertNotDispatched(ProvisionDatabase::class); + + $project = $provider->user->projects()->first(); + $this->assertCount(0, $project->databases); + $this->assertEquals('nyc3', $project->region); + $this->assertEquals('Laravel', $project->name); + } + + + public function test_job_to_provision_database_server_is_dispatched() + { + Bus::fake(); + + $provider = factory(ServerProvider::class)->create(); + $provider->user->sourceProviders()->save($source = factory(SourceProvider::class)->make()); + + ServerProviderClientFactory::shouldReceive('make->createServer')->andReturn(123); + + ServerProviderClientFactory::shouldReceive('make->regions')->andReturn([ + 'nyc3' => 'New York 3', + ]); + + ServerProviderClientFactory::shouldReceive('make->sizes')->andReturn([ + '2GB' => '', + ]); + + $response = $this->actingAs($provider->user, 'api')->json('POST', '/api/project', [ + 'name' => 'Laravel', + 'server_provider_id' => $provider->id, + 'region' => 'nyc3', + 'source_provider_id' => $source->id, + 'repository' => 'taylorotwell/hello-world', + 'database' => 'mysql', + 'database_size' => '2GB', + ]); + + $response->assertStatus(201); + + Bus::assertDispatched(ProvisionDatabase::class); + + $project = $provider->user->projects()->first(); + $this->assertCount(1, $project->databases); + $this->assertEquals('nyc3', $project->region); + $this->assertEquals('Laravel', $project->name); + $this->assertEquals('mysql', $project->databases->first()->name); + $this->assertEquals('2GB', $project->databases->first()->size); + $this->assertEquals(123, $project->databases->first()->provider_server_id); + } + + + public function test_projects_can_be_deleted() + { + Bus::fake(); + + $server = factory(AppServer::class)->create(); + $server->stack->markAsProvisioned(); + $project = $server->stack->project(); + $project->balancers()->save(factory(Balancer::class)->make()); + $project->databases()->save(factory(Database::class)->make()); + + $response = $this->actingAs($project->user, 'api')->json('DELETE', '/api/project/'.$project->id); + + $response->assertStatus(200); + + $this->assertCount(0, AppServer::all()); + $this->assertCount(0, Stack::all()); + $this->assertCount(0, Balancer::all()); + $this->assertCount(0, Database::all()); + + $this->assertTrue($project->fresh()->archived); + } + + + public function test_project_cant_be_deleted_if_stacks_are_provisioning() + { + Bus::fake(); + + $server = factory(AppServer::class)->create(); + $server->stack->update(['status' => 'provisioning']); + $project = $server->stack->project(); + + $response = $this->withExceptionHandling()->actingAs($project->user, 'api') + ->json('DELETE', '/api/project/'.$project->id); + + $response->assertStatus(422); + } + + + public function test_project_cant_be_deleted_if_stacks_are_deploying() + { + Bus::fake(); + + $server = factory(AppServer::class)->create(); + $server->stack->markAsDeploying(); + $project = $server->stack->project(); + + $response = $this->withExceptionHandling()->actingAs($project->user, 'api') + ->json('DELETE', '/api/project/'.$project->id); + + $response->assertStatus(422); + } +} diff --git a/tests/Feature/ProjectTest.php b/tests/Feature/ProjectTest.php new file mode 100644 index 00000000..8bc7b68d --- /dev/null +++ b/tests/Feature/ProjectTest.php @@ -0,0 +1,69 @@ +user(); + $anotherUser = $this->user(); + + $anotherUser->projects()->save($project = factory(Project::class)->create()); + $this->assertFalse($user->canAccessProject($project)); + $this->assertTrue($anotherUser->canAccessProject($project)); + + $project->shareWith($user, ['ssh:server']); + + $this->assertTrue($user->fresh()->canAccessProject($project)); + $this->assertTrue($anotherUser->fresh()->canAccessProject($project)); + + $project->stopSharingWith($user); + + $this->assertFalse($user->fresh()->canAccessProject($project)); + $this->assertTrue($anotherUser->fresh()->canAccessProject($project)); + } + + + public function test_proper_share_events_are_fired() + { + Event::fake(); + + $user = $this->user(); + $anotherUser = $this->user(); + + $anotherUser->projects()->save($project = factory(Project::class)->create()); + + $project->shareWith($user); + + Event::assertDispatched(ProjectShared::class, function ($event) use ($user) { + return $event->user->id === $user->id; + }); + } + + + public function test_project_can_only_have_30_alerts() + { + $project = factory(Project::class)->create(); + + for ($i = 0; $i < 40; $i++) { + $project->alerts()->create([ + 'type' => 'Test', + 'exception' => '', + 'meta' => [], + ]); + } + + $this->assertEquals(30, $project->alerts()->count()); + } +} diff --git a/tests/Feature/PromoteStackJobTest.php b/tests/Feature/PromoteStackJobTest.php new file mode 100644 index 00000000..32951b79 --- /dev/null +++ b/tests/Feature/PromoteStackJobTest.php @@ -0,0 +1,179 @@ +withoutExceptionHandling(); + } + + + public function test_stack_is_promoted_and_proper_jobs_are_dispatched() + { + Bus::fake(); + + $previousStack = factory(Stack::class)->create(['promoted' => true]); + $previousStack->appServers()->save($previousServer = factory(AppServer::class)->make()); + + $previousStack->deployments()->save($previousDeployment = factory(Deployment::class)->make([ + 'daemons' => ['first'], + 'schedule' => ['first'] + ])); + + $previousDeployment->serverDeployments()->save( + $previousServerDeployment = factory(ServerDeployment::class)->make([ + 'deployable_id' => $previousServer->id, + 'deployable_type' => get_class($previousServer), + ]) + ); + + $stack = factory(Stack::class)->create(); + $stack->appServers()->save($server = factory(AppServer::class)->make()); + + $stack->deployments()->save($deployment = factory(Deployment::class)->make([ + 'daemons' => ['first'], + 'schedule' => ['first'] + ])); + + $deployment->serverDeployments()->save( + $serverDeployment = factory(ServerDeployment::class)->make([ + 'deployable_id' => $server->id, + 'deployable_type' => get_class($server), + ]) + ); + + $previousStack->update([ + 'environment_id' => $stack->environment_id, + ]); + + $job = new PromoteStack($stack); + $job->handle(); + + Bus::assertDispatched(StartScheduler::class, function ($job) use ($serverDeployment) { + return $job->deployment->id === $serverDeployment->id; + }); + + Bus::assertDispatched(RestartDaemons::class, function ($job) use ($serverDeployment) { + return $job->deployment->id === $serverDeployment->id; + }); + + Bus::assertDispatched(StopScheduler::class, function ($job) use ($previousServerDeployment) { + return $job->deployment->id === $previousServerDeployment->id; + }); + + Bus::assertDispatched(StopDaemons::class, function ($job) use ($previousServerDeployment) { + return $job->deployment->id === $previousServerDeployment->id; + }); + } + + + public function test_background_services_arent_started_if_instructed_to_wait() + { + Bus::fake(); + + $previousStack = factory(Stack::class)->create(['promoted' => true]); + $previousStack->appServers()->save($previousServer = factory(AppServer::class)->make()); + $previousStack->deployments()->save($previousDeployment = factory(Deployment::class)->make([ + 'daemons' => ['first'] + ])); + $previousDeployment->serverDeployments()->save( + $previousServerDeployment = factory(ServerDeployment::class)->make([ + 'deployable_id' => $previousServer->id, + 'deployable_type' => get_class($previousServer), + ]) + ); + + $stack = factory(Stack::class)->create(); + $stack->appServers()->save($server = factory(AppServer::class)->make()); + $stack->deployments()->save($deployment = factory(Deployment::class)->make([ + 'daemons' => ['first'] + ])); + + $deployment->serverDeployments()->save( + $serverDeployment = factory(ServerDeployment::class)->make([ + 'deployable_id' => $server->id, + 'deployable_type' => get_class($server), + ]) + ); + + $previousStack->update([ + 'environment_id' => $stack->environment_id, + ]); + + $job = new PromoteStack($stack, ['wait' => true]); + $job->handle(); + + Bus::assertNotDispatched(StartScheduler::class); + Bus::assertNotDispatched(RestartDaemons::class); + + Bus::assertDispatched(StopDaemons::class, function ($job) use ($previousServerDeployment) { + return $job->deployment->id === $previousServerDeployment->id; + }); + } + + + public function test_hooks_are_copied_from_previously_promoted_stack_if_instructed() + { + Bus::fake(); + + $previousStack = factory(Stack::class)->create(['promoted' => true]); + $previousStack->appServers()->save($previousServer = factory(AppServer::class)->make()); + $previousStack->hooks()->save($hook = factory(Hook::class)->make()); + + $stack = factory(Stack::class)->create(); + $stack->appServers()->save($server = factory(AppServer::class)->make()); + + $previousStack->update([ + 'environment_id' => $stack->environment_id, + ]); + + $job = new PromoteStack($stack, ['wait' => true, 'hooks' => true]); + $job->handle(); + + $this->assertNotEquals($stack->id, $hook->stack_id); + $this->assertEquals($stack->id, $hook->fresh()->stack_id); + } + + + public function test_hooks_are_not_copied_from_previously_promoted_stack_if_not_instructed() + { + Bus::fake(); + + $previousStack = factory(Stack::class)->create(['promoted' => true]); + $previousStack->appServers()->save($previousServer = factory(AppServer::class)->make()); + $previousStack->hooks()->save($hook = factory(Hook::class)->make()); + + $stack = factory(Stack::class)->create(); + $stack->appServers()->save($server = factory(AppServer::class)->make()); + + $previousStack->update([ + 'environment_id' => $stack->environment_id, + ]); + + $job = new PromoteStack($stack, ['wait' => true, 'hooks' => false]); + $job->handle(); + + $this->assertEquals($previousStack->id, $hook->fresh()->stack_id); + } +} diff --git a/tests/Feature/PromotedStackControllerTest.php b/tests/Feature/PromotedStackControllerTest.php new file mode 100644 index 00000000..25fd4109 --- /dev/null +++ b/tests/Feature/PromotedStackControllerTest.php @@ -0,0 +1,159 @@ +withoutExceptionHandling(); + } + + + public function test_stacks_can_be_promoted() + { + Bus::fake(); + + $stack = factory(Stack::class)->create(); + + $stack->webServers()->save(factory(WebServer::class)->make([ + 'meta' => ['serves' => ['laravel.com']], + ])); + + $response = $this->actingAs($stack->project()->user, 'api') + ->json('PUT', '/api/environment/'.$stack->environment_id.'/promoted-stack', [ + 'stack' => $stack->id, + ]); + + $response->assertStatus(200); + + Bus::assertDispatched(PromoteStack::class, function ($job) use ($stack) { + return $job->stack->id === $stack->id; + }); + + $stack->environment->promotionLock()->release(); + } + + + public function test_stacks_cant_be_promoted_if_not_serving() + { + Bus::fake(); + + $stack = factory(Stack::class)->create(); + $stack->webServers()->save(factory(WebServer::class)->make()); + + $response = $this->withExceptionHandling()->actingAs($stack->project()->user, 'api') + ->json('PUT', '/api/environment/'.$stack->environment_id.'/promoted-stack', [ + 'stack' => $stack->id, + ]); + + $response->assertStatus(422); + + $stack->environment->promotionLock()->release(); + } + + + public function test_stacks_can_be_promoted_and_daemons_will_wait() + { + Bus::fake(); + + $stack = factory(Stack::class)->create(); + + $stack->webServers()->save(factory(WebServer::class)->make([ + 'meta' => ['serves' => ['laravel.com']], + ])); + + $stack->environment->promotionLock()->release(); + + $response = $this->actingAs($stack->project()->user, 'api') + ->json('PUT', '/api/environment/'.$stack->environment_id.'/promoted-stack', [ + 'stack' => $stack->id, + 'wait' => true, + ]); + + $response->assertStatus(200); + + Bus::assertDispatched(PromoteStack::class, function ($job) use ($stack) { + return $job->stack->id === $stack->id && + $job->options['wait'] === true; + }); + + $stack->environment->promotionLock()->release(); + } + + + public function test_stack_cant_be_promoted_if_locked() + { + Bus::fake(); + + $stack = factory(Stack::class)->create(); + + $stack->webServers()->save(factory(WebServer::class)->make([ + 'meta' => ['serves' => ['laravel.com']], + ])); + + $stack->environment->promotionLock()->get(); + + $response = $this->actingAs($stack->project()->user, 'api') + ->json('PUT', '/api/environment/'.$stack->environment_id.'/promoted-stack', [ + 'stack' => $stack->id, + ]); + + $response->assertStatus(409); + + $stack->environment->promotionLock()->release(); + } + + + public function test_stacks_cant_be_promoted_if_not_promotable() + { + $stack = factory(Stack::class)->create(); + + $response = $this->withExceptionHandling()->actingAs($stack->project()->user, 'api') + ->json('PUT', '/api/environment/'.$stack->environment_id.'/promoted-stack', [ + 'stack' => $stack->id, + ]); + + $response->assertStatus(422); + + $stack->environment->promotionLock()->release(); + } + + + public function test_collaborator_can_promote_stacks() + { + Bus::fake(); + + $stack = factory(Stack::class)->create(); + $user = factory(User::class)->create(); + + $stack->project()->shareWith($user); + + $stack->webServers()->save(factory(WebServer::class)->make([ + 'meta' => ['serves' => ['laravel.com']], + ])); + + $response = $this->withExceptionHandling()->actingAs($user, 'api') + ->json('PUT', '/api/environment/'.$stack->environment_id.'/promoted-stack', [ + 'stack' => $stack->id, + ]); + + $response->assertStatus(200); + + $stack->environment->promotionLock()->release(); + } +} diff --git a/tests/Feature/ProviderControllerTest.php b/tests/Feature/ProviderControllerTest.php new file mode 100644 index 00000000..7bba4a6b --- /dev/null +++ b/tests/Feature/ProviderControllerTest.php @@ -0,0 +1,56 @@ +withoutExceptionHandling(); + } + + + public function test_provider_can_be_created() + { + $user = $this->user(); + + $response = $this->actingAs($user, 'api')->json('POST', '/api/server-provider', [ + 'name' => 'Personal', + 'type' => 'DigitalOcean', + 'meta' => ['token' => env('DIGITAL_OCEAN_TEST_KEY')], + ]); + + $response->assertStatus(201); + $this->assertCount(1, $user->serverProviders); + + $provider = $user->serverProviders->first(); + $this->assertEquals('Personal', $provider->name); + $this->assertEquals('DigitalOcean', $provider->type); + $this->assertEquals(env('DIGITAL_OCEAN_TEST_KEY'), $provider->meta['token']); + $this->assertInstanceOf(DigitalOcean::class, $provider->client()); + } + + + public function test_provider_can_be_validated() + { + $user = $this->user(); + + $response = $this->withExceptionHandling()->actingAs($user, 'api')->json('POST', '/api/server-provider', [ + 'name' => 'Personal', + 'type' => 'DigitalOcean', + 'meta' => ['token' => 'foo'], + ]); + + $response->assertStatus(422); + $this->assertCount(0, $user->serverProviders); + } +} diff --git a/tests/Feature/ProvisionAppServerScriptTest.php b/tests/Feature/ProvisionAppServerScriptTest.php new file mode 100644 index 00000000..5fd49065 --- /dev/null +++ b/tests/Feature/ProvisionAppServerScriptTest.php @@ -0,0 +1,33 @@ +withoutExceptionHandling(); + } + + + public function test_script_can_be_rendered() + { + $server = factory(AppServer::class)->create(); + + $script = new ProvisionAppServer($server); + + $script = $script->script(); + + $this->assertNotNull($script); + } +} diff --git a/tests/Feature/ProvisionBalancerJobTest.php b/tests/Feature/ProvisionBalancerJobTest.php new file mode 100644 index 00000000..7f220002 --- /dev/null +++ b/tests/Feature/ProvisionBalancerJobTest.php @@ -0,0 +1,46 @@ +withoutExceptionHandling(); + } + + + public function test_balancer_is_deleted_on_failure() + { + Bus::fake(); + + $balancer = factory(Balancer::class)->create(); + $balancer->address()->save($address = factory(IpAddress::class)->make()); + + ServerProviderClientFactory::shouldReceive('make->deleteServer')->with(Mockery::on(function ($value) use ($balancer) { + return $value->id == $balancer->id; + })); + + $job = new ProvisionBalancer($balancer); + $job->failed(new Exception); + + Bus::assertDispatched(DeleteServerOnProvider::class); + $this->assertCount(1, $balancer->project->alerts); + } +} diff --git a/tests/Feature/ProvisionBalancerScriptTest.php b/tests/Feature/ProvisionBalancerScriptTest.php new file mode 100644 index 00000000..850e2f68 --- /dev/null +++ b/tests/Feature/ProvisionBalancerScriptTest.php @@ -0,0 +1,33 @@ +withoutExceptionHandling(); + } + + + public function test_script_can_be_rendered() + { + $balancer = factory(Balancer::class)->create(); + + $script = new ProvisionBalancer($balancer); + + $script = $script->script(); + + $this->assertNotNull($script); + } +} diff --git a/tests/Feature/ProvisionDatabaseJobTest.php b/tests/Feature/ProvisionDatabaseJobTest.php new file mode 100644 index 00000000..5fb9f358 --- /dev/null +++ b/tests/Feature/ProvisionDatabaseJobTest.php @@ -0,0 +1,46 @@ +withoutExceptionHandling(); + } + + + public function test_databases_is_deleted_on_failure() + { + Bus::fake(); + + $database = factory(Database::class)->create(); + $database->address()->save($address = factory(IpAddress::class)->make()); + + ServerProviderClientFactory::shouldReceive('make->deleteServer')->with(Mockery::on(function ($value) use ($database) { + return $value->id == $database->id; + })); + + $job = new ProvisionDatabase($database); + $job->failed(new Exception); + + Bus::assertDispatched(DeleteServerOnProvider::class); + $this->assertCount(1, $database->project->alerts); + } +} diff --git a/tests/Feature/ProvisionDatabaseScriptTest.php b/tests/Feature/ProvisionDatabaseScriptTest.php new file mode 100644 index 00000000..95228d98 --- /dev/null +++ b/tests/Feature/ProvisionDatabaseScriptTest.php @@ -0,0 +1,31 @@ +withoutExceptionHandling(); + } + + + public function test_script_can_be_rendered() + { + $database = factory(Database::class)->create(); + $script = new ProvisionDatabase($database); + $script = $script->script(); + + $this->assertNotNull($script); + } +} diff --git a/tests/Feature/ProvisionServersJobTest.php b/tests/Feature/ProvisionServersJobTest.php new file mode 100644 index 00000000..c3a60bda --- /dev/null +++ b/tests/Feature/ProvisionServersJobTest.php @@ -0,0 +1,78 @@ +withoutExceptionHandling(); + } + + + public function test_servers_are_created_and_provisioning_jobs_are_dispatched() + { + Bus::fake(); + + $stack = factory(Stack::class)->create(); + $stack->appServers()->save($server = factory(AppServer::class)->make(['provider_server_id' => null])); + + ServerProviderClientFactory::shouldReceive('make->createServer')->andReturn('123'); + + $job = new ProvisionServers($stack); + $job->handle(); + + Bus::assertDispatched(ProvisionAppServer::class); + $this->assertEquals(123, $server->fresh()->providerServerId()); + $this->assertEquals(1, $stack->fresh()->initial_server_count); + } + + + public function test_servers_are_not_created_if_provider_id_already_present() + { + Bus::fake(); + + $stack = factory(Stack::class)->create(); + $stack->appServers()->save($server = factory(AppServer::class)->make()); + + ServerProviderClientFactory::shouldReceive('make->createServer')->never(); + + $job = new ProvisionServers($stack); + $job->handle(); + + Bus::assertDispatched(ProvisionAppServer::class); + } + + + public function test_provisioning_job_not_dispatched_if_already_dispatched() + { + Bus::fake(); + + $stack = factory(Stack::class)->create(); + $stack->appServers()->save($server = factory(AppServer::class)->make([ + 'provisioning_job_dispatched_at' => Carbon::now(), + ])); + + ServerProviderClientFactory::shouldReceive('make->createServer')->never(); + + $job = new ProvisionServers($stack); + $job->handle(); + + Bus::assertNotDispatched(ProvisionAppServer::class); + } +} diff --git a/tests/Feature/ProvisionableTest.php b/tests/Feature/ProvisionableTest.php new file mode 100644 index 00000000..38b00fea --- /dev/null +++ b/tests/Feature/ProvisionableTest.php @@ -0,0 +1,109 @@ +create(); + $database->address()->save(factory(IpAddress::class)->make()); + + $this->assertEquals('127.0.0.1', $database->ipAddress()); + $this->assertEquals('127.0.0.2', $database->privateIpAddress()); + $this->assertTrue(file_exists($database->ownerKeyPath())); + $this->assertEquals(1, $database->providerServerId()); + + $this->assertFalse($database->isProvisioning()); + $this->assertTrue($database->isProvisioned()); + + $database->markAsProvisioning(); + $this->assertTrue($database->isProvisioning()); + $this->assertFalse($database->isProvisioned()); + + $database->markAsProvisioned(); + $this->assertFalse($database->isProvisioning()); + $this->assertTrue($database->isProvisioned()); + } + + + public function test_determining_if_ready_for_provisioning_will_retrieve_ip_addresses() + { + $database = factory(Database::class)->create(); + + ServerProviderClientFactory::shouldReceive('make->getPublicIpAddress')->with($database)->andReturn('127.0.0.3'); + ServerProviderClientFactory::shouldReceive('make->getPrivateIpAddress')->with($database)->andReturn('127.0.0.4'); + + ShellProcessRunner::mock([ + (object) ['exitCode' => 0, 'output' => '/root', 'timedOut' => false], + (object) ['exitCode' => 0, 'output' => '', 'timedOut' => false], + ]); + + $this->assertTrue($database->isReadyForProvisioning()); + + $database = $database->fresh(); + + $this->assertEquals('127.0.0.3', $database->ipAddress()); + $this->assertEquals('127.0.0.4', $database->privateIpAddress()); + } + + + public function test_determining_if_ready_for_provisioning_will_skip_pulling_ip_addresses_if_already_present() + { + $database = factory(Database::class)->create(); + $database->address()->save(factory(IpAddress::class)->make()); + + ServerProviderClientFactory::shouldReceive('make->getPublicIpAddress')->never(); + ServerProviderClientFactory::shouldReceive('make->getPrivateIpAddress')->never(); + + ShellProcessRunner::mock([ + (object) ['exitCode' => 0, 'output' => '/root', 'timedOut' => false], + (object) ['exitCode' => 0, 'output' => '', 'timedOut' => false], + ]); + + $this->assertTrue($database->isReadyForProvisioning()); + } + + + public function test_determining_if_ready_for_provisioning_will_return_false_if_no_output() + { + $database = factory(Database::class)->create(); + $database->address()->save(factory(IpAddress::class)->make()); + + ServerProviderClientFactory::shouldReceive('make->getPublicIpAddress')->never(); + ServerProviderClientFactory::shouldReceive('make->getPrivateIpAddress')->never(); + + ShellProcessRunner::mock([ + (object) ['exitCode' => 0, 'output' => '', 'timedOut' => false], + ]); + + $this->assertFalse($database->isReadyForProvisioning()); + } + + + public function test_determining_if_ready_for_provisioning_will_return_false_if_apt_is_locked() + { + $database = factory(Database::class)->create(); + $database->address()->save(factory(IpAddress::class)->make()); + + ServerProviderClientFactory::shouldReceive('make->getPublicIpAddress')->never(); + ServerProviderClientFactory::shouldReceive('make->getPrivateIpAddress')->never(); + + ShellProcessRunner::mock([ + (object) ['exitCode' => 0, 'output' => '/root', 'timedOut' => false], + (object) ['exitCode' => 0, 'output' => 'something', 'timedOut' => false], + ]); + + $this->assertFalse($database->isReadyForProvisioning()); + } +} diff --git a/tests/Feature/ReportHelperTest.php b/tests/Feature/ReportHelperTest.php new file mode 100644 index 00000000..84f04b30 --- /dev/null +++ b/tests/Feature/ReportHelperTest.php @@ -0,0 +1,34 @@ +withoutExceptionHandling(); + } + + + public function test_helper_reports_exceptions() + { + $e = new Exception; + $mock = Mockery::mock(); + $mock->shouldReceive('report')->once()->with($e); + $this->swap(ExceptionHandler::class, $mock); + report($e); + + $this->assertTrue(true); + } +} diff --git a/tests/Feature/RestartDaemonsJobTest.php b/tests/Feature/RestartDaemonsJobTest.php new file mode 100644 index 00000000..91767fcf --- /dev/null +++ b/tests/Feature/RestartDaemonsJobTest.php @@ -0,0 +1,48 @@ +withoutExceptionHandling(); + } + + + public function test_daemons_are_restarted() + { + $deployment = factory(Deployment::class)->create([ + 'daemons' => [ + 'first' => [ + 'command' => 'php artisan horizon', + ], + ], + ]); + + $deployment->serverDeployments()->save( + $serverDeployment = factory(ServerDeployment::class)->make() + ); + + ShellProcessRunner::mock([ + (object) ['exitCode' => 0, 'output' => '', 'timedOut' => false], + ]); + + $job = new RestartDaemons($serverDeployment); + $job->handle(); + + $this->assertCount(1, $serverDeployment->deployable->fresh()->daemonGenerations); + } +} diff --git a/tests/Feature/RestoreDatabaseBackupJobTest.php b/tests/Feature/RestoreDatabaseBackupJobTest.php new file mode 100644 index 00000000..a0f331c9 --- /dev/null +++ b/tests/Feature/RestoreDatabaseBackupJobTest.php @@ -0,0 +1,46 @@ +withoutExceptionHandling(); + } + + + public function test_script_is_started() + { + $restore = factory(DatabaseRestore::class)->create(); + + $job = new RestoreDatabaseBackup($restore); + + TaskFactory::shouldReceive('createFromScript')->once()->with( + Mockery::type(Database::class), Mockery::type(RestoreDatabaseBackupScript::class), Mockery::on(function ($options) use ($restore) { + return $options['then'][0] instanceof CheckDatabaseRestore && + $options['then'][0]->id === $restore->id; + }) + )->andReturn($task = new FakeTask); + + $job->handle(); + + $this->assertTrue($task->ranInBackground); + } +} diff --git a/tests/Feature/RestoreDatabaseBackupScriptTest.php b/tests/Feature/RestoreDatabaseBackupScriptTest.php new file mode 100644 index 00000000..6157576f --- /dev/null +++ b/tests/Feature/RestoreDatabaseBackupScriptTest.php @@ -0,0 +1,34 @@ +withoutExceptionHandling(); + } + + + public function test_script_can_be_rendered() + { + $backup = factory(DatabaseBackup::class)->create(); + + $backup->restores()->save($restore = factory(DatabaseRestore::class)->make()); + + $script = new RestoreDatabaseBackup($restore); + + $this->assertNotNull($script->script()); + } +} diff --git a/tests/Feature/Route53Test.php b/tests/Feature/Route53Test.php new file mode 100644 index 00000000..9b436b97 --- /dev/null +++ b/tests/Feature/Route53Test.php @@ -0,0 +1,53 @@ +withoutExceptionHandling(); + } + + + public function test_dns_records_can_be_added() + { + $route53 = app(Route53::class); + $stack = factory(Stack::class)->create(['balanced' => true]); + $stack->environment->project->balancers()->save($balancer = factory(Balancer::class)->make()); + $balancer->address()->save(factory(IpAddress::class)->make()); + + $route53->addRecord($stack); + + $stack = $stack->fresh(); + + $this->assertNotNull($stack->dns_record_id); + $this->assertEquals($balancer->address->public_address, $stack->dns_address); + + // Test re-adding a record works... + $oldRecordId = $stack->dns_record_id; + + $route53->addRecord($stack); + $this->assertNotEquals($oldRecordId, $stack->fresh()->dns_record_id); + + // Test deleting the record... + $route53->deleteRecord($stack); + + $stack = $stack->fresh(); + + $this->assertNull($stack->dns_record_id); + $this->assertNull($stack->dns_address); + } +} diff --git a/tests/Feature/S3Test.php b/tests/Feature/S3Test.php new file mode 100644 index 00000000..1af5c831 --- /dev/null +++ b/tests/Feature/S3Test.php @@ -0,0 +1,75 @@ +withoutExceptionHandling(); + } + + + public function test_can_determine_if_credentials_are_valid() + { + $provider = factory(StorageProvider::class)->create(); + $s3 = new S3($provider); + + $this->assertTrue($s3->valid()); + + + $provider = factory(StorageProvider::class)->create([ + 'meta' => ['key' => 'foo', 'secret' => 'baz', 'region' => 'us-east-1', 'bucket' => 'foobarbaz'] + ]); + $s3 = new S3($provider); + + $this->assertFalse($s3->valid()); + } + + + public function test_can_create_and_delete_buckets() + { + $provider = factory(StorageProvider::class)->create(); + + $s3 = new S3($provider); + $s3->createBucket('laravel-cloud-dummy'); + + sleep(1); + + retry(10, function () use ($s3) { + $this->assertTrue($s3->hasBucket('laravel-cloud-dummy')); + }, 1000); + + $s3->deleteBucket('laravel-cloud-dummy'); + + sleep(1); + + retry(10, function () use ($s3) { + $this->assertFalse($s3->hasBucket('laravel-cloud-dummy')); + }, 1000); + } + + + public function test_can_delete_files() + { + $provider = factory(StorageProvider::class)->create(); + $s3 = new S3($provider); + + $s3->put('hello-world', 'Hello World'); + $this->assertEquals(0, $s3->size('hello-world')); + $this->assertTrue($s3->has('hello-world')); + + $s3->delete('hello-world'); + $this->assertFalse($s3->has('hello-world')); + } +} diff --git a/tests/Feature/ScheduleControllerTest.php b/tests/Feature/ScheduleControllerTest.php new file mode 100644 index 00000000..3da641c3 --- /dev/null +++ b/tests/Feature/ScheduleControllerTest.php @@ -0,0 +1,32 @@ +withoutExceptionHandling(); + } + + + public function test_prune_tasks_can_is_dispatched() + { + Bus::fake(); + + $response = $this->post('/schedule/prune-tasks'); + + $response->assertStatus(200); + Bus::assertDispatched(PruneTasks::class); + } +} diff --git a/tests/Feature/SchedulerControllerTest.php b/tests/Feature/SchedulerControllerTest.php new file mode 100644 index 00000000..dbe3f735 --- /dev/null +++ b/tests/Feature/SchedulerControllerTest.php @@ -0,0 +1,80 @@ +withoutExceptionHandling(); + } + + + public function test_scheduler_can_be_started() + { + Bus::fake(); + + $stack = factory(Stack::class)->create(); + + $stack->deployments()->save(factory(Deployment::class)->make()); + + $stack->deployments()->save($lastDeployment = factory(Deployment::class)->make([ + 'schedule' => ['first'] + ])); + + $lastDeployment->serverDeployments()->save( + $serverDeployment = factory(ServerDeployment::class)->make() + ); + + $response = $this->actingAs($stack->project()->user, 'api') + ->json('post', '/api/stack/'.$stack->id.'/scheduler'); + + $response->assertStatus(201); + + Bus::assertDispatched(StartScheduler::class, function ($job) use ($serverDeployment) { + return $serverDeployment->id === $job->deployment->id; + }); + } + + + public function test_scheduler_can_be_stopped() + { + Bus::fake(); + + $stack = factory(Stack::class)->create(); + + $stack->deployments()->save(factory(Deployment::class)->make()); + + $stack->deployments()->save($lastDeployment = factory(Deployment::class)->make([ + 'schedule' => ['first'] + ])); + + $lastDeployment->serverDeployments()->save( + $serverDeployment = factory(ServerDeployment::class)->make() + ); + + $response = $this->actingAs($stack->project()->user, 'api') + ->json('delete', '/api/stack/'.$stack->id.'/scheduler'); + + $response->assertStatus(200); + + Bus::assertDispatched(StopScheduler::class, function ($job) use ($serverDeployment) { + return $serverDeployment->id === $job->deployment->id; + }); + } +} diff --git a/tests/Feature/ServerConfigurationControllerTest.php b/tests/Feature/ServerConfigurationControllerTest.php new file mode 100644 index 00000000..e355c635 --- /dev/null +++ b/tests/Feature/ServerConfigurationControllerTest.php @@ -0,0 +1,40 @@ +withoutExceptionHandling(); + } + + + public function test_stack_server_configurations_can_be_rebuilt() + { + Bus::fake(); + + $stack = factory(Stack::class)->create(); + + $response = $this->actingAs($stack->project()->user, 'api') + ->json('PUT', '/api/stack/'.$stack->id.'/server-configuration'); + + $response->assertStatus(200); + + Bus::assertDispatched(SyncServers::class, function ($job) use ($stack) { + return $job->stack->id === $stack->id; + }); + } +} diff --git a/tests/Feature/ServerDeploymentTest.php b/tests/Feature/ServerDeploymentTest.php new file mode 100644 index 00000000..764246c5 --- /dev/null +++ b/tests/Feature/ServerDeploymentTest.php @@ -0,0 +1,37 @@ +create(); + $deployment->stack()->databases()->save($database = factory(Database::class)->make()); + $database->address()->save(factory(IpAddress::class)->make()); + + $this->assertEquals($database->address->private_address, $deployment->databaseHost()); + $this->assertEquals($database->password, $deployment->databasePassword()); + } + + + public function test_app_server_information_is_used_if_no_other_databases_are_present() + { + $deployment = factory(ServerDeployment::class)->create(); + $deployment->setRelation('deployable', $server = factory(AppServer::class)->make()); + + $this->assertEquals('127.0.0.1', $deployment->databaseHost()); + $this->assertEquals($server->database_password, $deployment->databasePassword()); + } +} diff --git a/tests/Feature/ServerTest.php b/tests/Feature/ServerTest.php new file mode 100644 index 00000000..392e7195 --- /dev/null +++ b/tests/Feature/ServerTest.php @@ -0,0 +1,54 @@ +withoutExceptionHandling(); + } + + + public function test_should_respond_to_returns_proper_addresses() + { + $server = factory(AppServer::class)->create([ + 'meta' => [ + 'serves' => [ + 'laravel.com', + ], + ], + ]); + + $this->assertEquals([ + 'laravel.com:80', + 'laravel.com:443', + 'www.laravel.com:80', + 'www.laravel.com:443', + $server->stack->url.'.laravel.build:80', + $server->stack->url.'.laravel.build:443', + ], $server->shouldRespondToWithPorts()); + } + + + public function test_daemon_generations_are_trimmed() + { + $server = factory(AppServer::class)->create(); + + for ($i = 0; $i < 30; $i++) { + $server->createDaemonGeneration(); + } + + $this->assertEquals(10, $server->daemonGenerations()->count()); + } +} diff --git a/tests/Feature/ShellProcessRunnerTest.php b/tests/Feature/ShellProcessRunnerTest.php new file mode 100644 index 00000000..8744a79c --- /dev/null +++ b/tests/Feature/ShellProcessRunnerTest.php @@ -0,0 +1,49 @@ +withoutExceptionHandling(); + } + + + public function test_process_runner_runs_process() + { + $process = Mockery::mock(); + + $process->shouldReceive('run'); + $process->shouldReceive('getExitCode')->andReturn(0); + + $response = (new ShellProcessRunner)->run($process); + + $this->assertEquals(0, $response->exitCode); + $this->assertEquals('', $response->output); + $this->assertFalse($response->timedOut); + } + + + public function test_process_runner_handles_timeouts() + { + $process = (new Process('sleep 2'))->setTimeout(2); + + $response = (new ShellProcessRunner)->run($process); + + $this->assertEquals(0, $response->exitCode); + $this->assertEquals('', $response->output); + $this->assertTrue($response->timedOut); + } +} diff --git a/tests/Feature/SourceControllerTest.php b/tests/Feature/SourceControllerTest.php new file mode 100644 index 00000000..1029c431 --- /dev/null +++ b/tests/Feature/SourceControllerTest.php @@ -0,0 +1,96 @@ +withoutExceptionHandling(); + } + + + public function test_source_can_be_created() + { + $user = $this->user(); + + $response = $this->actingAs($user, 'api')->json('POST', '/api/source-provider', [ + 'name' => 'Personal', + 'type' => 'GitHub', + 'meta' => ['token' => env('GITHUB_TEST_KEY')], + ]); + + $response->assertStatus(201); + $this->assertCount(1, $user->sourceProviders); + + $source = $user->sourceProviders->first(); + $this->assertEquals('Personal', $source->name); + $this->assertEquals('GitHub', $source->type); + $this->assertEquals(env('GITHUB_TEST_KEY'), $source->meta['token']); + $this->assertInstanceOf(GitHub::class, $source->client()); + } + + + public function test_source_can_be_validated() + { + $user = $this->user(); + + $response = $this->withExceptionHandling()->actingAs($user, 'api')->json('POST', '/api/source-provider', [ + 'name' => 'Personal', + 'type' => 'GitHub', + 'meta' => ['token' => 'foo'], + ]); + + $response->assertStatus(422); + $this->assertCount(0, $user->sourceProviders); + } + + + public function test_source_provider_can_be_deleted() + { + $provider = factory(SourceProvider::class)->create(); + + $response = $this->actingAs($provider->user, 'api')->json( + 'DELETE', "/api/source-provider/{$provider->id}" + ); + + $response->assertStatus(200); + $this->assertCount(0, $provider->user->sourceProviders()->get()); + } + + + public function test_only_owner_can_delete_source_providers() + { + $provider = factory(SourceProvider::class)->create(); + + $response = $this->withExceptionHandling()->actingAs($this->user(), 'api')->json( + 'DELETE', "/api/source-provider/{$provider->id}" + ); + + $response->assertStatus(403); + } + + + public function test_source_providers_can_not_be_deleted_if_attached_to_projects() + { + $provider = factory(SourceProvider::class)->create(); + $provider->projects()->save($project = factory(Project::class)->make()); + + $response = $this->withExceptionHandling()->actingAs($provider->user, 'api')->json( + 'DELETE', "/api/source-provider/{$provider->id}" + ); + + $response->assertStatus(422); + } +} diff --git a/tests/Feature/StackControllerTest.php b/tests/Feature/StackControllerTest.php new file mode 100644 index 00000000..d58fc468 --- /dev/null +++ b/tests/Feature/StackControllerTest.php @@ -0,0 +1,348 @@ +withoutExceptionHandling(); + } + + + public function test_stacks_can_be_listed() + { + $stack = factory(Stack::class)->create(); + $project = $stack->project(); + $project->environments()->save($environment2 = factory(Environment::class)->make()); + $environment2->stacks()->save(factory(Stack::class)->make()); + + $response = $this->actingAs($stack->environment->project->user, 'api')->get( + '/api/project/'.$stack->environment->project->id.'/stacks' + ); + + $this->assertCount(2, $response->original); + } + + + public function test_404_returned_if_environment_doesnt_exist() + { + Bus::fake(); + + $user = $this->user(); + + $response = $this->withExceptionHandling()->actingAs($user, 'api')->json('POST', '/api/stack/24820439', [ + 'source_provider_id' => 'github', + 'name' => 'test-stack', + 'repository' => 'laravel/laravel', + 'branch' => 'master', + 'databases' => ['mysql'], + 'web' => [ + 'size' => '2GB', + 'serves' => ['laravel.com'], + 'scale' => 2, + ], + 'worker' => [ + 'size' => '2GB', + 'scale' => 2, + 'daemons' => [ + 'first' => [ + 'command' => 'php artisan horizon', + 'processes' => 1, + 'wait' => 60, + ], + ], + ], + 'build' => [ + 'composer install -o', + 'php artisan migrate', + 'php artisan route:cache', + 'php artisan config:cache', + ], + ]); + + $response->assertStatus(404); + + Bus::assertNotDispatched(CreateLoadBalancerIfNecessary::class); + } + + + public function test_validation_fails_if_no_web_servers_present() + { + Bus::fake(); + + $environment = factory(Environment::class)->create(); + $environment->project->user->sourceProviders()->save($source = factory(SourceProvider::class)->make()); + $environment->project->databases()->save(factory(Database::class)->make(['name' => 'mysql'])); + + $response = $this->withExceptionHandling()->actingAs($environment->project->user, 'api')->json('POST', '/api/environment/'.$environment->id.'/stack', [ + 'source_provider_id' => $source->name, + 'name' => 'test-stack', + 'repository' => 'laravel/laravel', + 'branch' => 'master', + 'databases' => ['mysql'], + 'worker' => [ + 'size' => '2GB', + 'scale' => 2, + 'daemons' => [ + 'first' => [ + 'command' => 'php artisan horizon', + 'processes' => 1, + 'wait' => 60, + ], + ], + ], + 'build' => [ + 'composer install -o', + 'php artisan migrate', + 'php artisan route:cache', + 'php artisan config:cache', + ], + ]); + + $response->assertStatus(422); + $this->assertEquals('At least one web server must be defined.', $response->original['errors']['web'][0]); + + Bus::assertNotDispatched(CreateLoadBalancerIfNecessary::class); + } + + + public function test_validation_fails_if_app_servers_combined_with_other_servers() + { + Bus::fake(); + + $environment = factory(Environment::class)->create(); + $environment->project->user->sourceProviders()->save($source = factory(SourceProvider::class)->make()); + + $response = $this->withExceptionHandling()->actingAs($environment->project->user, 'api')->json('POST', '/api/environment/'.$environment->id.'/stack', [ + 'source_provider_id' => $source->name, + 'repository' => 'laravel/laravel', + 'branch' => 'master', + 'databases' => ['mysql'], + 'app' => [ + 'size' => '2GB', + 'serves' => ['laravel.com'], + 'scale' => 1, + ], + 'worker' => [ + 'size' => '2GB', + 'scale' => 2, + 'daemons' => [ + 'first' => [ + 'command' => 'php artisan horizon', + 'processes' => 1, + 'wait' => 60, + ], + ], + ], + 'build' => [ + 'composer install -o', + 'php artisan migrate', + 'php artisan route:cache', + 'php artisan config:cache', + ], + ]); + + $response->assertStatus(422); + $this->assertEquals('App servers may not be provisioned with web and worker servers.', $response->original['errors']['app'][0]); + + Bus::assertNotDispatched(CreateLoadBalancerIfNecessary::class); + } + + + public function test_stacks_can_be_provisioned() + { + Bus::fake(); + + $environment = factory(Environment::class)->create(); + $project = $environment->project; + $environment->project->user->sourceProviders()->save($source = factory(SourceProvider::class)->make()); + $environment->project->databases()->save(factory(Database::class)->make(['name' => 'mysql'])); + + $response = $this->actingAs($environment->project->user, 'api')->json('POST', '/api/environment/'.$environment->id.'/stack', [ + 'source_provider_id' => $source->name, + 'name' => 'test-stack', + 'repository' => 'laravel/laravel', + 'branch' => 'master', + 'databases' => ['mysql'], + 'web' => [ + 'size' => '2GB', + 'tls' => 'self-signed', + 'serves' => ['laravel.com'], + 'scale' => 2, + 'scripts' => [ + 'exit 1', + ], + ], + 'worker' => [ + 'size' => '2GB', + 'scale' => 2, + 'daemons' => [ + 'first' => [ + 'command' => 'php artisan horizon', + 'processes' => 1, + 'wait' => 60, + ], + ], + ], + 'build' => [ + 'composer install -o', + 'php artisan migrate', + 'php artisan route:cache', + 'php artisan config:cache', + ], + ]); + + $response->assertStatus(201); + Bus::assertDispatched(CreateLoadBalancerIfNecessary::class); + + $environment = $environment->fresh(); + + // Stack assertions... + $stack = $environment->stacks->first(); + + $this->assertEquals($environment->project->user->id, $stack->creator->id); + $this->assertCount(1, $stack->databases); + $this->assertEquals('mysql', $stack->databases->first()->name); + + $this->assertCount(2, $stack->webServers); + $this->assertEquals('exit 1', $stack->meta['scripts']['web'][0]); + $this->assertEquals($stack->name.'-web-1', $stack->webServers->first()->name); + $this->assertEquals(['laravel.com'], $stack->webServers->first()->meta['serves']); + $this->assertEquals('self-signed', $stack->webServers->first()->meta['tls']); + + $this->assertCount(2, $stack->workerServers); + $this->assertEquals('php artisan horizon', $stack->meta['initial_daemons']['first']['command']); + + $this->assertEquals('provisioning', $stack->status); + } + + + public function test_stacks_can_be_provisioned_with_app_servers() + { + Bus::fake(); + + $environment = factory(Environment::class)->create(); + $project = $environment->project; + $project->user->sourceProviders()->save($source = factory(SourceProvider::class)->make()); + $project->databases()->save(factory(Database::class)->make(['name' => 'mysql'])); + + $response = $this->actingAs($environment->project->user, 'api')->json('POST', '/api/environment/'.$environment->id.'/stack', [ + 'source_provider_id' => $source->name, + 'name' => 'test-stack', + 'name' => 'test-stack', + 'repository' => 'laravel/laravel', + 'branch' => 'master', + 'databases' => ['mysql'], + 'app' => [ + 'size' => '2GB', + 'serves' => ['laravel.com'], + 'daemons' => [ + 'first' => [ + 'command' => 'php artisan horizon', + 'processes' => 1, + 'wait' => 60, + ], + ], + ], + 'build' => [ + 'composer install -o', + 'php artisan migrate', + 'php artisan route:cache', + 'php artisan config:cache', + ], + ]); + + $response->assertStatus(201); + Bus::assertDispatched(CreateLoadBalancerIfNecessary::class); + + $environment = $environment->fresh(); + + // Environment assertions... + $this->assertEquals('production', $environment->name); + $this->assertCount(1, $environment->stacks); + $this->assertEquals('DigitalOcean', $environment->project->serverProvider->name); + + // Stack assertions... + $stack = $environment->stacks->first(); + + $this->assertCount(1, $stack->appServers); + $this->assertEquals($stack->name.'-app-1', $stack->appServers->first()->name); + $this->assertEquals(['laravel.com'], $stack->appServers->first()->meta['serves']); + $this->assertEquals('php artisan horizon', $stack->meta['initial_daemons']['first']['command']); + + $this->assertEquals('provisioning', $stack->status); + } + + + public function test_stacks_may_be_deleted() + { + $stack = factory(Stack::class)->create(); + + $response = $this->actingAs($stack->environment->project->user, 'api')->json( + 'delete', '/api/stacks/'.$stack->id + ); + + $response->assertStatus(200); + + $this->assertCount(0, Stack::all()); + } + + + public function test_stacks_cant_be_deleted_without_permission() + { + $stack = factory(Stack::class)->create(); + + $user = $this->user(); + + $response = $this->withExceptionHandling()->actingAs($user, 'api')->json( + 'delete', '/api/stacks/'.$stack->id + ); + + $response->assertStatus(403); + } + + + public function test_stacks_can_always_be_deleted_by_the_user_that_created_them() + { + $stack = factory(Stack::class)->create(); + + $user = $this->user(); + $stack->update(['creator_id' => $user->id]); + + $response = $this->withExceptionHandling()->actingAs($user, 'api')->json( + 'delete', '/api/stacks/'.$stack->id + ); + + $response->assertStatus(200); + } + + + public function test_stacks_may_not_be_deleted_while_deploying() + { + $stack = factory(Stack::class)->create(); + $stack->markAsDeploying(); + + $response = $this->withExceptionHandling()->actingAs($stack->environment->project->user, 'api')->json( + 'delete', '/api/stacks/'.$stack->id + ); + + $response->assertStatus(422); + } +} diff --git a/tests/Feature/StackDatabaseControllerTest.php b/tests/Feature/StackDatabaseControllerTest.php new file mode 100644 index 00000000..ec367bdd --- /dev/null +++ b/tests/Feature/StackDatabaseControllerTest.php @@ -0,0 +1,108 @@ +withoutExceptionHandling(); + } + + + public function test_database_can_be_added_to_stack() + { + Bus::fake(); + + $stack = factory(Stack::class)->create([ + 'status' => 'provisioned', + ]); + + $database = factory(Database::class)->create([ + 'project_id' => $stack->project()->id, + ]); + + $response = $this->actingAs( + $stack->environment->project->user, 'api' + )->json('put', '/api/stack/'.$stack->id.'/databases', [ + 'databases' => [$database->name], + ]); + + $response->assertStatus(200); + + $stack = $stack->fresh(); + $this->assertTrue($stack->databases->contains($database)); + + Bus::assertDispatched(SyncNetwork::class); + } + + + public function test_database_can_be_removed_from_stack() + { + Bus::fake(); + + $stack = factory(Stack::class)->create([ + 'status' => 'provisioned', + ]); + + $database = factory(Database::class)->create([ + 'project_id' => $stack->project()->id, + ]); + + $stack->databases()->attach($database); + + $response = $this->actingAs( + $stack->environment->project->user, 'api' + )->json('put', '/api/stack/'.$stack->id.'/databases', [ + 'databases' => [], + ]); + + $response->assertStatus(200); + + $stack = $stack->fresh(); + $this->assertFalse($stack->databases->contains($database)); + + Bus::assertDispatched(SyncNetwork::class); + } + + + public function test_nothing_happens_if_no_databases_are_affected() + { + Bus::fake(); + + $stack = factory(Stack::class)->create([ + 'status' => 'provisioned', + ]); + + $database = factory(Database::class)->create([ + 'project_id' => $stack->project()->id, + ]); + + $stack->databases()->attach($database); + + $response = $this->actingAs( + $stack->environment->project->user, 'api' + )->json('put', '/api/stack/'.$stack->id.'/databases', [ + 'databases' => [$database->name], + ]); + + $response->assertStatus(200); + + $stack = $stack->fresh(); + $this->assertTrue($stack->databases->contains($database)); + + Bus::assertNotDispatched(SyncNetwork::class); + } +} diff --git a/tests/Feature/StackDeploymentTest.php b/tests/Feature/StackDeploymentTest.php new file mode 100644 index 00000000..37e0167d --- /dev/null +++ b/tests/Feature/StackDeploymentTest.php @@ -0,0 +1,112 @@ +withoutExceptionHandling(); + } + + + public function test_deploy_dispatches_proper_jobs_and_creates_deployment_record() + { + Bus::fake(); + + $stack = factory(Stack::class)->create([ + 'status' => 'provisioned', + ]); + $stack->appServers()->save($server = factory(AppServer::class)->create()); + + $stack->deploymentLock()->release(); + + $deployment = $stack->deploy('hash', ['first'], ['second'], ['storage'], [ + 'first' => [ + 'connection' => 'redis', + ], + ]); + + $this->assertEquals('hash', $deployment->commit_hash); + $this->assertEquals(['first'], $deployment->build_commands); + $this->assertEquals(['second'], $deployment->activation_commands); + $this->assertEquals('building', $deployment->status); + + Bus::assertDispatched(MonitorDeployment::class, function ($job) use ($deployment) { + return $job->deployment->id === $deployment->id; + }); + + Bus::assertDispatched(TimeOutDeploymentIfStillRunning::class, function ($job) use ($deployment) { + return $job->deployment->id === $deployment->id; + }); + + Bus::assertDispatched(Build::class, function ($job) use ($server) { + return $job->deployment->deployable->id === $server->id; + }); + + $stack->deploymentLock()->release(); + } + + + public function test_deploy_can_be_performed_by_branch() + { + Bus::fake(); + + $stack = factory(Stack::class)->create([ + 'status' => 'provisioned', + ]); + $stack->appServers()->save($server = factory(AppServer::class)->create()); + + $stack->deploymentLock()->release(); + + $deployment = $stack->deployBranch('master', ['first'], ['second'], ['storage'], [ + 'first' => [ + 'connection' => 'redis', + ], + ]); + + $this->assertEquals('master', $deployment->branch); + $this->assertNotNull('hash', $deployment->commit_hash); + + $stack->deploymentLock()->release(); + } + + + public function test_previous_deployments_are_trimmed() + { + Bus::fake(); + + $stack = factory(Stack::class)->create([ + 'status' => 'provisioned', + ]); + + $stack->deploymentLock()->release(); + + $stack->appServers()->save($server = factory(AppServer::class)->create()); + + for ($i = 0; $i < 30; $i++) { + $stack->deployments()->save(factory(Deployment::class)->make()); + } + + $this->assertCount(30, Deployment::all()); + $deployment = $stack->deploy('master', ['first'], ['second']); + $this->assertCount(20, Deployment::all()); + + $stack->deploymentLock()->release(); + } +} diff --git a/tests/Feature/StackServerControllerTest.php b/tests/Feature/StackServerControllerTest.php new file mode 100644 index 00000000..f11dc4be --- /dev/null +++ b/tests/Feature/StackServerControllerTest.php @@ -0,0 +1,45 @@ +withoutExceptionHandling(); + } + + + public function test_all_servers_are_returned() + { + $stack = factory(Stack::class)->create(); + $stack->appServers()->save($appServer = factory(AppServer::class)->make()); + $stack->appServers()->save($appServer2 = factory(AppServer::class)->make()); + $stack->webServers()->save($webServer = factory(WebServer::class)->make()); + $stack->workerServers()->save($workerServer = factory(WorkerServer::class)->make()); + $workerServer->address()->save($address = factory(IpAddress::class)->make()); + + $response = $this->actingAs( + $stack->environment->project->user, 'api' + )->get("/api/stack/{$stack->id}/servers"); + + $response->assertStatus(200); + $this->assertEquals(2, count($response->original['app'])); + $this->assertEquals(1, count($response->original['web'])); + $this->assertEquals(1, count($response->original['worker'])); + $this->assertEquals($address->public_address, $response->original['worker'][0]['address']['public_address']); + } +} diff --git a/tests/Feature/StackTaskControllerTest.php b/tests/Feature/StackTaskControllerTest.php new file mode 100644 index 00000000..6ee26ff3 --- /dev/null +++ b/tests/Feature/StackTaskControllerTest.php @@ -0,0 +1,121 @@ +withoutExceptionHandling(); + } + + + public function test_stack_tasks_can_be_created() + { + Bus::fake(); + + $stack = factory(Stack::class)->create(); + + $response = $this->actingAs($stack->environment->project->user, 'api')->json( + 'post', '/api/stack/'.$stack->id.'/stack-tasks', [ + 'name' => 'Some Task', + 'user' => 'root', + 'commands' => [ + 'exit 1', + ], + ] + ); + + $response->assertStatus(201); + + Bus::assertDispatched(RunStackTask::class, function ($job) use ($response) { + return $job->task->id === $response->original->id; + }); + } + + + public function test_user_with_access_may_run_tasks() + { + Bus::fake(); + + $stack = factory(Stack::class)->create(); + $user = $this->user(); + $stack->environment->project->shareWith($user); + + $response = $this->withExceptionHandling()->actingAs($user, 'api')->json( + 'post', '/api/stack/'.$stack->id.'/stack-tasks', [ + 'name' => 'Some Task', + 'user' => 'cloud', + 'commands' => [ + 'exit 1', + ], + ] + ); + + $response->assertStatus(201); + + Bus::assertDispatched(RunStackTask::class); + } + + + public function test_user_may_not_run_tasks_without_ssh_permission() + { + Bus::fake(); + + $stack = factory(Stack::class)->create(); + $user = $this->user(); + $stack->environment->project->shareWith($user); + + $response = $this->withExceptionHandling()->actingAs($user, 'api')->json( + 'post', '/api/stack/'.$stack->id.'/stack-tasks', [ + 'name' => 'Some Task', + 'user' => 'root', + 'commands' => [ + 'exit 1', + ], + ] + ); + + $response->assertStatus(403); + + Bus::assertNotDispatched(RunStackTask::class); + } + + + public function test_users_with_ssh_access_still_cant_run_as_root() + { + Bus::fake(); + + $stack = factory(Stack::class)->create(); + $user = $this->user(); + $stack->environment->project->shareWith($user, ['ssh:server']); + + $response = $this->withExceptionHandling()->actingAs($user, 'api')->json( + 'post', '/api/stack/'.$stack->id.'/stack-tasks', [ + 'name' => 'Some Task', + 'user' => 'root', + 'commands' => [ + 'exit 1', + ], + ] + ); + + $response->assertStatus(403); + + Bus::assertNotDispatched(RunStackTask::class); + } +} diff --git a/tests/Feature/StackTaskTest.php b/tests/Feature/StackTaskTest.php new file mode 100644 index 00000000..cbea6ceb --- /dev/null +++ b/tests/Feature/StackTaskTest.php @@ -0,0 +1,175 @@ +withoutExceptionHandling(); + } + + + public function test_commands_are_distributed_to_appropriate_servers() + { + ShellProcessRunner::mock([ + (object) ['exitCode' => 0, 'output' => '', 'timedOut' => false], + (object) ['exitCode' => 0, 'output' => '', 'timedOut' => false], + ]); + + $stack = factory(Stack::class)->create(); + + $stack->webServers()->save(factory(WebServer::class)->make()); + $stack->workerServers()->save(factory(WorkerServer::class)->make()); + + $task = $stack->tasks()->create([ + 'name' => 'Task', + 'user' => 'cloud', + 'commands' => [ + 'echo 1', + 'web: echo 2', + 'worker: echo 3', + 'once: echo 4', + ], + ]); + + $task->run(); + + $serverTasks = $task->serverTasks; + + $this->assertEquals([ + 'echo 1', + 'echo 2', + 'echo 4', + ], $serverTasks[0]->commands); + + $this->assertInstanceOf(WebServer::class, $serverTasks[0]->taskable); + + $this->assertEquals([ + 'echo 1', + 'echo 3', + ], $serverTasks[1]->commands); + + $this->assertInstanceOf(WorkerServer::class, $serverTasks[1]->taskable); + } + + + public function test_server_tasks_are_not_created_if_no_applicable_commands() + { + ShellProcessRunner::mock([ + (object) ['exitCode' => 0, 'output' => '', 'timedOut' => false], + (object) ['exitCode' => 0, 'output' => '', 'timedOut' => false], + ]); + + $stack = factory(Stack::class)->create(); + + $stack->webServers()->save(factory(WebServer::class)->make()); + + $task = $stack->tasks()->create([ + 'name' => 'Task', + 'user' => 'cloud', + 'commands' => [ + 'worker: echo 1', + ], + ]); + + $task->run(); + + $serverTasks = $task->serverTasks; + + $this->assertCount(0, $serverTasks); + } + + + public function test_updating_status_is_properly_synced() + { + Event::fake(); + + $task = factory(ServerTask::class)->create(); + $task->markAsFinished(); + + Event::assertDispatched(ServerTaskFinished::class); + Event::assertDispatched(StackTaskFinished::class); + } + + + public function test_stack_task_not_marked_as_finished_if_server_tasks_still_running() + { + Event::fake(); + + $task = factory(StackTask::class)->create(); + $task->serverTasks()->save($serverTask1 = factory(ServerTask::class)->make()); + $task->serverTasks()->save($serverTask2 = factory(ServerTask::class)->make()); + + $serverTask1->markAsFinished(); + + $this->assertTrue($serverTask1->isFinished()); + Event::assertDispatched(ServerTaskFinished::class); + Event::assertNotDispatched(StackTaskFinished::class); + } + + + public function test_stack_test_is_updated_if_all_server_tasks_have_failed() + { + Event::fake(); + + $task = factory(ServerTask::class)->create(); + $task->markAsFailed(); + + Event::assertDispatched(ServerTaskFailed::class); + Event::assertDispatched(StackTaskFailed::class); + + $this->assertTrue($task->hasFailed()); + $this->assertTrue($task->stackTask->hasFailed()); + } + + + public function test_tasks_actually_execute() + { + $stack = factory(Stack::class)->create(); + + $stack->webServers()->save(factory(WebServer::class)->make([ + 'port' => 2288, + ])); + $stack->workerServers()->save(factory(WorkerServer::class)->make([ + 'port' => 2288, + ])); + + $task = $stack->tasks()->create([ + 'name' => 'Task', + 'user' => 'root', + 'commands' => [ + 'echo "Hello Stack Test" > /root/stack_test', + ], + ]); + + $task->run(); + + sleep(2); + + $output = $task->serverTasks[0]->task->retrieveOutput('/root/stack_test'); + $this->assertEquals('Hello Stack Test', $output); + + $output = $task->serverTasks[1]->task->retrieveOutput('/root/stack_test'); + $this->assertEquals('Hello Stack Test', $output); + } +} diff --git a/tests/Feature/StackTest.php b/tests/Feature/StackTest.php new file mode 100644 index 00000000..7044243b --- /dev/null +++ b/tests/Feature/StackTest.php @@ -0,0 +1,339 @@ +create(); + $stack->appServer()->save(factory(AppServer::class)->make([ + 'meta' => ['serves' => []] + ])); + + $this->assertFalse($stack->promotable()); + + + $stack = factory(Stack::class)->create(); + $stack->appServer()->save(factory(AppServer::class)->make([ + 'meta' => [] + ])); + + $this->assertFalse($stack->promotable()); + + $stack = factory(Stack::class)->create(); + $stack->webServers()->save(factory(WebServer::class)->make([ + 'meta' => ['serves' => ['laravel.com']], + ])); + $this->assertTrue($stack->promotable()); + } + + + public function test_can_retrieve_web_addresses() + { + $stack = factory(Stack::class)->create(); + + $stack->appServers()->save($server1 = factory(AppServer::class)->make()); + $server1->address()->save(factory(IpAddress::class)->make([ + 'private_address' => '192.168.1.1', + ])); + + $stack->webServers()->save($server2 = factory(WebServer::class)->make()); + $server2->address()->save(factory(IpAddress::class)->make([ + 'private_address' => '192.168.2.2', + ])); + + $this->assertEquals( + ['https://192.168.1.1', 'https://192.168.2.2'], + $stack->privateWebAddresses() + ); + } + + + public function test_can_determine_the_urls_the_stack_responds_to() + { + $stack = factory(Stack::class)->create(); + + $stack->appServers()->save($server1 = factory(AppServer::class)->make([ + 'meta' => ['serves' => ['laravel.com']], + ])); + + $stack->webServers()->save($server2 = factory(WebServer::class)->make([ + 'meta' => ['serves' => ['laravel.com']], + ])); + + $this->assertEquals( + collect([$stack->url.'.laravel.build'])->sort()->values()->all(), + collect($stack->shouldRespondTo())->sort()->values()->all() + ); + + $this->assertEquals( + collect([ + $stack->url.'.laravel.build:80', + $stack->url.'.laravel.build:443', + ])->sort()->values()->all(), + collect($stack->shouldRespondToWithPorts())->sort()->values()->all() + ); + + $stack->update(['promoted' => true]); + + $stack = $stack->fresh(); + + $this->assertEquals( + collect([$stack->url.'.laravel.build', 'laravel.com', 'www.laravel.com'])->sort()->values()->all(), + collect($stack->shouldRespondTo())->sort()->values()->all() + ); + + $this->assertEquals( + collect([ + 'laravel.com:80', + 'laravel.com:443', + 'www.laravel.com:80', + 'www.laravel.com:443', + $stack->url.'.laravel.build:80', + $stack->url.'.laravel.build:443', + ])->sort()->values()->all(), + collect($stack->shouldRespondToWithPorts())->sort()->values()->all() + ); + } + + + public function test_cannot_provision_if_already_provisioning() + { + Bus::fake(); + + $stack = factory(Stack::class)->create([ + 'environment_id' => 1, + 'status' => 'provisioning', + ]); + + $stack->provision(); + + Bus::assertNotDispatched(EnsureFloatingIpExists::class); + } + + + public function test_recommended_balancer_size_returns_proper_size() + { + $stack = factory(Stack::class)->create(); + + $stack->appServers()->save(factory(AppServer::class)->make(['size' => '2GB'])); + $stack->webServers()->save(factory(WebServer::class)->make(['size' => '2GB'])); + + $this->assertEquals('1GB', $stack->recommendedBalancerSize()); + } + + + public function test_stack_entrypoint_can_be_determined() + { + // With Balancers... + $stack = factory(Stack::class)->create(['balanced' => true]); + $stack->environment->project->balancers()->save($balancer = factory(Balancer::class)->make()); + $balancer->address()->save(factory(IpAddress::class)->make()); + + $this->assertEquals($balancer->address->public_address, $stack->entrypoint()); + + // With Multiple Balancers... + $stack = factory(Stack::class)->create(['balanced' => true]); + $stack->environment->project->balancers()->save($balancer = factory(Balancer::class)->make([ + 'size' => '2GB', + ])); + $stack->environment->project->balancers()->save($balancer2 = factory(Balancer::class)->make([ + 'size' => '8GB', + ])); + $balancer->address()->save(factory(IpAddress::class)->make()); + $balancer2->address()->save(factory(IpAddress::class)->make()); + + $this->assertEquals($balancer2->address->public_address, $stack->entrypoint()); + } + + + public function test_stack_entrypoint_returns_master_server_ip_with_only_web_servers() + { + $stack = factory(Stack::class)->create(); + $stack->webServers()->save($server = factory(WebServer::class)->make()); + $stack->webServers()->save($server2 = factory(WebServer::class)->make()); + $server->address()->save(factory(IpAddress::class)->make()); + $server2->address()->save(factory(IpAddress::class)->make()); + + $this->assertEquals($server->address->public_address, $stack->entrypoint()); + } + + + public function test_stack_can_be_deployed() + { + Bus::fake(); + + $stack = factory(Stack::class)->create([ + 'status' => 'provisioned', + ]); + $stack->deploymentLock()->release(); + $deployment = $stack->deploy('3b478197c05f0bb60ee484e01389bd2fff1d2bfc', ['build'], ['activate']); + + $this->assertTrue($stack->isDeploying()); + $this->assertNotNull($deployment->commit_hash); + + Bus::assertDispatched(MonitorDeployment::class); + } + + + public function test_deleting_stack_deletes_related_entities() + { + Bus::fake(); + + $stack = factory(Stack::class)->create(); + $stack->appServers()->save($server = factory(AppServer::class)->make()); + $stack->deployments()->save(factory(Deployment::class)->make()); + $stack->tasks()->save(factory(StackTask::class)->make()); + $stack->hooks()->save(factory(Hook::class)->make()); + + $stack->delete(); + + $this->assertCount(0, StackTask::all()); + $this->assertCount(0, Deployment::all()); + $this->assertCount(0, Hook::all()); + + Bus::assertDispatched(DeleteServerOnProvider::class, function ($job) use ($server) { + return $job->providerServerId == $server->providerServerId(); + }); + } + + + public function test_deleting_stack_detaches_from_all_databases() + { + Bus::fake(); + + $stack = factory(Stack::class)->create(); + $stack->project()->databases()->save($database = factory(Database::class)->make()); + $database->stacks()->sync([$stack->id]); + + $this->assertCount(1, $database->fresh()->stacks); + + $stack->delete(); + + $this->assertCount(0, $database->fresh()->stacks); + + Bus::assertDispatched(SyncNetwork::class, function ($job) use ($database) { + return $job->database->id === $database->id; + }); + } + + + public function test_deleting_stack_deletes_all_deployments() + { + $stack = factory(Stack::class)->create(); + $stack->deployments()->save($deployment = factory(Deployment::class)->make()); + $deployment->serverDeployments()->save($serverDeployment = factory(ServerDeployment::class)->make([ + 'deployment_id' => $deployment->id, + ])); + + $this->assertEquals(1, Deployment::count()); + $this->assertEquals(1, ServerDeployment::count()); + + $stack->delete(); + + $this->assertEquals(0, Deployment::count()); + $this->assertEquals(0, ServerDeployment::count()); + } + + + public function test_can_return_canonical_domain() + { + // Test non-www... + $stack = factory(Stack::class)->create(); + $stack->webServers()->save($server = factory(WebServer::class)->make([ + 'meta' => ['serves' => ['laravel.com']], + ])); + + $stack = $stack->fresh(); + + $this->assertEquals('laravel.com', $stack->canonicalDomain('www.laravel.com')); + $this->assertEquals('dev.laravel.com', $stack->canonicalDomain('dev.laravel.com')); + $this->assertEquals('www.laravel.com', $stack->nonCanonicalDomain('laravel.com')); + $this->assertEquals('dev.laravel.com', $stack->nonCanonicalDomain('dev.laravel.com')); + + + // Test www... + $stack = factory(Stack::class)->create(); + $stack->webServers()->save($server = factory(WebServer::class)->make([ + 'meta' => ['serves' => ['www.laravel.com']], + ])); + + $stack = $stack->fresh(); + + $this->assertEquals('www.laravel.com', $stack->canonicalDomain('laravel.com')); + $this->assertEquals('www.laravel.com', $stack->canonicalDomain('www.laravel.com')); + $this->assertEquals('dev.laravel.com', $stack->canonicalDomain('dev.laravel.com')); + $this->assertEquals('foobar.com', $stack->canonicalDomain('foobar.com')); + $this->assertTrue($stack->isCanonicalDomain('foobar.com')); + $this->assertTrue($stack->isCanonicalDomain('dev.laravel.com')); + + $this->assertEquals('laravel.com', $stack->nonCanonicalDomain('www.laravel.com')); + $this->assertEquals('laravel.com', $stack->nonCanonicalDomain('laravel.com')); + } + + + public function test_can_deploy_pending_deployments() + { + Bus::fake(); + + $hook = factory(Hook::class)->create(); + + $hook->stack->deploymentLock()->release(); + + $hook->stack->environment->update([ + 'name' => 'workbench', + ]); + + $hook->stack->update([ + 'name' => 'stack-1', + ]); + + $hook->stack->storePendingDeployment($hook, 'd8f05f1696032982dd8bf77aa9186d2aea744801'); + + $deployment = $hook->stack->deployPending(); + + $this->assertInstanceOf(Deployment::class, $deployment); + $this->assertNull($deployment->branch); + $this->assertEquals('d8f05f1696032982dd8bf77aa9186d2aea744801', $deployment->commit_hash); + } + + + public function test_pending_deployments_not_deployed_for_commits_that_dont_exist() + { + Bus::fake(); + + $hook = factory(Hook::class)->create(); + + $hook->stack->deploymentLock()->release(); + + $hook->stack->environment->update([ + 'name' => 'workbench', + ]); + + $hook->stack->storePendingDeployment($hook, 'asldjfalkjdkjdjsjasdlkj'); + + $this->assertNull($hook->stack->deployPending()); + } +} diff --git a/tests/Feature/StartBackgroundServicesCallbackTest.php b/tests/Feature/StartBackgroundServicesCallbackTest.php new file mode 100644 index 00000000..51dd62a8 --- /dev/null +++ b/tests/Feature/StartBackgroundServicesCallbackTest.php @@ -0,0 +1,104 @@ +withoutExceptionHandling(); + } + + + public function test_background_services_are_started_if_applicable() + { + Bus::fake(); + + $deployment = factory(ServerDeployment::class)->create(); + + $deployment->stack()->environment->update([ + 'name' => 'workbench', + ]); + + $deployment->deployment->update([ + 'stack_id' => $deployment->stack()->id, + 'daemons' => ['first'], + 'schedule' => ['first'], + ]); + + $callback = new StartBackgroundServices($deployment->id); + $callback->handle(factory(Task::class)->create()); + + $this->assertTrue($deployment->deployable->fresh()->daemonsAreRunning()); + + Bus::assertDispatched(StartScheduler::class, function ($job) use ($deployment) { + return $job->deployment->id === $deployment->id; + }); + + Bus::assertDispatched(RestartDaemons::class, function ($job) use ($deployment) { + return $job->deployment->id === $deployment->id; + }); + } + + + + public function test_background_services_are_not_started_if_in_production_and_are_not_already_running() + { + Bus::fake(); + + $deployment = factory(ServerDeployment::class)->create(); + $deployment->deployment->update([ + 'stack_id' => $deployment->stack()->id, + 'daemons' => ['first'], + 'schedule' => ['first'], + ]); + + $callback = new StartBackgroundServices($deployment->id); + $callback->handle(factory(Task::class)->create()); + + $this->assertFalse($deployment->deployable->fresh()->daemonsAreRunning()); + + Bus::assertNotDispatched(StartScheduler::class); + Bus::assertNotDispatched(RestartDaemons::class); + } + + + public function test_background_services_are_started_if_in_production_and_are_already_running() + { + Bus::fake(); + + $deployment = factory(ServerDeployment::class)->create(); + + $deployment->deployable->update([ + 'daemon_status' => 'running', + ]); + + $deployment->deployment->update([ + 'stack_id' => $deployment->stack()->id, + 'daemons' => ['first'], + 'schedule' => ['first'], + ]); + + $callback = new StartBackgroundServices($deployment->id); + $callback->handle(factory(Task::class)->create()); + + $this->assertTrue($deployment->deployable->fresh()->daemonsAreRunning()); + + Bus::assertDispatched(StartScheduler::class); + Bus::assertDispatched(RestartDaemons::class); + } +} diff --git a/tests/Feature/StorageProviderControllerTest.php b/tests/Feature/StorageProviderControllerTest.php new file mode 100644 index 00000000..d44c3708 --- /dev/null +++ b/tests/Feature/StorageProviderControllerTest.php @@ -0,0 +1,90 @@ +withoutExceptionHandling(); + } + + + public function test_storage_provider_can_be_created() + { + $user = $this->user(); + + $response = $this->actingAs($user, 'api')->json('POST', '/api/storage-provider', [ + 'name' => 'Personal', + 'type' => 'S3', + 'meta' => [ + 'key' => env('S3_KEY'), + 'secret' => env('S3_SECRET'), + 'region' => 'us-east-1', + 'bucket' => 'laravel-cloud-test', + ], + ]); + + $response->assertStatus(201); + $this->assertCount(1, $user->storageProviders); + + $storage = $user->storageProviders->first(); + $this->assertEquals('Personal', $storage->name); + $this->assertEquals('S3', $storage->type); + $this->assertInstanceOf(S3::class, $storage->client()); + } + + + public function test_storage_provider_can_be_validated() + { + $user = $this->user(); + + $response = $this->withExceptionHandling()->actingAs($user, 'api')->json('POST', '/api/storage-provider', [ + 'name' => 'Personal', + 'type' => 'GitHub', + 'meta' => [], + ]); + + $response->assertStatus(422); + $this->assertCount(0, $user->storageProviders); + } + + + public function test_storage_provider_can_be_deleted() + { + $provider = factory(StorageProvider::class)->create(); + $provider->backups()->save($backup = factory(DatabaseBackup::class)->make()); + + $response = $this->actingAs($provider->user, 'api')->json( + 'DELETE', "/api/storage-provider/{$provider->id}" + ); + + $response->assertStatus(200); + $this->assertCount(0, $provider->user->storageProviders()->get()); + $this->assertCount(0, $provider->backups); + } + + + public function test_only_owners_may_delete_storage_providers() + { + $provider = factory(StorageProvider::class)->create(); + $provider->backups()->save($backup = factory(DatabaseBackup::class)->make()); + + $response = $this->withExceptionHandling()->actingAs($this->user(), 'api')->json( + 'DELETE', "/api/storage-provider/{$provider->id}" + ); + + $response->assertStatus(403); + } +} diff --git a/tests/Feature/StoreDatabaseBackupJobTest.php b/tests/Feature/StoreDatabaseBackupJobTest.php new file mode 100644 index 00000000..0017c610 --- /dev/null +++ b/tests/Feature/StoreDatabaseBackupJobTest.php @@ -0,0 +1,46 @@ +withoutExceptionHandling(); + } + + + public function test_script_is_started() + { + $backup = factory(DatabaseBackup::class)->create(); + + $job = new StoreDatabaseBackup($backup); + + TaskFactory::shouldReceive('createFromScript')->once()->with( + Mockery::type(Database::class), Mockery::type(StoreDatabaseBackupScript::class), Mockery::on(function ($options) use ($backup) { + return $options['then'][0] instanceof CheckDatabaseBackup && + $options['then'][0]->id === $backup->id; + }) + )->andReturn($task = new FakeTask); + + $job->handle(); + + $this->assertTrue($task->ranInBackground); + } +} diff --git a/tests/Feature/StoreDatabaseBackupScriptTest.php b/tests/Feature/StoreDatabaseBackupScriptTest.php new file mode 100644 index 00000000..bf4d14e7 --- /dev/null +++ b/tests/Feature/StoreDatabaseBackupScriptTest.php @@ -0,0 +1,31 @@ +withoutExceptionHandling(); + } + + + public function test_script_can_be_rendered() + { + $backup = factory(DatabaseBackup::class)->create(); + + $script = new StoreDatabaseBackup($backup); + + $this->assertNotNull($script->script()); + } +} diff --git a/tests/Feature/SyncBalancerScriptTest.php b/tests/Feature/SyncBalancerScriptTest.php new file mode 100644 index 00000000..89b62f3d --- /dev/null +++ b/tests/Feature/SyncBalancerScriptTest.php @@ -0,0 +1,31 @@ +withoutExceptionHandling(); + } + + + public function test_script_can_be_rendered() + { + $balancer = factory(Balancer::class)->create(); + + $script = new SyncBalancer($balancer); + + $this->assertNotNull($script->script()); + } +} diff --git a/tests/Feature/SyncNetworkJobTest.php b/tests/Feature/SyncNetworkJobTest.php new file mode 100644 index 00000000..42599444 --- /dev/null +++ b/tests/Feature/SyncNetworkJobTest.php @@ -0,0 +1,117 @@ +withoutExceptionHandling(); + } + + + public function test_task_id_is_stored() + { + $database = factory(Database::class)->create(); + $database->networkLock()->release(); + + $appServer = factory(AppServer::class)->create(); + $appServer->address()->save(factory(IpAddress::class)->make()); + $appServer = $appServer->fresh(); + + $webServer = factory(WebServer::class)->create(['stack_id' => $appServer->stack->id]); + $webServer->address()->save(factory(IpAddress::class)->make()); + $webServer = $webServer->fresh(); + + $database->stacks()->sync([$appServer->stack->id]); + + $job = new SyncNetworkFakeJob($database); + $job->handle(); + + $database->networkLock()->release(); + + $this->assertEquals([ + $appServer->address->public_address, + $appServer->address->private_address, + $webServer->address->public_address, + $webServer->address->private_address, + ], $job->ipAddresses); + + $this->assertEquals( + $job->ipAddresses, $database->fresh()->allows_access_from + ); + } + + + public function test_job_is_released_if_database_is_not_provisioned() + { + $database = factory(Database::class)->create([ + 'status' => 'provisioning', + ]); + $database->networkLock()->release(); + + $job = new SyncNetworkFakeJob($database); + $job->handle(); + + $database->networkLock()->release(); + + $this->assertEquals(15, $job->released); + } + + + public function test_job_is_released_if_no_lock_can_be_acquired() + { + $database = factory(Database::class)->create([ + 'status' => 'provisioned', + ]); + $database->networkLock()->get(); + + $job = new SyncNetworkFakeJob($database); + $job->handle(); + + $database->networkLock()->release(); + + $this->assertEquals(15, $job->released); + } +} + + +class SyncNetworkFakeJob extends SyncNetwork +{ + public $database; + public $ipAddresses; + public $released; + public $deleted = false; + + protected function sync(Database $database) + { + $this->database = $database; + $this->ipAddresses = $database->shouldAllowAccessFrom(); + + return parent::sync($database); + } + + public function release($delay = 0) + { + $this->released = $delay; + } + + public function delete() + { + $this->deleted = true; + } +} diff --git a/tests/Feature/TaskTest.php b/tests/Feature/TaskTest.php new file mode 100644 index 00000000..1c1998f5 --- /dev/null +++ b/tests/Feature/TaskTest.php @@ -0,0 +1,81 @@ +create([ + 'port' => 2288, + ]); + + $task = $database->run(new GetCurrentDirectory); + + $this->assertEquals('finished', $task->status); + $this->assertEquals(0, $task->exit_code); + $this->assertEquals('/root', $task->output); + } + + + public function test_scripts_can_be_run_in_background() + { + $database = factory(Database::class)->create([ + 'port' => 2288, + ]); + + $task = $database->runInBackground(new WriteDummyFile); + + sleep(2); + + $output = $task->retrieveOutput('/root/dummy'); + + $this->assertEquals('Hello World', $output); + } + + + public function test_scripts_can_timeout() + { + $database = factory(Database::class)->create([ + 'port' => 2288, + ]); + + $task = $database->run(new Sleep, ['timeout' => 3]); + + $this->assertEquals('timeout', $task->status); + $this->assertNotEquals(0, $task->exit_code); + $this->assertEquals('', $task->output); + } + + + public function test_tasks_can_be_pruned() + { + $task1 = factory(Task::class)->create([ + 'created_at' => Carbon::now()->subDays(1), + ]); + + $task2 = factory(Task::class)->create([ + 'created_at' => Carbon::now()->subDays(3), + ]); + + $task3 = factory(Task::class)->create([ + 'created_at' => Carbon::now()->subDays(3), + ]); + + $this->assertEquals(2, Task::prune(Carbon::now()->subDays(2), 1)); + $this->assertEquals(1, Task::count()); + $this->assertEquals($task1->id, Task::all()->first()->id); + } +} diff --git a/tests/Feature/UpdateLastAlertTimestampForCollaboratorsListenerTest.php b/tests/Feature/UpdateLastAlertTimestampForCollaboratorsListenerTest.php new file mode 100644 index 00000000..52da3d68 --- /dev/null +++ b/tests/Feature/UpdateLastAlertTimestampForCollaboratorsListenerTest.php @@ -0,0 +1,40 @@ +withoutExceptionHandling(); + } + + + public function test_last_alert_received_at_timestamps_are_updated() + { + $project = factory(Project::class)->create(); + $project->shareWith($collaborator = factory(User::class)->create()); + + $this->assertNull($project->user->fresh()->last_alert_received_at); + $this->assertNull($collaborator->fresh()->last_alert_received_at); + + $alert = $project->alerts()->create([ + 'type' => 'Something', + 'exception' => 'exception', + 'meta' => [], + ]); + + $this->assertNotNull($project->user->fresh()->last_alert_received_at); + $this->assertNotNull($collaborator->fresh()->last_alert_received_at); + } +} diff --git a/tests/Feature/UpdateStackDnsRecordsJobTest.php b/tests/Feature/UpdateStackDnsRecordsJobTest.php new file mode 100644 index 00000000..f8141ee7 --- /dev/null +++ b/tests/Feature/UpdateStackDnsRecordsJobTest.php @@ -0,0 +1,55 @@ +withoutExceptionHandling(); + } + + + public function test_proper_stacks_are_updated() + { + $environment = factory(Environment::class)->create(); + + $environment->stacks()->save($stack1 = factory(Stack::class)->make([ + 'dns_address' => '192.168.1.1' + ])); + + $environment->stacks()->save($stack2 = factory(Stack::class)->make([ + 'dns_address' => '192.168.2.2', + ])); + + $job = new UpdateStackDnsRecords($environment->project, '192.168.1.1'); + + $dns = Mockery::mock(DnsProvider::class); + + $dns->shouldReceive('addRecord')->once()->with(Mockery::on(function ($stack) use ($stack1) { + return $stack->id === $stack1->id; + })); + + $dns->shouldReceive('addRecord')->never()->with(Mockery::on(function ($stack) use ($stack2) { + return $stack->id === $stack2->id; + })); + + $job->handle($dns); + + $this->assertTrue(true); + } +} diff --git a/tests/Feature/ValidDatabaseNameRuleTest.php b/tests/Feature/ValidDatabaseNameRuleTest.php new file mode 100644 index 00000000..fba337ff --- /dev/null +++ b/tests/Feature/ValidDatabaseNameRuleTest.php @@ -0,0 +1,37 @@ +create(); + $rule = new ValidDatabaseName($database->project); + $this->assertTrue($rule->passes('database', $database->name)); + } + + + public function test_rule_fails_when_database_doesnt_exist() + { + $database = factory(Database::class)->create(); + $rule = new ValidDatabaseName($database->project); + $this->assertFalse($rule->passes('database', 'missing')); + } + + + public function test_rule_passes_when_not_a_project() + { + $rule = new ValidDatabaseName('something'); + $this->assertTrue($rule->passes('database', 'missing')); + } +} diff --git a/tests/Feature/ValidRepositoryRuleTest.php b/tests/Feature/ValidRepositoryRuleTest.php new file mode 100644 index 00000000..85795b7c --- /dev/null +++ b/tests/Feature/ValidRepositoryRuleTest.php @@ -0,0 +1,48 @@ +create(); + $source->user->projects()->save($project = factory(Project::class)->make()); + $rule = new ValidRepository($source, 'master'); + $this->assertTrue($rule->passes('repository', 'laravel/laravel')); + } + + + public function test_rule_fails_when_repository_doesnt_exist() + { + $source = factory(SourceProvider::class)->create(); + $source->user->projects()->save($project = factory(Project::class)->make()); + $rule = new ValidRepository($source, 'master'); + $this->assertFalse($rule->passes('repository', 'something/missing')); + } + + + public function test_rule_fails_when_branch_doesnt_exist() + { + $source = factory(SourceProvider::class)->create(); + $source->user->projects()->save($project = factory(Project::class)->make()); + $rule = new ValidRepository($source, 'missing-branch-name-x1111'); + $this->assertFalse($rule->passes('repository', 'laravel/laravel')); + } + + + public function test_rule_fails_when_no_source_given() + { + $rule = new ValidRepository('something', 'master'); + $this->assertFalse($rule->passes('repository', 'laravel/laravel')); + } +} diff --git a/tests/Feature/ValidServeListRuleTest.php b/tests/Feature/ValidServeListRuleTest.php new file mode 100644 index 00000000..8191aef7 --- /dev/null +++ b/tests/Feature/ValidServeListRuleTest.php @@ -0,0 +1,39 @@ +create(); + $rule = new ValidServeList($stack->environment->project); + $this->assertTrue($rule->passes('web.serves', ['laravel.com'])); + } + + + public function test_rule_fails_when_being_served_by_other_environments() + { + $stack = factory(Stack::class)->create(); + + $project = $stack->environment->project; + $project->environments()->save($environment = factory(Environment::class)->make()); + $environment->stacks()->save($otherStack = factory(Stack::class)->make()); + $otherStack->webServers()->save(factory(WebServer::class)->make([ + 'meta' => ['serves' => ['laravel.com']], + ])); + + $rule = new ValidServeList($stack->environment->project); + $this->assertFalse($rule->passes('web.serves', ['laravel.com'])); + } +} diff --git a/tests/Feature/ValidSourceNameRuleTest.php b/tests/Feature/ValidSourceNameRuleTest.php new file mode 100644 index 00000000..6082d67e --- /dev/null +++ b/tests/Feature/ValidSourceNameRuleTest.php @@ -0,0 +1,39 @@ +create(); + $source->user->projects()->save($project = factory(Project::class)->make()); + $rule = new ValidSourceName($project); + $this->assertTrue($rule->passes('source', $source->name)); + } + + + public function test_rule_fails_when_source_doesnt_exist() + { + $source = factory(SourceProvider::class)->create(); + $source->user->projects()->save($project = factory(Project::class)->make()); + $rule = new ValidSourceName($project); + $this->assertFalse($rule->passes('source', 'missing')); + } + + + public function test_rule_passes_when_not_a_project() + { + $rule = new ValidSourceName('something'); + $this->assertTrue($rule->passes('source', 'missing')); + } +} diff --git a/tests/Feature/WaitForServersToFinishProvisioningJobTest.php b/tests/Feature/WaitForServersToFinishProvisioningJobTest.php new file mode 100644 index 00000000..ef481e8a --- /dev/null +++ b/tests/Feature/WaitForServersToFinishProvisioningJobTest.php @@ -0,0 +1,103 @@ +withoutExceptionHandling(); + } + + + public function test_job_is_not_deleted_when_stack_is_provisioned_but_balancer_isnt_finished() + { + $stack = factory(Stack::class)->create(); + $stack->project()->balancers()->save(factory(Balancer::class)->make([ + 'status' => 'provisioning', + ])); + $job = new WaitForServersToFinishProvisioningFakeJob($stack); + $job->handle(); + + $this->assertFalse($job->deleted); + } + + + public function test_job_is_deleted_when_stack_is_provisioned_and_no_balancers() + { + $stack = factory(Stack::class)->create(['initial_server_count' => 1]); + $stack->appServers()->save(factory(AppServer::class)->make([ + 'status' => 'provisioned', + ])); + $job = new WaitForServersToFinishProvisioningFakeJob($stack); + $job->handle(); + + $this->assertTrue($job->deleted); + } + + + public function test_job_is_deleted_when_stack_is_provisioned_and_has_provisioned_balancer() + { + $stack = factory(Stack::class)->create(['initial_server_count' => 1]); + $stack->appServers()->save(factory(AppServer::class)->make([ + 'status' => 'provisioned', + ])); + $stack->environment->project->balancers()->save($balancer = factory(Balancer::class)->make()); + $balancer->markAsProvisioned(); + $job = new WaitForServersToFinishProvisioningFakeJob($stack); + $job->handle(); + + $this->assertTrue($job->deleted); + } + + + public function test_job_fails_when_stack_is_old() + { + $stack = factory(Stack::class)->create(['created_at' => Carbon::now()->subDays(10)]); + $stack->appServers()->save(factory(AppServer::class)->create(['status' => 'pending'])); + $job = new WaitForServersToFinishProvisioningFakeJob($stack); + $job->handle(); + + $this->assertNotNull($job->exception); + } + + + public function test_job_fails_when_no_app_or_web_servers() + { + $stack = factory(Stack::class)->create(['initial_server_count' => 3]); + $job = new WaitForServersToFinishProvisioningFakeJob($stack); + $job->handle(); + + $this->assertNotNull($job->exception); + } +} + + +class WaitForServersToFinishProvisioningFakeJob extends WaitForServersToFinishProvisioning +{ + public $deleted = false; + public $exception; + + public function delete() + { + $this->deleted = true; + } + + public function fail($exception = null) + { + $this->exception = $exception; + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 00000000..ad4f1e56 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,54 @@ +create(); + } + + /** + * Refresh the SSH keys on the user instance. + * + * @param \App\User $user + * @return \App\User + */ + protected function refreshKeys($user) + { + return tap($user)->update([ + 'keypair' => SecureShellKey::make(), + 'worker_keypair' => SecureShellKey::make(), + ]); + } +} diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php new file mode 100644 index 00000000..06ece2c2 --- /dev/null +++ b/tests/Unit/ExampleTest.php @@ -0,0 +1,18 @@ +assertTrue(true); + } +} diff --git a/webpack.mix.js b/webpack.mix.js new file mode 100644 index 00000000..3e6f5712 --- /dev/null +++ b/webpack.mix.js @@ -0,0 +1,15 @@ +const { mix } = require('laravel-mix'); + +/* + |-------------------------------------------------------------------------- + | Mix Asset Management + |-------------------------------------------------------------------------- + | + | Mix provides a clean, fluent API for defining some Webpack build steps + | for your Laravel application. By default, we are compiling the Sass + | file for the application as well as bundling up all the JS files. + | + */ + +mix.js('resources/assets/js/app.js', 'public/js') + .sass('resources/assets/sass/app.scss', 'public/css'); diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 00000000..ad2de2b2 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,6176 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 +abbrev@1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f" + +accepts@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" + dependencies: + mime-types "~2.1.11" + negotiator "0.6.1" + +acorn-dynamic-import@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4" + dependencies: + acorn "^4.0.3" + +acorn@^4.0.3: + version "4.0.13" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" + +acorn@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.1.tgz#53fe161111f912ab999ee887a90a0bc52822fd75" + +adjust-sourcemap-loader@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-1.1.0.tgz#412d92404eb61e4113635012cba53a33d008e0e2" + dependencies: + assert "^1.3.0" + camelcase "^1.2.1" + loader-utils "^1.0.2" + lodash.assign "^4.0.1" + lodash.defaults "^3.1.2" + object-path "^0.9.2" + regex-parser "^2.2.1" + +ajv-keywords@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0" + +ajv@^4.9.1: + version "4.11.8" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + +ajv@^5.0.0, ajv@^5.1.5: + version "5.2.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + json-schema-traverse "^0.3.0" + json-stable-stringify "^1.0.1" + +align-text@^0.1.1, align-text@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" + dependencies: + kind-of "^3.0.2" + longest "^1.0.1" + repeat-string "^1.5.2" + +alphanum-sort@^1.0.1, alphanum-sort@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + +ansi-html@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.1.0.tgz#09c202d5c917ec23188caa5c9cb9179cd9547750" + dependencies: + color-convert "^1.0.0" + +ansicolors@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.2.1.tgz#be089599097b74a5c9c4a84a0cdbcdb62bd87aef" + +anymatch@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507" + dependencies: + arrify "^1.0.0" + micromatch "^2.1.5" + +aproba@^1.0.3: + version "1.1.2" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.2.tgz#45c6629094de4e96f693ef7eab74ae079c240fc1" + +archive-type@^3.0.0, archive-type@^3.0.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/archive-type/-/archive-type-3.2.0.tgz#9cd9c006957ebe95fadad5bd6098942a813737f6" + dependencies: + file-type "^3.1.0" + +are-we-there-yet@~1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +argparse@^1.0.7: + version "1.0.9" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" + dependencies: + arr-flatten "^1.0.1" + +arr-flatten@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + +array-differ@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + +array-flatten@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.0, array-uniq@^1.0.1, array-uniq@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +array-unique@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + +asn1.js@^4.0.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.1.tgz#48ba240b45a9280e94748990ba597d216617fd40" + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + +assert-plus@^1.0.0, assert-plus@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +assert@^1.1.1, assert@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" + dependencies: + util "0.10.3" + +ast-types@0.9.6: + version "0.9.6" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9" + +async-each-series@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/async-each-series/-/async-each-series-1.1.0.tgz#f42fd8155d38f21a5b8ea07c28e063ed1700b138" + +async-each@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" + +async-foreach@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" + +async@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + +async@^2.1.2, async@^2.1.5, async@^2.4.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" + dependencies: + lodash "^4.14.0" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +atob@~1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/atob/-/atob-1.1.3.tgz#95f13629b12c3a51a5d215abdce2aa9f32f80773" + +autoprefixer@^6.3.1: + version "6.7.7" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014" + dependencies: + browserslist "^1.7.6" + caniuse-db "^1.0.30000634" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^5.2.16" + postcss-value-parser "^3.2.3" + +autoprefixer@^7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.1.2.tgz#fbeaf07d48fd878e0682bf7cbeeade728adb2b18" + dependencies: + browserslist "^2.1.5" + caniuse-lite "^1.0.30000697" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^6.0.6" + postcss-value-parser "^3.2.3" + +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" + +aws4@^1.2.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" + +axios@^0.16.2: + version "0.16.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.16.2.tgz#ba4f92f17167dfbab40983785454b9ac149c3c6d" + dependencies: + follow-redirects "^1.2.3" + is-buffer "^1.1.5" + +babel-code-frame@^6.11.0, babel-code-frame@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" + dependencies: + chalk "^1.1.0" + esutils "^2.0.2" + js-tokens "^3.0.0" + +babel-core@^6.24.1: + version "6.25.0" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.25.0.tgz#7dd42b0463c742e9d5296deb3ec67a9322dad729" + dependencies: + babel-code-frame "^6.22.0" + babel-generator "^6.25.0" + babel-helpers "^6.24.1" + babel-messages "^6.23.0" + babel-register "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.25.0" + babel-traverse "^6.25.0" + babel-types "^6.25.0" + babylon "^6.17.2" + convert-source-map "^1.1.0" + debug "^2.1.1" + json5 "^0.5.0" + lodash "^4.2.0" + minimatch "^3.0.2" + path-is-absolute "^1.0.0" + private "^0.1.6" + slash "^1.0.0" + source-map "^0.5.0" + +babel-generator@^6.25.0: + version "6.25.0" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.25.0.tgz#33a1af70d5f2890aeb465a4a7793c1df6a9ea9fc" + dependencies: + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-types "^6.25.0" + detect-indent "^4.0.0" + jsesc "^1.3.0" + lodash "^4.2.0" + source-map "^0.5.0" + trim-right "^1.0.1" + +babel-helper-builder-binary-assignment-operator-visitor@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664" + dependencies: + babel-helper-explode-assignable-expression "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-call-delegate@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d" + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-define-map@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.24.1.tgz#7a9747f258d8947d32d515f6aa1c7bd02204a080" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + lodash "^4.2.0" + +babel-helper-explode-assignable-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa" + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-function-name@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" + dependencies: + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-get-function-arity@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-hoist-variables@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-optimise-call-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-regex@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.24.1.tgz#d36e22fab1008d79d88648e32116868128456ce8" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + lodash "^4.2.0" + +babel-helper-remap-async-to-generator@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-replace-supers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a" + dependencies: + babel-helper-optimise-call-expression "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helpers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-loader@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.1.tgz#b87134c8b12e3e4c2a94e0546085bc680a2b8488" + dependencies: + find-cache-dir "^1.0.0" + loader-utils "^1.0.2" + mkdirp "^0.5.1" + +babel-messages@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-check-es2015-constants@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-syntax-async-functions@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" + +babel-plugin-syntax-exponentiation-operator@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" + +babel-plugin-syntax-trailing-function-commas@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" + +babel-plugin-transform-async-to-generator@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761" + dependencies: + babel-helper-remap-async-to-generator "^6.24.1" + babel-plugin-syntax-async-functions "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-arrow-functions@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoped-functions@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoping@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.24.1.tgz#76c295dc3a4741b1665adfd3167215dcff32a576" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + lodash "^4.2.0" + +babel-plugin-transform-es2015-classes@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db" + dependencies: + babel-helper-define-map "^6.24.1" + babel-helper-function-name "^6.24.1" + babel-helper-optimise-call-expression "^6.24.1" + babel-helper-replace-supers "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-computed-properties@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-destructuring@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-duplicate-keys@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-for-of@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-function-name@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-literals@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154" + dependencies: + babel-plugin-transform-es2015-modules-commonjs "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.1.tgz#d3e310b40ef664a36622200097c6d440298f2bfe" + dependencies: + babel-plugin-transform-strict-mode "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-modules-systemjs@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23" + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-umd@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468" + dependencies: + babel-plugin-transform-es2015-modules-amd "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-object-super@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d" + dependencies: + babel-helper-replace-supers "^6.24.1" + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-parameters@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b" + dependencies: + babel-helper-call-delegate "^6.24.1" + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-shorthand-properties@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-spread@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-sticky-regex@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc" + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-template-literals@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-typeof-symbol@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-unicode-regex@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9" + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + regexpu-core "^2.0.0" + +babel-plugin-transform-exponentiation-operator@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e" + dependencies: + babel-helper-builder-binary-assignment-operator-visitor "^6.24.1" + babel-plugin-syntax-exponentiation-operator "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-regenerator@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.24.1.tgz#b8da305ad43c3c99b4848e4fe4037b770d23c418" + dependencies: + regenerator-transform "0.9.11" + +babel-plugin-transform-strict-mode@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-preset-env@^1.5.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.6.0.tgz#2de1c782a780a0a5d605d199c957596da43c44e4" + dependencies: + babel-plugin-check-es2015-constants "^6.22.0" + babel-plugin-syntax-trailing-function-commas "^6.22.0" + babel-plugin-transform-async-to-generator "^6.22.0" + babel-plugin-transform-es2015-arrow-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoped-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoping "^6.23.0" + babel-plugin-transform-es2015-classes "^6.23.0" + babel-plugin-transform-es2015-computed-properties "^6.22.0" + babel-plugin-transform-es2015-destructuring "^6.23.0" + babel-plugin-transform-es2015-duplicate-keys "^6.22.0" + babel-plugin-transform-es2015-for-of "^6.23.0" + babel-plugin-transform-es2015-function-name "^6.22.0" + babel-plugin-transform-es2015-literals "^6.22.0" + babel-plugin-transform-es2015-modules-amd "^6.22.0" + babel-plugin-transform-es2015-modules-commonjs "^6.23.0" + babel-plugin-transform-es2015-modules-systemjs "^6.23.0" + babel-plugin-transform-es2015-modules-umd "^6.23.0" + babel-plugin-transform-es2015-object-super "^6.22.0" + babel-plugin-transform-es2015-parameters "^6.23.0" + babel-plugin-transform-es2015-shorthand-properties "^6.22.0" + babel-plugin-transform-es2015-spread "^6.22.0" + babel-plugin-transform-es2015-sticky-regex "^6.22.0" + babel-plugin-transform-es2015-template-literals "^6.22.0" + babel-plugin-transform-es2015-typeof-symbol "^6.23.0" + babel-plugin-transform-es2015-unicode-regex "^6.22.0" + babel-plugin-transform-exponentiation-operator "^6.22.0" + babel-plugin-transform-regenerator "^6.22.0" + browserslist "^2.1.2" + invariant "^2.2.2" + semver "^5.3.0" + +babel-register@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.24.1.tgz#7e10e13a2f71065bdfad5a1787ba45bca6ded75f" + dependencies: + babel-core "^6.24.1" + babel-runtime "^6.22.0" + core-js "^2.4.0" + home-or-tmp "^2.0.0" + lodash "^4.2.0" + mkdirp "^0.5.1" + source-map-support "^0.4.2" + +babel-runtime@^6.18.0, babel-runtime@^6.22.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b" + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.10.0" + +babel-template@^6.24.1, babel-template@^6.25.0: + version "6.25.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.25.0.tgz#665241166b7c2aa4c619d71e192969552b10c071" + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.25.0" + babel-types "^6.25.0" + babylon "^6.17.2" + lodash "^4.2.0" + +babel-traverse@^6.24.1, babel-traverse@^6.25.0: + version "6.25.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.25.0.tgz#2257497e2fcd19b89edc13c4c91381f9512496f1" + dependencies: + babel-code-frame "^6.22.0" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-types "^6.25.0" + babylon "^6.17.2" + debug "^2.2.0" + globals "^9.0.0" + invariant "^2.2.0" + lodash "^4.2.0" + +babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.25.0: + version "6.25.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.25.0.tgz#70afb248d5660e5d18f811d91c8303b54134a18e" + dependencies: + babel-runtime "^6.22.0" + esutils "^2.0.2" + lodash "^4.2.0" + to-fast-properties "^1.0.1" + +babylon@^6.17.2: + version "6.17.4" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.17.4.tgz#3e8b7402b88d22c3423e137a1577883b15ff869a" + +balanced-match@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +base64-js@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886" + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + +bcrypt-pbkdf@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" + dependencies: + tweetnacl "^0.14.3" + +beeper@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809" + +big.js@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.1.3.tgz#4cada2193652eb3ca9ec8e55c9015669c9806978" + +bin-build@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/bin-build/-/bin-build-2.2.0.tgz#11f8dd61f70ffcfa2bdcaa5b46f5e8fedd4221cc" + dependencies: + archive-type "^3.0.1" + decompress "^3.0.0" + download "^4.1.2" + exec-series "^1.0.0" + rimraf "^2.2.6" + tempfile "^1.0.0" + url-regex "^3.0.0" + +bin-check@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/bin-check/-/bin-check-2.0.0.tgz#86f8e6f4253893df60dc316957f5af02acb05930" + dependencies: + executable "^1.0.0" + +bin-version-check@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/bin-version-check/-/bin-version-check-2.1.0.tgz#e4e5df290b9069f7d111324031efc13fdd11a5b0" + dependencies: + bin-version "^1.0.0" + minimist "^1.1.0" + semver "^4.0.3" + semver-truncate "^1.0.0" + +bin-version@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/bin-version/-/bin-version-1.0.4.tgz#9eb498ee6fd76f7ab9a7c160436f89579435d78e" + dependencies: + find-versions "^1.0.0" + +bin-wrapper@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/bin-wrapper/-/bin-wrapper-3.0.2.tgz#67d3306262e4b1a5f2f88ee23464f6a655677aeb" + dependencies: + bin-check "^2.0.0" + bin-version-check "^2.1.0" + download "^4.0.0" + each-async "^1.1.1" + lazy-req "^1.0.0" + os-filter-obj "^1.0.0" + +binary-extensions@^1.0.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.8.0.tgz#48ec8d16df4377eae5fa5884682480af4d95c774" + +bl@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e" + dependencies: + readable-stream "^2.0.5" + +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + dependencies: + inherits "~2.0.0" + +bluebird@^3.0.5, bluebird@^3.1.1: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: + version "4.11.7" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.7.tgz#ddb048e50d9482790094c13eb3fcfc833ce7ab46" + +bonjour@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" + dependencies: + array-flatten "^2.1.0" + deep-equal "^1.0.1" + dns-equal "^1.0.0" + dns-txt "^2.0.2" + multicast-dns "^6.0.1" + multicast-dns-service-types "^1.1.0" + +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + dependencies: + hoek "2.x.x" + +bootstrap-sass@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/bootstrap-sass/-/bootstrap-sass-3.3.7.tgz#6596c7ab40f6637393323ab0bc80d064fc630498" + +brace-expansion@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^1.8.2: + version "1.8.5" + resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" + dependencies: + expand-range "^1.8.1" + preserve "^0.2.0" + repeat-element "^1.1.2" + +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.0.6.tgz#5e7725dbdef1fd5930d4ebab48567ce451c48a0a" + dependencies: + buffer-xor "^1.0.2" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + inherits "^2.0.1" + +browserify-cipher@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a" + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd" + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + +browserify-rsa@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" + dependencies: + bn.js "^4.1.1" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.2" + elliptic "^6.0.0" + inherits "^2.0.1" + parse-asn1 "^5.0.0" + +browserify-zlib@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d" + dependencies: + pako "~0.2.0" + +browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: + version "1.7.7" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9" + dependencies: + caniuse-db "^1.0.30000639" + electron-to-chromium "^1.2.7" + +browserslist@^2.1.2, browserslist@^2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.1.5.tgz#e882550df3d1cd6d481c1a3e0038f2baf13a4711" + dependencies: + caniuse-lite "^1.0.30000684" + electron-to-chromium "^1.3.14" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + +buffer-indexof@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.0.tgz#f54f647c4f4e25228baa656a2e57e43d5f270982" + +buffer-to-vinyl@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-to-vinyl/-/buffer-to-vinyl-1.1.0.tgz#00f15faee3ab7a1dda2cde6d9121bffdd07b2262" + dependencies: + file-type "^3.1.0" + readable-stream "^2.0.2" + uuid "^2.0.1" + vinyl "^1.0.0" + +buffer-xor@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + +buffer@^4.3.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +builtin-modules@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + +bytes@2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.5.0.tgz#4c9423ea2d252c270c41b2bdefeff9bb6b62c06a" + +camel-case@3.0.x: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" + dependencies: + no-case "^2.2.0" + upper-case "^1.1.1" + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^1.0.2, camelcase@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + +camelcase@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" + +camelcase@^4.0.0, camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + +caniuse-api@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.6.1.tgz#b534e7c734c4f81ec5fbe8aca2ad24354b962c6c" + dependencies: + browserslist "^1.3.6" + caniuse-db "^1.0.30000529" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: + version "1.0.30000701" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000701.tgz#2e32b06993bf3dbd90b43d93f04e26d11afddcba" + +caniuse-lite@^1.0.30000684, caniuse-lite@^1.0.30000697: + version "1.0.30000701" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000701.tgz#9d673cf6b74dcb3d5c21d213176b011ac6a45baa" + +capture-stack-trace@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d" + +cardinal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-1.0.0.tgz#50e21c1b0aa37729f9377def196b5a9cec932ee9" + dependencies: + ansicolors "~0.2.1" + redeyed "~1.0.0" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + +caw@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/caw/-/caw-1.2.0.tgz#ffb226fe7efc547288dc62ee3e97073c212d1034" + dependencies: + get-proxy "^1.0.1" + is-obj "^1.0.0" + object-assign "^3.0.0" + tunnel-agent "^0.4.0" + +center-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" + dependencies: + align-text "^0.1.3" + lazy-cache "^1.0.3" + +chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.0.1.tgz#dbec49436d2ae15f536114e76d14656cdbc0f44d" + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + +charenc@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + +chokidar@^1.4.3, chokidar@^1.6.0, chokidar@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" + dependencies: + anymatch "^1.3.0" + async-each "^1.0.0" + glob-parent "^2.0.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^2.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + optionalDependencies: + fsevents "^1.0.0" + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +clap@^1.0.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/clap/-/clap-1.2.0.tgz#59c90fe3e137104746ff19469a27a634ff68c857" + dependencies: + chalk "^1.1.3" + +clean-css@^4.1.3, clean-css@4.1.x: + version "4.1.7" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.1.7.tgz#b9aea4f85679889cf3eae8b40349ec4ebdfdd032" + dependencies: + source-map "0.5.x" + +cli-table@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" + dependencies: + colors "1.0.3" + +cli-usage@^0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/cli-usage/-/cli-usage-0.1.4.tgz#7c01e0dc706c234b39c933838c8e20b2175776e2" + dependencies: + marked "^0.3.6" + marked-terminal "^1.6.2" + +cliui@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" + dependencies: + center-align "^0.1.1" + right-align "^0.1.1" + wordwrap "0.0.2" + +cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + +clone-deep@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.3.0.tgz#348c61ae9cdbe0edfe053d91ff4cc521d790ede8" + dependencies: + for-own "^1.0.0" + is-plain-object "^2.0.1" + kind-of "^3.2.2" + shallow-clone "^0.1.2" + +clone-stats@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1" + +clone@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/clone/-/clone-0.2.0.tgz#c6126a90ad4f72dbf5acdb243cc37724fe93fc1f" + +clone@^1.0.0, clone@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +co@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/co/-/co-3.1.0.tgz#4ea54ea5a08938153185e15210c68d9092bc1b78" + +coa@~1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd" + dependencies: + q "^1.1.2" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +color-convert@^1.0.0, color-convert@^1.3.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" + dependencies: + color-name "^1.1.1" + +color-name@^1.0.0, color-name@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.2.tgz#5c8ab72b64bd2215d617ae9559ebb148475cf98d" + +color-string@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" + dependencies: + color-name "^1.0.0" + +color@^0.11.0: + version "0.11.4" + resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764" + dependencies: + clone "^1.0.2" + color-convert "^1.3.0" + color-string "^0.3.0" + +colormin@^1.0.5: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colormin/-/colormin-1.1.2.tgz#ea2f7420a72b96881a38aae59ec124a6f7298133" + dependencies: + color "^0.11.0" + css-color-names "0.0.4" + has "^1.0.1" + +colors@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" + +colors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" + +combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + dependencies: + delayed-stream "~1.0.0" + +commander@^2.9.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" + +commander@~2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4" + dependencies: + graceful-readlink ">= 1.0.0" + +commander@~2.9.0, commander@2.9.x: + version "2.9.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" + dependencies: + graceful-readlink ">= 1.0.0" + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + +compressible@~2.0.10: + version "2.0.10" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.10.tgz#feda1c7f7617912732b29bf8cf26252a20b9eecd" + dependencies: + mime-db ">= 1.27.0 < 2" + +compression@^1.5.2: + version "1.7.0" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.0.tgz#030c9f198f1643a057d776a738e922da4373012d" + dependencies: + accepts "~1.3.3" + bytes "2.5.0" + compressible "~2.0.10" + debug "2.6.8" + on-headers "~1.0.1" + safe-buffer "5.1.1" + vary "~1.1.1" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@^1.4.6, concat-stream@^1.4.7: + version "1.6.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +concatenate@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/concatenate/-/concatenate-0.0.2.tgz#0b49d6e8c41047d7728cdc8d62a086623397b49f" + dependencies: + globs "^0.1.2" + +config-chain@~1.1.5: + version "1.1.11" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.11.tgz#aba09747dfbe4c3e70e766a6e41586e1859fc6f2" + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + +connect-history-api-fallback@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.3.0.tgz#e51d17f8f0ef0db90a64fdb47de3051556e9f169" + +console-browserify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" + dependencies: + date-now "^0.1.4" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + +console-stream@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/console-stream/-/console-stream-0.1.1.tgz#a095fe07b20465955f2fafd28b5d72bccd949d44" + +consolidate@^0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.14.5.tgz#5a25047bc76f73072667c8cb52c989888f494c63" + dependencies: + bluebird "^3.1.1" + +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + +content-type@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" + +convert-source-map@^0.3.3: + version "0.3.5" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190" + +convert-source-map@^1.1.0, convert-source-map@^1.1.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + +core-js@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +cosmiconfig@^2.1.0, cosmiconfig@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.1.3.tgz#952771eb0dddc1cb3fa2f6fbe51a522e93b3ee0a" + dependencies: + is-directory "^0.3.1" + js-yaml "^3.4.3" + minimist "^1.2.0" + object-assign "^4.1.0" + os-homedir "^1.0.1" + parse-json "^2.2.0" + require-from-string "^1.1.0" + +create-ecdh@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" + dependencies: + bn.js "^4.1.0" + elliptic "^6.0.0" + +create-error-class@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" + dependencies: + capture-stack-trace "^1.0.0" + +create-hash@^1.1.0, create-hash@^1.1.1, create-hash@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd" + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + ripemd160 "^2.0.0" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: + version "1.1.6" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06" + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +cross-env@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.0.1.tgz#ff4e72ea43b47da2486b43a7f2043b2609e44913" + dependencies: + cross-spawn "^5.1.0" + is-windows "^1.0.0" + +cross-spawn@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + +cross-spawn@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + +cross-spawn@^5.0.1, cross-spawn@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +crypt@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + dependencies: + boom "2.x.x" + +crypto-browserify@^3.11.0: + version "3.11.1" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.1.tgz#948945efc6757a400d6e5e5af47194d10064279f" + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + +css-color-names@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" + +css-loader@^0.28.3: + version "0.28.4" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.4.tgz#6cf3579192ce355e8b38d5f42dd7a1f2ec898d0f" + dependencies: + babel-code-frame "^6.11.0" + css-selector-tokenizer "^0.7.0" + cssnano ">=2.6.1 <4" + icss-utils "^2.1.0" + loader-utils "^1.0.2" + lodash.camelcase "^4.3.0" + object-assign "^4.0.1" + postcss "^5.0.6" + postcss-modules-extract-imports "^1.0.0" + postcss-modules-local-by-default "^1.0.1" + postcss-modules-scope "^1.0.0" + postcss-modules-values "^1.1.0" + postcss-value-parser "^3.3.0" + source-list-map "^0.1.7" + +css-selector-tokenizer@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86" + dependencies: + cssesc "^0.1.0" + fastparse "^1.1.1" + regexpu-core "^1.0.0" + +css@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/css/-/css-2.2.1.tgz#73a4c81de85db664d4ee674f7d47085e3b2d55dc" + dependencies: + inherits "^2.0.1" + source-map "^0.1.38" + source-map-resolve "^0.3.0" + urix "^0.1.0" + +cssesc@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4" + +"cssnano@>=2.6.1 <4": + version "3.10.0" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-3.10.0.tgz#4f38f6cea2b9b17fa01490f23f1dc68ea65c1c38" + dependencies: + autoprefixer "^6.3.1" + decamelize "^1.1.2" + defined "^1.0.0" + has "^1.0.1" + object-assign "^4.0.1" + postcss "^5.0.14" + postcss-calc "^5.2.0" + postcss-colormin "^2.1.8" + postcss-convert-values "^2.3.4" + postcss-discard-comments "^2.0.4" + postcss-discard-duplicates "^2.0.1" + postcss-discard-empty "^2.0.1" + postcss-discard-overridden "^0.1.1" + postcss-discard-unused "^2.2.1" + postcss-filter-plugins "^2.0.0" + postcss-merge-idents "^2.1.5" + postcss-merge-longhand "^2.0.1" + postcss-merge-rules "^2.0.3" + postcss-minify-font-values "^1.0.2" + postcss-minify-gradients "^1.0.1" + postcss-minify-params "^1.0.4" + postcss-minify-selectors "^2.0.4" + postcss-normalize-charset "^1.1.0" + postcss-normalize-url "^3.0.7" + postcss-ordered-values "^2.1.0" + postcss-reduce-idents "^2.2.2" + postcss-reduce-initial "^1.0.0" + postcss-reduce-transforms "^1.0.3" + postcss-svgo "^2.1.1" + postcss-unique-selectors "^2.0.2" + postcss-value-parser "^3.2.3" + postcss-zindex "^2.0.1" + +csso@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/csso/-/csso-2.3.2.tgz#ddd52c587033f49e94b71fc55569f252e8ff5f85" + dependencies: + clap "^1.0.9" + source-map "^0.5.3" + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + dependencies: + array-find-index "^1.0.1" + +d@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" + dependencies: + es5-ext "^0.10.9" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +date-now@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" + +dateformat@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.0.0.tgz#2743e3abb5c3fc2462e527dca445e04e9f4dee17" + +de-indent@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" + +debug@^2.1.1, debug@^2.2.0, debug@^2.4.5, debug@^2.6.8, debug@2.6.8: + version "2.6.8" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" + dependencies: + ms "2.0.0" + +debug@2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e" + dependencies: + ms "2.0.0" + +decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + +decompress-tar@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-3.1.0.tgz#217c789f9b94450efaadc5c5e537978fc333c466" + dependencies: + is-tar "^1.0.0" + object-assign "^2.0.0" + strip-dirs "^1.0.0" + tar-stream "^1.1.1" + through2 "^0.6.1" + vinyl "^0.4.3" + +decompress-tarbz2@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/decompress-tarbz2/-/decompress-tarbz2-3.1.0.tgz#8b23935681355f9f189d87256a0f8bdd96d9666d" + dependencies: + is-bzip2 "^1.0.0" + object-assign "^2.0.0" + seek-bzip "^1.0.3" + strip-dirs "^1.0.0" + tar-stream "^1.1.1" + through2 "^0.6.1" + vinyl "^0.4.3" + +decompress-targz@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/decompress-targz/-/decompress-targz-3.1.0.tgz#b2c13df98166268991b715d6447f642e9696f5a0" + dependencies: + is-gzip "^1.0.0" + object-assign "^2.0.0" + strip-dirs "^1.0.0" + tar-stream "^1.1.1" + through2 "^0.6.1" + vinyl "^0.4.3" + +decompress-unzip@^3.0.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/decompress-unzip/-/decompress-unzip-3.4.0.tgz#61475b4152066bbe3fee12f9d629d15fe6478eeb" + dependencies: + is-zip "^1.0.0" + read-all-stream "^3.0.0" + stat-mode "^0.2.0" + strip-dirs "^1.0.0" + through2 "^2.0.0" + vinyl "^1.0.0" + yauzl "^2.2.1" + +decompress@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/decompress/-/decompress-3.0.0.tgz#af1dd50d06e3bfc432461d37de11b38c0d991bed" + dependencies: + buffer-to-vinyl "^1.0.0" + concat-stream "^1.4.6" + decompress-tar "^3.0.0" + decompress-tarbz2 "^3.0.0" + decompress-targz "^3.0.0" + decompress-unzip "^3.0.0" + stream-combiner2 "^1.1.1" + vinyl-assign "^1.0.1" + vinyl-fs "^2.2.0" + +deep-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + +deep-extend@~0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" + +defined@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" + +del@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5" + dependencies: + globby "^6.1.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + p-map "^1.1.1" + pify "^3.0.0" + rimraf "^2.2.8" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +depd@~1.1.0, depd@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" + +des.js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + +detect-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" + dependencies: + repeating "^2.0.0" + +detect-node@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127" + +diffie-hellman@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + +dns-packet@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.1.1.tgz#2369d45038af045f3898e6fa56862aed3f40296c" + dependencies: + ip "^1.1.0" + safe-buffer "^5.0.1" + +dns-txt@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" + dependencies: + buffer-indexof "^1.0.0" + +domain-browser@^1.1.1: + version "1.1.7" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" + +dotenv-expand@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-4.0.1.tgz#68fddc1561814e0a10964111057ff138ced7d7a8" + +dotenv@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d" + +download@^4.0.0, download@^4.1.2: + version "4.4.3" + resolved "https://registry.yarnpkg.com/download/-/download-4.4.3.tgz#aa55fdad392d95d4b68e8c2be03e0c2aa21ba9ac" + dependencies: + caw "^1.0.1" + concat-stream "^1.4.7" + each-async "^1.0.0" + filenamify "^1.0.1" + got "^5.0.0" + gulp-decompress "^1.2.0" + gulp-rename "^1.2.0" + is-url "^1.2.0" + object-assign "^4.0.1" + read-all-stream "^3.0.0" + readable-stream "^2.0.2" + stream-combiner2 "^1.1.1" + vinyl "^1.0.0" + vinyl-fs "^2.2.0" + ware "^1.2.0" + +duplexer2@^0.1.4, duplexer2@~0.1.0: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + dependencies: + readable-stream "^2.0.2" + +duplexer2@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.0.2.tgz#c614dcf67e2fb14995a91711e5a617e8a60a31db" + dependencies: + readable-stream "~1.1.9" + +duplexify@^3.2.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.0.tgz#1aa773002e1578457e9d9d4a50b0ccaaebcbd604" + dependencies: + end-of-stream "1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +each-async@^1.0.0, each-async@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/each-async/-/each-async-1.1.1.tgz#dee5229bdf0ab6ba2012a395e1b869abf8813473" + dependencies: + onetime "^1.0.0" + set-immediate-shim "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +editorconfig@^0.13.2: + version "0.13.2" + resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.13.2.tgz#8e57926d9ee69ab6cb999f027c2171467acceb35" + dependencies: + bluebird "^3.0.5" + commander "^2.9.0" + lru-cache "^3.2.0" + sigmund "^1.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + +electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.14: + version "1.3.15" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.15.tgz#08397934891cbcfaebbd18b82a95b5a481138369" + +elliptic@^6.0.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + +encodeurl@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" + +end-of-stream@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.0.tgz#7a90d833efda6cfa6eac0f4949dbb0fad3a63206" + dependencies: + once "^1.4.0" + +end-of-stream@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.0.0.tgz#d4596e702734a93e40e9af864319eabd99ff2f0e" + dependencies: + once "~1.3.0" + +enhanced-resolve@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.3.0.tgz#950964ecc7f0332a42321b673b38dc8ff15535b3" + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.4.0" + object-assign "^4.0.1" + tapable "^0.2.5" + +errno@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d" + dependencies: + prr "~0.0.0" + +error-ex@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" + dependencies: + is-arrayish "^0.2.1" + +error-stack-parser@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.1.tgz#a3202b8fb03114aa9b40a0e3669e48b2b65a010a" + dependencies: + stackframe "^1.0.3" + +es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14: + version "0.10.24" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.24.tgz#a55877c9924bc0c8d9bd3c2cbe17495ac1709b14" + dependencies: + es6-iterator "2" + es6-symbol "~3.1" + +es6-iterator@^2.0.1, es6-iterator@~2.0.1, es6-iterator@2: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512" + dependencies: + d "1" + es5-ext "^0.10.14" + es6-symbol "^3.1" + +es6-map@^0.1.3: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-set "~0.1.5" + es6-symbol "~3.1.1" + event-emitter "~0.3.5" + +es6-set@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-symbol "3.1.1" + event-emitter "~0.3.5" + +es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1, es6-symbol@~3.1.1, es6-symbol@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" + dependencies: + d "1" + es5-ext "~0.10.14" + +es6-templates@^0.2.2: + version "0.2.3" + resolved "https://registry.yarnpkg.com/es6-templates/-/es6-templates-0.2.3.tgz#5cb9ac9fb1ded6eb1239342b81d792bbb4078ee4" + dependencies: + recast "~0.11.12" + through "~2.3.6" + +es6-weak-map@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f" + dependencies: + d "1" + es5-ext "^0.10.14" + es6-iterator "^2.0.1" + es6-symbol "^3.1.1" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +escope@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" + dependencies: + es6-map "^0.1.3" + es6-weak-map "^2.0.1" + esrecurse "^4.1.0" + estraverse "^4.1.1" + +esprima@^2.6.0: + version "2.7.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" + +esprima@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" + +esprima@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.0.0.tgz#53cf247acda77313e551c3aa2e73342d3fb4f7d9" + +esprima@~3.1.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + +esrecurse@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163" + dependencies: + estraverse "^4.1.0" + object-assign "^4.0.1" + +estraverse@^4.1.0, estraverse@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +etag@~1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051" + +event-emitter@~0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + dependencies: + d "1" + es5-ext "~0.10.14" + +eventemitter3@1.x.x: + version "1.2.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" + +events@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + +eventsource@0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-0.1.6.tgz#0acede849ed7dd1ccc32c811bb11b944d4f29232" + dependencies: + original ">=0.0.5" + +evp_bytestokey@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.0.tgz#497b66ad9fef65cd7c08a6180824ba1476b66e53" + dependencies: + create-hash "^1.1.1" + +exec-buffer@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/exec-buffer/-/exec-buffer-3.2.0.tgz#b1686dbd904c7cf982e652c1f5a79b1e5573082b" + dependencies: + execa "^0.7.0" + p-finally "^1.0.0" + pify "^3.0.0" + rimraf "^2.5.4" + tempfile "^2.0.0" + +exec-series@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/exec-series/-/exec-series-1.0.3.tgz#6d257a9beac482a872c7783bc8615839fc77143a" + dependencies: + async-each-series "^1.1.0" + object-assign "^4.1.0" + +execa@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.5.1.tgz#de3fb85cb8d6e91c85bcbceb164581785cb57b36" + dependencies: + cross-spawn "^4.0.0" + get-stream "^2.2.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +executable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/executable/-/executable-1.1.0.tgz#877980e9112f3391066da37265de7ad8434ab4d9" + dependencies: + meow "^3.1.0" + +expand-brackets@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" + dependencies: + is-posix-bracket "^0.1.0" + +expand-range@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + dependencies: + fill-range "^2.1.0" + +express@^4.13.3: + version "4.15.3" + resolved "https://registry.yarnpkg.com/express/-/express-4.15.3.tgz#bab65d0f03aa80c358408972fc700f916944b662" + dependencies: + accepts "~1.3.3" + array-flatten "1.1.1" + content-disposition "0.5.2" + content-type "~1.0.2" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.7" + depd "~1.1.0" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.0" + finalhandler "~1.0.3" + fresh "0.5.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.1" + path-to-regexp "0.1.7" + proxy-addr "~1.1.4" + qs "6.4.0" + range-parser "~1.2.0" + send "0.15.3" + serve-static "1.12.3" + setprototypeof "1.0.3" + statuses "~1.3.1" + type-is "~1.6.15" + utils-merge "1.0.0" + vary "~1.1.1" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + dependencies: + is-extendable "^0.1.0" + +extend@^3.0.0, extend@~3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" + +extglob@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" + dependencies: + is-extglob "^1.0.0" + +extract-text-webpack-plugin@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.0.tgz#90caa7907bc449f335005e3ac7532b41b00de612" + dependencies: + async "^2.4.1" + loader-utils "^1.1.0" + schema-utils "^0.3.0" + webpack-sources "^1.0.1" + +extsprintf@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" + +fancy-log@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.0.tgz#45be17d02bb9917d60ccffd4995c999e6c8c9948" + dependencies: + chalk "^1.1.1" + time-stamp "^1.0.0" + +fast-deep-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + +fastparse@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" + +faye-websocket@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" + dependencies: + websocket-driver ">=0.5.1" + +faye-websocket@~0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.1.tgz#f0efe18c4f56e4f40afc7e06c719fd5ee6188f38" + dependencies: + websocket-driver ">=0.5.1" + +faye-websocket@0.9.4: + version "0.9.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.9.4.tgz#885934c79effb0409549e0c0a3801ed17a40cdad" + dependencies: + websocket-driver ">=0.5.1" + +fd-slicer@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" + dependencies: + pend "~1.2.0" + +figures@^1.3.5: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + +file-loader@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-0.11.2.tgz#4ff1df28af38719a6098093b88c82c71d1794a34" + dependencies: + loader-utils "^1.0.2" + +file-type@^3.1.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" + +file-type@^4.1.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-4.4.0.tgz#1b600e5fca1fbdc6e80c0a70c71c8dba5f7906c5" + +filename-regex@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" + +filename-reserved-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-1.0.0.tgz#e61cf805f0de1c984567d0386dc5df50ee5af7e4" + +filenamify@^1.0.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/filenamify/-/filenamify-1.2.1.tgz#a9f2ffd11c503bed300015029272378f1f1365a5" + dependencies: + filename-reserved-regex "^1.0.0" + strip-outer "^1.0.0" + trim-repeated "^1.0.0" + +fill-range@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" + dependencies: + is-number "^2.1.0" + isobject "^2.0.0" + randomatic "^1.1.3" + repeat-element "^1.1.2" + repeat-string "^1.5.2" + +finalhandler@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.3.tgz#ef47e77950e999780e86022a560e3217e0d0cc89" + dependencies: + debug "2.6.7" + encodeurl "~1.0.1" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.1" + statuses "~1.3.1" + unpipe "~1.0.0" + +find-cache-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" + dependencies: + commondir "^1.0.1" + make-dir "^1.0.0" + pkg-dir "^2.0.0" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + +find-versions@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-1.2.1.tgz#cbde9f12e38575a0af1be1b9a2c5d5fd8f186b62" + dependencies: + array-uniq "^1.0.0" + get-stdin "^4.0.1" + meow "^3.5.0" + semver-regex "^1.0.0" + +first-chunk-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz#59bfb50cd905f60d7c394cd3d9acaab4e6ad934e" + +flatten@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" + +follow-redirects@^1.2.3: + version "1.2.4" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.2.4.tgz#355e8f4d16876b43f577b0d5ce2668b9723214ea" + dependencies: + debug "^2.4.5" + +for-in@^0.1.3: + version "0.1.8" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" + +for-in@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + +for-own@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" + dependencies: + for-in "^1.0.1" + +for-own@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" + dependencies: + for-in "^1.0.1" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~2.1.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +forwarded@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" + +fresh@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" + +friendly-errors-webpack-plugin@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.6.1.tgz#e32781c4722f546a06a9b5d7a7cfa28520375d70" + dependencies: + chalk "^1.1.3" + error-stack-parser "^2.0.0" + string-length "^1.0.1" + +fs-extra@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^3.0.0" + universalify "^0.1.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +fsevents@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.2.tgz#3282b713fb3ad80ede0e9fcf4611b5aa6fc033f4" + dependencies: + nan "^2.3.0" + node-pre-gyp "^0.6.36" + +fstream-ignore@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" + dependencies: + fstream "^1.0.0" + inherits "2" + minimatch "^3.0.0" + +fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +function-bind@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771" + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +gaze@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.2.tgz#847224677adb8870d679257ed3388fdb61e40105" + dependencies: + globule "^1.0.0" + +get-caller-file@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" + +get-proxy@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/get-proxy/-/get-proxy-1.1.0.tgz#894854491bc591b0f147d7ae570f5c678b7256eb" + dependencies: + rc "^1.1.2" + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + +get-stream@^2.2.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de" + dependencies: + object-assign "^4.0.1" + pinkie-promise "^2.0.0" + +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + dependencies: + assert-plus "^1.0.0" + +gifsicle@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/gifsicle/-/gifsicle-3.0.4.tgz#f45cb5ed10165b665dc929e0e9328b6c821dfa3b" + dependencies: + bin-build "^2.0.0" + bin-wrapper "^3.0.0" + logalot "^2.0.0" + +glob-base@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" + dependencies: + glob-parent "^2.0.0" + is-glob "^2.0.0" + +glob-parent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" + dependencies: + is-glob "^2.0.0" + +glob-parent@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob-stream@^5.3.2: + version "5.3.5" + resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-5.3.5.tgz#a55665a9a8ccdc41915a87c701e32d4e016fad22" + dependencies: + extend "^3.0.0" + glob "^5.0.3" + glob-parent "^3.0.0" + micromatch "^2.3.7" + ordered-read-streams "^0.3.0" + through2 "^0.6.0" + to-absolute-glob "^0.1.1" + unique-stream "^2.0.2" + +glob@^5.0.3: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^9.0.0: + version "9.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" + +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +globs@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/globs/-/globs-0.1.3.tgz#670037125287cb6549aad96a44cfa684fd7c5502" + dependencies: + glob "^7.1.1" + +globule@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09" + dependencies: + glob "~7.1.1" + lodash "~4.17.4" + minimatch "~3.0.2" + +glogg@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/glogg/-/glogg-1.0.0.tgz#7fe0f199f57ac906cf512feead8f90ee4a284fc5" + dependencies: + sparkles "^1.0.0" + +got@^5.0.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/got/-/got-5.7.1.tgz#5f81635a61e4a6589f180569ea4e381680a51f35" + dependencies: + create-error-class "^3.0.1" + duplexer2 "^0.1.4" + is-redirect "^1.0.0" + is-retry-allowed "^1.0.0" + is-stream "^1.0.0" + lowercase-keys "^1.0.0" + node-status-codes "^1.0.0" + object-assign "^4.0.1" + parse-json "^2.1.0" + pinkie-promise "^2.0.0" + read-all-stream "^3.0.0" + readable-stream "^2.0.5" + timed-out "^3.0.0" + unzip-response "^1.0.2" + url-parse-lax "^1.0.0" + +graceful-fs@^4.0.0, graceful-fs@^4.1.2, graceful-fs@^4.1.6: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +"graceful-readlink@>= 1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" + +growly@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + +gulp-decompress@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gulp-decompress/-/gulp-decompress-1.2.0.tgz#8eeb65a5e015f8ed8532cafe28454960626f0dc7" + dependencies: + archive-type "^3.0.0" + decompress "^3.0.0" + gulp-util "^3.0.1" + readable-stream "^2.0.2" + +gulp-rename@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-1.2.2.tgz#3ad4428763f05e2764dec1c67d868db275687817" + +gulp-sourcemaps@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz#b86ff349d801ceb56e1d9e7dc7bbcb4b7dee600c" + dependencies: + convert-source-map "^1.1.1" + graceful-fs "^4.1.2" + strip-bom "^2.0.0" + through2 "^2.0.0" + vinyl "^1.0.0" + +gulp-util@^3.0.1: + version "3.0.8" + resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" + dependencies: + array-differ "^1.0.0" + array-uniq "^1.0.2" + beeper "^1.0.0" + chalk "^1.0.0" + dateformat "^2.0.0" + fancy-log "^1.1.0" + gulplog "^1.0.0" + has-gulplog "^0.1.0" + lodash._reescape "^3.0.0" + lodash._reevaluate "^3.0.0" + lodash._reinterpolate "^3.0.0" + lodash.template "^3.0.0" + minimist "^1.1.0" + multipipe "^0.1.2" + object-assign "^3.0.0" + replace-ext "0.0.1" + through2 "^2.0.0" + vinyl "^0.5.0" + +gulplog@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/gulplog/-/gulplog-1.0.0.tgz#e28c4d45d05ecbbed818363ce8f9c5926229ffe5" + dependencies: + glogg "^1.0.0" + +handle-thing@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4" + +har-schema@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" + +har-validator@~4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" + dependencies: + ajv "^4.9.1" + har-schema "^1.0.5" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + +has-gulplog@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/has-gulplog/-/has-gulplog-0.1.0.tgz#6414c82913697da51590397dafb12f22967811ce" + dependencies: + sparkles "^1.0.0" + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + +has@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" + dependencies: + function-bind "^1.0.2" + +hash-base@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" + dependencies: + inherits "^2.0.1" + +hash-sum@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846" + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.0" + +hawk@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + +he@^1.1.0, he@1.1.x: + version "1.1.1" + resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + +hmac-drbg@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + +home-or-tmp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.1" + +hosted-git-info@^2.1.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-comment-regex@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" + +html-entities@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" + +html-loader@^0.4.5: + version "0.4.5" + resolved "https://registry.yarnpkg.com/html-loader/-/html-loader-0.4.5.tgz#5fbcd87cd63a5c49a7fce2fe56f425e05729c68c" + dependencies: + es6-templates "^0.2.2" + fastparse "^1.1.1" + html-minifier "^3.0.1" + loader-utils "^1.0.2" + object-assign "^4.1.0" + +html-minifier@^3.0.1: + version "3.5.2" + resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.2.tgz#d73bc3ff448942408818ce609bf3fb0ea7ef4eb7" + dependencies: + camel-case "3.0.x" + clean-css "4.1.x" + commander "2.9.x" + he "1.1.x" + ncname "1.0.x" + param-case "2.1.x" + relateurl "0.2.x" + uglify-js "3.0.x" + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + +http-errors@~1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257" + dependencies: + depd "1.1.0" + inherits "2.0.3" + setprototypeof "1.0.3" + statuses ">= 1.3.1 < 2" + +http-proxy-middleware@~0.17.4: + version "0.17.4" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833" + dependencies: + http-proxy "^1.16.2" + is-glob "^3.1.0" + lodash "^4.17.2" + micromatch "^2.3.11" + +http-proxy@^1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.16.2.tgz#06dff292952bf64dbe8471fa9df73066d4f37742" + dependencies: + eventemitter3 "1.x.x" + requires-port "1.x.x" + +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-browserify@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82" + +icss-replace-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" + +icss-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-2.1.0.tgz#83f0a0ec378bf3246178b6c2ad9136f135b1c962" + dependencies: + postcss "^6.0.1" + +ieee754@^1.1.4: + version "1.1.8" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" + +imagemin-gifsicle@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/imagemin-gifsicle/-/imagemin-gifsicle-5.2.0.tgz#3781524c457612ef04916af34241a2b42bfcb40a" + dependencies: + exec-buffer "^3.0.0" + gifsicle "^3.0.0" + is-gif "^1.0.0" + +imagemin-mozjpeg@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/imagemin-mozjpeg/-/imagemin-mozjpeg-6.0.0.tgz#71a32a457aa1b26117a68eeef2d9b190c2e5091e" + dependencies: + exec-buffer "^3.0.0" + is-jpg "^1.0.0" + mozjpeg "^4.0.0" + +imagemin-optipng@^5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/imagemin-optipng/-/imagemin-optipng-5.2.1.tgz#d22da412c09f5ff00a4339960b98a88b1dbe8695" + dependencies: + exec-buffer "^3.0.0" + is-png "^1.0.0" + optipng-bin "^3.0.0" + +imagemin-pngquant@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/imagemin-pngquant/-/imagemin-pngquant-5.0.1.tgz#d8a329da553afa226b11ce62debe0b7e37b439e6" + dependencies: + exec-buffer "^3.0.0" + is-png "^1.0.0" + pngquant-bin "^3.0.0" + +imagemin-svgo@^5.2.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/imagemin-svgo/-/imagemin-svgo-5.2.2.tgz#501699f5789730a57922b8736ea15c53f7b55838" + dependencies: + is-svg "^2.0.0" + svgo "^0.7.0" + +imagemin@^5.2.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/imagemin/-/imagemin-5.3.1.tgz#f19c2eee1e71ba6c6558c515f9fc96680189a6d4" + dependencies: + file-type "^4.1.0" + globby "^6.1.0" + make-dir "^1.0.0" + p-pipe "^1.1.0" + pify "^2.3.0" + replace-ext "^1.0.0" + +img-loader@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/img-loader/-/img-loader-2.0.0.tgz#583740b3e2a38aeba5435c7dd530be9ce7454fd9" + dependencies: + imagemin "^5.2.0" + imagemin-gifsicle "^5.1.0" + imagemin-mozjpeg "^6.0.0" + imagemin-optipng "^5.2.0" + imagemin-pngquant "^5.0.0" + imagemin-svgo "^5.2.0" + loader-utils "^1.0.4" + +in-publish@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + dependencies: + repeating "^2.0.0" + +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3, inherits@2, inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + +ini@^1.3.4, ini@~1.3.0: + version "1.3.4" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" + +internal-ip@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c" + dependencies: + meow "^3.3.0" + +interpret@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90" + +invariant@^2.2.0, invariant@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" + dependencies: + loose-envify "^1.0.0" + +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + +ip-regex@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-1.0.3.tgz#dc589076f659f419c222039a33316f1c7387effd" + +ip@^1.1.0: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + +ipaddr.js@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.3.0.tgz#1e03a52fdad83a8bbb2b25cbf4998b4cffcd3dec" + +is-absolute-url@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" + +is-absolute@^0.1.5: + version "0.1.7" + resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-0.1.7.tgz#847491119fccb5fb436217cc737f7faad50f603f" + dependencies: + is-relative "^0.1.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.0.2, is-buffer@^1.1.5, is-buffer@~1.1.1: + version "1.1.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc" + +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + +is-bzip2@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-bzip2/-/is-bzip2-1.0.0.tgz#5ee58eaa5a2e9c80e21407bedf23ae5ac091b3fc" + +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + +is-dotfile@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" + +is-equal-shallow@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" + dependencies: + is-primitive "^2.0.0" + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-extglob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" + +is-extglob@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-gif@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-gif/-/is-gif-1.0.0.tgz#a6d2ae98893007bffa97a1d8c01d63205832097e" + +is-glob@^2.0.0, is-glob@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" + dependencies: + is-extglob "^1.0.0" + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + dependencies: + is-extglob "^2.1.0" + +is-gzip@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-gzip/-/is-gzip-1.0.0.tgz#6ca8b07b99c77998025900e555ced8ed80879a83" + +is-jpg@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-jpg/-/is-jpg-1.0.0.tgz#2959c17e73430db38264da75b90dd54f2d86da1c" + +is-natural-number@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-2.1.1.tgz#7d4c5728377ef386c3e194a9911bf57c6dc335e7" + +is-number@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + dependencies: + kind-of "^3.0.2" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + dependencies: + kind-of "^3.0.2" + +is-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + +is-path-in-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f" + dependencies: + path-is-inside "^1.0.1" + +is-plain-obj@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + +is-plain-object@^2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + dependencies: + isobject "^3.0.1" + +is-png@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-png/-/is-png-1.1.0.tgz#d574b12bf275c0350455570b0e5b57ab062077ce" + +is-posix-bracket@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" + +is-primitive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" + +is-redirect@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" + +is-relative@^0.1.0: + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-0.1.3.tgz#905fee8ae86f45b3ec614bc3c15c869df0876e82" + +is-retry-allowed@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" + +is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-svg@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-2.1.0.tgz#cf61090da0d9efbcab8722deba6f032208dbb0e9" + dependencies: + html-comment-regex "^1.1.0" + +is-tar@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-tar/-/is-tar-1.0.0.tgz#2f6b2e1792c1f5bb36519acaa9d65c0d26fe853d" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +is-url@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.2.tgz#498905a593bf47cc2d9e7f738372bbf7696c7f26" + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + +is-valid-glob@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-0.3.0.tgz#d4b55c69f51886f9b65c70d6c2622d37e29f48fe" + +is-windows@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.1.tgz#310db70f742d259a16a369202b51af84233310d9" + +is-zip@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-zip/-/is-zip-1.0.0.tgz#47b0a8ff4d38a76431ccfd99a8e15a4c86ba2325" + +isarray@^1.0.0, isarray@~1.0.0, isarray@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + dependencies: + isarray "1.0.0" + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +jquery@^3.1.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.2.1.tgz#5c4d9de652af6cd0a770154a631bba12b015c787" + +js-base64@^2.1.8, js-base64@^2.1.9: + version "2.1.9" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.1.9.tgz#f0e80ae039a4bd654b5f281fc93f04a914a7fcce" + +js-beautify@^1.6.3: + version "1.6.14" + resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.6.14.tgz#d3b8f7322d02b9277d58bd238264c327e58044cd" + dependencies: + config-chain "~1.1.5" + editorconfig "^0.13.2" + mkdirp "~0.5.0" + nopt "~3.0.1" + +js-tokens@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +js-yaml@^3.4.3: + version "3.9.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.0.tgz#4ffbbf25c2ac963b8299dc74da7e3740de1c18ce" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@~3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80" + dependencies: + argparse "^1.0.7" + esprima "^2.6.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +jsesc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + +json-loader@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.4.tgz#8baa1365a632f58a3c46d20175fc6002c96e37de" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +json3@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" + +json5@^0.5.0, json5@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + +jsonfile@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-3.0.1.tgz#a5ecc6f65f53f662c4415c7675a0331d0992ec66" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + +jsprim@^1.2.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.0.tgz#a3b87e40298d8c380552d8cc7628a0bb95a22918" + dependencies: + assert-plus "1.0.0" + extsprintf "1.0.2" + json-schema "0.2.3" + verror "1.3.6" + +kind-of@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5" + dependencies: + is-buffer "^1.0.2" + +kind-of@^3.0.2, kind-of@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + dependencies: + is-buffer "^1.1.5" + +laravel-echo@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/laravel-echo/-/laravel-echo-1.3.0.tgz#790c9a0d01b69c835d2303dc1d003e8818438e74" + +laravel-mix@^1.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/laravel-mix/-/laravel-mix-1.2.2.tgz#fad1a3181826004ed9a35a02fa0531494d03a811" + dependencies: + autoprefixer "^7.1.1" + babel-core "^6.24.1" + babel-loader "^7.1.1" + babel-preset-env "^1.5.1" + chokidar "^1.7.0" + clean-css "^4.1.3" + concatenate "0.0.2" + css-loader "^0.28.3" + dotenv "^4.0.0" + dotenv-expand "^4.0.1" + extract-text-webpack-plugin "^3.0.0" + file-loader "^0.11.1" + friendly-errors-webpack-plugin "^1.6.1" + fs-extra "^3.0.1" + glob "^7.1.2" + html-loader "^0.4.5" + img-loader "^2.0.0" + lodash "^4.17.4" + md5 "^2.2.1" + node-sass "^4.5.3" + postcss-loader "^2.0.5" + resolve-url-loader "^2.0.2" + sass-loader "^6.0.5" + style-loader "^0.18.1" + uglify-js "^2.8.28" + uglifyjs-webpack-plugin "^0.4.6" + vue-loader "^12.1.1" + vue-template-compiler "^2.3.3" + webpack "^3.1.0" + webpack-chunk-hash "^0.4.0" + webpack-dev-server "^2.5.1" + webpack-merge "^4.1.0" + webpack-notifier "^1.5.0" + yargs "^8.0.1" + +lazy-cache@^0.2.3: + version "0.2.7" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65" + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + +lazy-req@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/lazy-req/-/lazy-req-1.1.0.tgz#bdaebead30f8d824039ce0ce149d4daa07ba1fac" + +lazystream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" + dependencies: + readable-stream "^2.0.5" + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + dependencies: + invert-kv "^1.0.0" + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +loader-runner@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" + +loader-utils@^1.0.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.0.4, loader-utils@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +lodash._arraycopy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1" + +lodash._arrayeach@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz#bab156b2a90d3f1bbd5c653403349e5e5933ef9e" + +lodash._baseassign@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" + dependencies: + lodash._basecopy "^3.0.0" + lodash.keys "^3.0.0" + +lodash._baseclone@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lodash._baseclone/-/lodash._baseclone-3.3.0.tgz#303519bf6393fe7e42f34d8b630ef7794e3542b7" + dependencies: + lodash._arraycopy "^3.0.0" + lodash._arrayeach "^3.0.0" + lodash._baseassign "^3.0.0" + lodash._basefor "^3.0.0" + lodash.isarray "^3.0.0" + lodash.keys "^3.0.0" + +lodash._basecopy@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" + +lodash._basefor@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash._basefor/-/lodash._basefor-3.0.3.tgz#7550b4e9218ef09fad24343b612021c79b4c20c2" + +lodash._basetostring@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz#d1861d877f824a52f669832dcaf3ee15566a07d5" + +lodash._basevalues@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz#5b775762802bde3d3297503e26300820fdf661b7" + +lodash._bindcallback@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" + +lodash._createassigner@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz#838a5bae2fdaca63ac22dee8e19fa4e6d6970b11" + dependencies: + lodash._bindcallback "^3.0.0" + lodash._isiterateecall "^3.0.0" + lodash.restparam "^3.0.0" + +lodash._getnative@^3.0.0: + version "3.9.1" + resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" + +lodash._isiterateecall@^3.0.0: + version "3.0.9" + resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" + +lodash._reescape@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reescape/-/lodash._reescape-3.0.0.tgz#2b1d6f5dfe07c8a355753e5f27fac7f1cde1616a" + +lodash._reevaluate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz#58bc74c40664953ae0b124d806996daca431e2ed" + +lodash._reinterpolate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + +lodash._root@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" + +lodash.assign@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-3.2.0.tgz#3ce9f0234b4b2223e296b8fa0ac1fee8ebca64fa" + dependencies: + lodash._baseassign "^3.0.0" + lodash._createassigner "^3.0.0" + lodash.keys "^3.0.0" + +lodash.assign@^4.0.1, lodash.assign@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + +lodash.clonedeep@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-3.0.2.tgz#a0a1e40d82a5ea89ff5b147b8444ed63d92827db" + dependencies: + lodash._baseclone "^3.0.0" + lodash._bindcallback "^3.0.0" + +lodash.clonedeep@^4.3.2: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + +lodash.defaults@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-3.1.2.tgz#c7308b18dbf8bc9372d701a73493c61192bd2e2c" + dependencies: + lodash.assign "^3.0.0" + lodash.restparam "^3.0.0" + +lodash.defaults@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + +lodash.escape@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-3.2.0.tgz#995ee0dc18c1b48cc92effae71a10aab5b487698" + dependencies: + lodash._root "^3.0.0" + +lodash.isarguments@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + +lodash.isarray@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" + +lodash.isequal@^4.0.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + +lodash.keys@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" + dependencies: + lodash._getnative "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + +lodash.mergewith@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55" + +lodash.restparam@^3.0.0: + version "3.6.1" + resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" + +lodash.tail@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" + +lodash.template@^3.0.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-3.6.2.tgz#f8cdecc6169a255be9098ae8b0c53d378931d14f" + dependencies: + lodash._basecopy "^3.0.0" + lodash._basetostring "^3.0.0" + lodash._basevalues "^3.0.0" + lodash._isiterateecall "^3.0.0" + lodash._reinterpolate "^3.0.0" + lodash.escape "^3.0.0" + lodash.keys "^3.0.0" + lodash.restparam "^3.0.0" + lodash.templatesettings "^3.0.0" + +lodash.templatesettings@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5" + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.escape "^3.0.0" + +lodash.toarray@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + +lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@~4.17.4: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" + +logalot@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/logalot/-/logalot-2.1.0.tgz#5f8e8c90d304edf12530951a5554abb8c5e3f552" + dependencies: + figures "^1.3.5" + squeak "^1.0.0" + +longest@^1.0.0, longest@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + +loose-envify@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" + dependencies: + js-tokens "^3.0.0" + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +lower-case@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" + +lowercase-keys@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" + +lpad-align@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/lpad-align/-/lpad-align-1.1.2.tgz#21f600ac1c3095c3c6e497ee67271ee08481fe9e" + dependencies: + get-stdin "^4.0.1" + indent-string "^2.1.0" + longest "^1.0.0" + meow "^3.3.0" + +lru-cache@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-3.2.0.tgz#71789b3b7f5399bec8565dda38aa30d2a097efee" + dependencies: + pseudomap "^1.0.1" + +lru-cache@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +macaddress@^0.2.8: + version "0.2.8" + resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" + +make-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978" + dependencies: + pify "^2.3.0" + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + +marked-terminal@^1.6.2: + version "1.7.0" + resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-1.7.0.tgz#c8c460881c772c7604b64367007ee5f77f125904" + dependencies: + cardinal "^1.0.0" + chalk "^1.1.3" + cli-table "^0.3.1" + lodash.assign "^4.2.0" + node-emoji "^1.4.1" + +marked@^0.3.6: + version "0.3.6" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.6.tgz#b2c6c618fccece4ef86c4fc6cb8a7cbf5aeda8d7" + +math-expression-evaluator@^1.2.14: + version "1.2.17" + resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" + +md5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" + dependencies: + charenc "~0.0.1" + crypt "~0.0.1" + is-buffer "~1.1.1" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + +mem@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" + dependencies: + mimic-fn "^1.0.0" + +memory-fs@^0.4.0, memory-fs@~0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +meow@^3.1.0, meow@^3.3.0, meow@^3.5.0, meow@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + +merge-stream@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1" + dependencies: + readable-stream "^2.0.1" + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + +micromatch@^2.1.5, micromatch@^2.3.11, micromatch@^2.3.7: + version "2.3.11" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" + dependencies: + arr-diff "^2.0.0" + array-unique "^0.2.1" + braces "^1.8.2" + expand-brackets "^0.1.4" + extglob "^0.3.1" + filename-regex "^2.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.1" + kind-of "^3.0.2" + normalize-path "^2.0.1" + object.omit "^2.0.0" + parse-glob "^3.0.4" + regex-cache "^0.4.2" + +miller-rabin@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.0.tgz#4a62fb1d42933c05583982f4c716f6fb9e6c6d3d" + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +"mime-db@>= 1.27.0 < 2": + version "1.29.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878" + +mime-db@~1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" + +mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7: + version "2.1.15" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" + dependencies: + mime-db "~1.27.0" + +mime@^1.3.4: + version "1.3.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.6.tgz#591d84d3653a6b0b4a3b9df8de5aa8108e72e5e0" + +mime@1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" + +mimic-fn@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" + +minimalistic-assert@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" + +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + +minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2, "minimatch@2 || 3": + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +mixin-object@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" + dependencies: + for-in "^0.1.3" + is-extendable "^0.1.1" + +mkdirp@^0.5.0, mkdirp@^0.5.1, "mkdirp@>=0.5 0", mkdirp@~0.5.0, mkdirp@~0.5.1, mkdirp@0.5.x: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +mozjpeg@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/mozjpeg/-/mozjpeg-4.1.1.tgz#859030b24f689a53db9b40f0160d89195b88fd50" + dependencies: + bin-build "^2.0.0" + bin-wrapper "^3.0.0" + logalot "^2.0.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +multicast-dns-service-types@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" + +multicast-dns@^6.0.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.1.1.tgz#6e7de86a570872ab17058adea7160bbeca814dde" + dependencies: + dns-packet "^1.0.1" + thunky "^0.1.0" + +multipipe@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/multipipe/-/multipipe-0.1.2.tgz#2a8f2ddf70eed564dff2d57f1e1a137d9f05078b" + dependencies: + duplexer2 "0.0.2" + +nan@^2.3.0, nan@^2.3.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45" + +ncname@1.0.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ncname/-/ncname-1.0.0.tgz#5b57ad18b1ca092864ef62b0b1ed8194f383b71c" + dependencies: + xml-char-classes "^1.0.0" + +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + +no-case@^2.2.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.1.tgz#7aeba1c73a52184265554b7dc03baf720df80081" + dependencies: + lower-case "^1.1.1" + +node-emoji@^1.4.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.7.0.tgz#a400490aac409b616d13941532200f128af037f9" + dependencies: + lodash.toarray "^4.4.0" + string.prototype.codepointat "^0.2.0" + +node-forge@0.6.33: + version "0.6.33" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.33.tgz#463811879f573d45155ad6a9f43dc296e8e85ebc" + +node-gyp@^3.3.1: + version "3.6.2" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60" + dependencies: + fstream "^1.0.0" + glob "^7.0.3" + graceful-fs "^4.1.2" + minimatch "^3.0.2" + mkdirp "^0.5.0" + nopt "2 || 3" + npmlog "0 || 1 || 2 || 3 || 4" + osenv "0" + request "2" + rimraf "2" + semver "~5.3.0" + tar "^2.0.0" + which "1" + +node-libs-browser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.0.0.tgz#a3a59ec97024985b46e958379646f96c4b616646" + dependencies: + assert "^1.1.1" + browserify-zlib "^0.1.4" + buffer "^4.3.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "^3.11.0" + domain-browser "^1.1.1" + events "^1.0.0" + https-browserify "0.0.1" + os-browserify "^0.2.0" + path-browserify "0.0.0" + process "^0.11.0" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.0.5" + stream-browserify "^2.0.1" + stream-http "^2.3.1" + string_decoder "^0.10.25" + timers-browserify "^2.0.2" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.10.3" + vm-browserify "0.0.4" + +node-notifier@^4.1.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-4.6.1.tgz#056d14244f3dcc1ceadfe68af9cff0c5473a33f3" + dependencies: + cli-usage "^0.1.1" + growly "^1.2.0" + lodash.clonedeep "^3.0.0" + minimist "^1.1.1" + semver "^5.1.0" + shellwords "^0.1.0" + which "^1.0.5" + +node-pre-gyp@^0.6.36: + version "0.6.36" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786" + dependencies: + mkdirp "^0.5.1" + nopt "^4.0.1" + npmlog "^4.0.2" + rc "^1.1.7" + request "^2.81.0" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^2.2.1" + tar-pack "^3.4.0" + +node-sass@^4.5.3: + version "4.5.3" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.5.3.tgz#d09c9d1179641239d1b97ffc6231fdcec53e1568" + dependencies: + async-foreach "^0.1.3" + chalk "^1.1.1" + cross-spawn "^3.0.0" + gaze "^1.0.0" + get-stdin "^4.0.1" + glob "^7.0.3" + in-publish "^2.0.0" + lodash.assign "^4.2.0" + lodash.clonedeep "^4.3.2" + lodash.mergewith "^4.6.0" + meow "^3.7.0" + mkdirp "^0.5.1" + nan "^2.3.2" + node-gyp "^3.3.1" + npmlog "^4.0.0" + request "^2.79.0" + sass-graph "^2.1.1" + stdout-stream "^1.4.0" + +node-status-codes@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f" + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + dependencies: + abbrev "1" + osenv "^0.1.4" + +nopt@~3.0.1, "nopt@2 || 3": + version "3.0.6" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + dependencies: + abbrev "1" + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + +normalize-url@^1.4.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" + dependencies: + object-assign "^4.0.1" + prepend-http "^1.0.0" + query-string "^4.1.0" + sort-keys "^1.0.0" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + dependencies: + path-key "^2.0.0" + +npmlog@^4.0.0, npmlog@^4.0.2, "npmlog@0 || 1 || 2 || 3 || 4": + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +num2fraction@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +oauth-sign@~0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +object-assign@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-2.1.1.tgz#43c36e5d569ff8e4816c4efa8be02d26967c18aa" + +object-assign@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" + +object-assign@^4.0.0, object-assign@^4.0.1, object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +object-path@^0.9.2: + version "0.9.2" + resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.9.2.tgz#0fd9a74fc5fad1ae3968b586bda5c632bd6c05a5" + +object.omit@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" + dependencies: + for-own "^0.1.4" + is-extendable "^0.1.1" + +obuf@^1.0.0, obuf@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.1.tgz#104124b6c602c6796881a042541d36db43a5264e" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" + +once@^1.3.0, once@^1.3.3, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +once@~1.3.0: + version "1.3.3" + resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20" + dependencies: + wrappy "1" + +onetime@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + +opn@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/opn/-/opn-4.0.2.tgz#7abc22e644dff63b0a96d5ab7f2790c0f01abc95" + dependencies: + object-assign "^4.0.1" + pinkie-promise "^2.0.0" + +optipng-bin@^3.0.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/optipng-bin/-/optipng-bin-3.1.4.tgz#95d34f2c488704f6fd70606bfea0c659f1d95d84" + dependencies: + bin-build "^2.0.0" + bin-wrapper "^3.0.0" + logalot "^2.0.0" + +ordered-read-streams@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz#7137e69b3298bb342247a1bbee3881c80e2fd78b" + dependencies: + is-stream "^1.0.1" + readable-stream "^2.0.1" + +original@>=0.0.5: + version "1.0.0" + resolved "https://registry.yarnpkg.com/original/-/original-1.0.0.tgz#9147f93fa1696d04be61e01bd50baeaca656bd3b" + dependencies: + url-parse "1.0.x" + +os-browserify@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f" + +os-filter-obj@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/os-filter-obj/-/os-filter-obj-1.0.3.tgz#5915330d90eced557d2d938a31c6dd214d9c63ad" + +os-homedir@^1.0.0, os-homedir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-locale@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + dependencies: + lcid "^1.0.0" + +os-locale@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.0.0.tgz#15918ded510522b81ee7ae5a309d54f639fc39a4" + dependencies: + execa "^0.5.0" + lcid "^1.0.0" + mem "^1.1.0" + +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +osenv@^0.1.4, osenv@0: + version "0.1.4" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + +p-limit@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + dependencies: + p-limit "^1.1.0" + +p-map@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.1.1.tgz#05f5e4ae97a068371bc2a5cc86bfbdbc19c4ae7a" + +p-pipe@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-pipe/-/p-pipe-1.1.0.tgz#2e9dc7cc57ce67d2ce2db348ca03f28731854075" + +pako@~0.2.0: + version "0.2.9" + resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" + +param-case@2.1.x: + version "2.1.1" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" + dependencies: + no-case "^2.2.0" + +parse-asn1@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712" + dependencies: + asn1.js "^4.0.0" + browserify-aes "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + +parse-glob@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" + dependencies: + glob-base "^0.3.0" + is-dotfile "^1.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.0" + +parse-json@^2.1.0, parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +parseurl@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" + +path-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-is-inside@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + +path-key@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + +path-parse@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + dependencies: + pify "^2.0.0" + +pbkdf2@^3.0.3: + version "3.0.12" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.12.tgz#be36785c5067ea48d806ff923288c5f750b6b8a2" + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + +performance-now@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" + +pify@^2.0.0, pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + dependencies: + find-up "^2.1.0" + +pngquant-bin@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/pngquant-bin/-/pngquant-bin-3.1.1.tgz#d124d98a75a9487f40c1640b4dbfcbb2bd5a1fd1" + dependencies: + bin-build "^2.0.0" + bin-wrapper "^3.0.0" + logalot "^2.0.0" + +portfinder@^1.0.9: + version "1.0.13" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9" + dependencies: + async "^1.5.2" + debug "^2.2.0" + mkdirp "0.5.x" + +postcss-calc@^5.2.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e" + dependencies: + postcss "^5.0.2" + postcss-message-helpers "^2.0.0" + reduce-css-calc "^1.2.6" + +postcss-colormin@^2.1.8: + version "2.2.2" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-2.2.2.tgz#6631417d5f0e909a3d7ec26b24c8a8d1e4f96e4b" + dependencies: + colormin "^1.0.5" + postcss "^5.0.13" + postcss-value-parser "^3.2.3" + +postcss-convert-values@^2.3.4: + version "2.6.1" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz#bbd8593c5c1fd2e3d1c322bb925dcae8dae4d62d" + dependencies: + postcss "^5.0.11" + postcss-value-parser "^3.1.2" + +postcss-discard-comments@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz#befe89fafd5b3dace5ccce51b76b81514be00e3d" + dependencies: + postcss "^5.0.14" + +postcss-discard-duplicates@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz#b9abf27b88ac188158a5eb12abcae20263b91932" + dependencies: + postcss "^5.0.4" + +postcss-discard-empty@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz#d2b4bd9d5ced5ebd8dcade7640c7d7cd7f4f92b5" + dependencies: + postcss "^5.0.14" + +postcss-discard-overridden@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz#8b1eaf554f686fb288cd874c55667b0aa3668d58" + dependencies: + postcss "^5.0.16" + +postcss-discard-unused@^2.2.1: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz#bce30b2cc591ffc634322b5fb3464b6d934f4433" + dependencies: + postcss "^5.0.14" + uniqs "^2.0.0" + +postcss-filter-plugins@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz#6d85862534d735ac420e4a85806e1f5d4286d84c" + dependencies: + postcss "^5.0.4" + uniqid "^4.0.0" + +postcss-load-config@^1.1.0, postcss-load-config@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a" + dependencies: + cosmiconfig "^2.1.0" + object-assign "^4.1.0" + postcss-load-options "^1.2.0" + postcss-load-plugins "^2.3.0" + +postcss-load-options@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-load-options/-/postcss-load-options-1.2.0.tgz#b098b1559ddac2df04bc0bb375f99a5cfe2b6d8c" + dependencies: + cosmiconfig "^2.1.0" + object-assign "^4.1.0" + +postcss-load-plugins@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz#745768116599aca2f009fad426b00175049d8d92" + dependencies: + cosmiconfig "^2.1.1" + object-assign "^4.1.0" + +postcss-loader@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.0.6.tgz#8c7e0055a3df1889abc6bad52dd45b2f41bbc6fc" + dependencies: + loader-utils "^1.1.0" + postcss "^6.0.2" + postcss-load-config "^1.2.0" + schema-utils "^0.3.0" + +postcss-merge-idents@^2.1.5: + version "2.1.7" + resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz#4c5530313c08e1d5b3bbf3d2bbc747e278eea270" + dependencies: + has "^1.0.1" + postcss "^5.0.10" + postcss-value-parser "^3.1.1" + +postcss-merge-longhand@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz#23d90cd127b0a77994915332739034a1a4f3d658" + dependencies: + postcss "^5.0.4" + +postcss-merge-rules@^2.0.3: + version "2.1.2" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz#d1df5dfaa7b1acc3be553f0e9e10e87c61b5f721" + dependencies: + browserslist "^1.5.2" + caniuse-api "^1.5.2" + postcss "^5.0.4" + postcss-selector-parser "^2.2.2" + vendors "^1.0.0" + +postcss-message-helpers@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e" + +postcss-minify-font-values@^1.0.2: + version "1.0.5" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz#4b58edb56641eba7c8474ab3526cafd7bbdecb69" + dependencies: + object-assign "^4.0.1" + postcss "^5.0.4" + postcss-value-parser "^3.0.2" + +postcss-minify-gradients@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz#5dbda11373703f83cfb4a3ea3881d8d75ff5e6e1" + dependencies: + postcss "^5.0.12" + postcss-value-parser "^3.3.0" + +postcss-minify-params@^1.0.4: + version "1.2.2" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz#ad2ce071373b943b3d930a3fa59a358c28d6f1f3" + dependencies: + alphanum-sort "^1.0.1" + postcss "^5.0.2" + postcss-value-parser "^3.0.2" + uniqs "^2.0.0" + +postcss-minify-selectors@^2.0.4: + version "2.1.1" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz#b2c6a98c0072cf91b932d1a496508114311735bf" + dependencies: + alphanum-sort "^1.0.2" + has "^1.0.1" + postcss "^5.0.14" + postcss-selector-parser "^2.0.0" + +postcss-modules-extract-imports@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.0.tgz#66140ecece38ef06bf0d3e355d69bf59d141ea85" + dependencies: + postcss "^6.0.1" + +postcss-modules-local-by-default@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069" + dependencies: + css-selector-tokenizer "^0.7.0" + postcss "^6.0.1" + +postcss-modules-scope@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90" + dependencies: + css-selector-tokenizer "^0.7.0" + postcss "^6.0.1" + +postcss-modules-values@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20" + dependencies: + icss-replace-symbols "^1.1.0" + postcss "^6.0.1" + +postcss-normalize-charset@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz#ef9ee71212d7fe759c78ed162f61ed62b5cb93f1" + dependencies: + postcss "^5.0.5" + +postcss-normalize-url@^3.0.7: + version "3.0.8" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz#108f74b3f2fcdaf891a2ffa3ea4592279fc78222" + dependencies: + is-absolute-url "^2.0.0" + normalize-url "^1.4.0" + postcss "^5.0.14" + postcss-value-parser "^3.2.3" + +postcss-ordered-values@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz#eec6c2a67b6c412a8db2042e77fe8da43f95c11d" + dependencies: + postcss "^5.0.4" + postcss-value-parser "^3.0.1" + +postcss-reduce-idents@^2.2.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz#c2c6d20cc958284f6abfbe63f7609bf409059ad3" + dependencies: + postcss "^5.0.4" + postcss-value-parser "^3.0.2" + +postcss-reduce-initial@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz#68f80695f045d08263a879ad240df8dd64f644ea" + dependencies: + postcss "^5.0.4" + +postcss-reduce-transforms@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz#ff76f4d8212437b31c298a42d2e1444025771ae1" + dependencies: + has "^1.0.1" + postcss "^5.0.8" + postcss-value-parser "^3.0.1" + +postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90" + dependencies: + flatten "^1.0.2" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-svgo@^2.1.1: + version "2.1.6" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d" + dependencies: + is-svg "^2.0.0" + postcss "^5.0.14" + postcss-value-parser "^3.2.3" + svgo "^0.7.0" + +postcss-unique-selectors@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz#981d57d29ddcb33e7b1dfe1fd43b8649f933ca1d" + dependencies: + alphanum-sort "^1.0.1" + postcss "^5.0.4" + uniqs "^2.0.0" + +postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^3.1.1, postcss-value-parser@^3.1.2, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15" + +postcss-zindex@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-2.2.0.tgz#d2109ddc055b91af67fc4cb3b025946639d2af22" + dependencies: + has "^1.0.1" + postcss "^5.0.4" + uniqs "^2.0.0" + +postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.21, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16: + version "5.2.17" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.17.tgz#cf4f597b864d65c8a492b2eabe9d706c879c388b" + dependencies: + chalk "^1.1.3" + js-base64 "^2.1.9" + source-map "^0.5.6" + supports-color "^3.2.3" + +postcss@^6.0.1, postcss@^6.0.2, postcss@^6.0.6: + version "6.0.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.6.tgz#bba4d58e884fc78c840d1539e10eddaabb8f73bd" + dependencies: + chalk "^2.0.1" + source-map "^0.5.6" + supports-color "^4.1.0" + +prepend-http@^1.0.0, prepend-http@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + +preserve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" + +private@^0.1.6, private@~0.1.5: + version "0.1.7" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1" + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + +process@^0.11.0: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + +proxy-addr@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.4.tgz#27e545f6960a44a627d9b44467e35c1b6b4ce2f3" + dependencies: + forwarded "~0.1.0" + ipaddr.js "1.3.0" + +prr@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" + +pseudomap@^1.0.1, pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +public-encrypt@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + +punycode@^1.2.4, punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + +pusher-js@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/pusher-js/-/pusher-js-4.1.0.tgz#5297973de935994ba65c2048ccf3b8c007247341" + dependencies: + faye-websocket "0.9.4" + xmlhttprequest "^1.8.0" + +q@^1.1.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1" + +qs@~6.4.0, qs@6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" + +query-string@^4.1.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" + dependencies: + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +querystring-es3@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + +querystringify@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-1.0.0.tgz#6286242112c5b712fa654e526652bf6a13ff05cb" + +querystringify@0.0.x: + version "0.0.4" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-0.0.4.tgz#0cf7f84f9463ff0ae51c4c4b142d95be37724d9c" + +randomatic@^1.1.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +randombytes@^2.0.0, randombytes@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.5.tgz#dc009a246b8d09a177b4b7a0ae77bc570f4b1b79" + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.0.3, range-parser@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + +rc@^1.1.2, rc@^1.1.7: + version "1.2.1" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95" + dependencies: + deep-extend "~0.4.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +read-all-stream@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa" + dependencies: + pinkie-promise "^2.0.0" + readable-stream "^2.0.0" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.6, readable-stream@^2.2.9: + version "2.3.3" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + +"readable-stream@>=1.0.33-1 <1.1.0-0": + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@~1.1.9: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readdirp@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" + dependencies: + graceful-fs "^4.1.2" + minimatch "^3.0.2" + readable-stream "^2.0.2" + set-immediate-shim "^1.0.1" + +recast@~0.11.12: + version "0.11.23" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3" + dependencies: + ast-types "0.9.6" + esprima "~3.1.0" + private "~0.1.5" + source-map "~0.5.0" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +redeyed@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-1.0.1.tgz#e96c193b40c0816b00aec842698e61185e55498a" + dependencies: + esprima "~3.0.0" + +reduce-css-calc@^1.2.6: + version "1.3.0" + resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" + dependencies: + balanced-match "^0.4.2" + math-expression-evaluator "^1.2.14" + reduce-function-call "^1.0.1" + +reduce-function-call@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.2.tgz#5a200bf92e0e37751752fe45b0ab330fd4b6be99" + dependencies: + balanced-match "^0.4.2" + +regenerate@^1.2.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260" + +regenerator-runtime@^0.10.0: + version "0.10.5" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" + +regenerator-transform@0.9.11: + version "0.9.11" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.9.11.tgz#3a7d067520cb7b7176769eb5ff868691befe1283" + dependencies: + babel-runtime "^6.18.0" + babel-types "^6.19.0" + private "^0.1.6" + +regex-cache@^0.4.2: + version "0.4.3" + resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145" + dependencies: + is-equal-shallow "^0.1.3" + is-primitive "^2.0.0" + +regex-parser@^2.2.1: + version "2.2.7" + resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.7.tgz#bd090e09181849acc45457e765f7be2a63f50ef1" + +regexpu-core@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b" + dependencies: + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +regexpu-core@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" + dependencies: + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +regjsgen@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" + +regjsparser@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" + dependencies: + jsesc "~0.5.0" + +relateurl@0.2.x: + version "0.2.7" + resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" + +remove-trailing-separator@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.0.2.tgz#69b062d978727ad14dc6b56ba4ab772fd8d70511" + +repeat-element@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" + +repeat-string@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + dependencies: + is-finite "^1.0.0" + +replace-ext@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" + +replace-ext@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924" + +request@^2.79.0, request@^2.81.0, request@2: + version "2.81.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~4.2.1" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + performance-now "^0.2.0" + qs "~6.4.0" + safe-buffer "^5.0.1" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "^0.6.0" + uuid "^3.0.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + +require-from-string@^1.1.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418" + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + +requires-port@1.0.x, requires-port@1.x.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + +resolve-url-loader@^2.0.2: + version "2.1.0" + resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-2.1.0.tgz#27c95cc16a4353923fdbdc2dbaf5eef22232c477" + dependencies: + adjust-sourcemap-loader "^1.1.0" + camelcase "^4.0.0" + convert-source-map "^1.1.1" + loader-utils "^1.0.0" + lodash.defaults "^4.0.0" + rework "^1.0.1" + rework-visit "^1.0.0" + source-map "^0.5.6" + urix "^0.1.0" + +resolve-url@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + +resolve@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5" + dependencies: + path-parse "^1.0.5" + +rework-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rework-visit/-/rework-visit-1.0.0.tgz#9945b2803f219e2f7aca00adb8bc9f640f842c9a" + +rework@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rework/-/rework-1.0.1.tgz#30806a841342b54510aa4110850cd48534144aa7" + dependencies: + convert-source-map "^0.3.3" + css "^2.0.0" + +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" + dependencies: + align-text "^0.1.1" + +rimraf@^2.2.6, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@2: + version "2.6.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" + dependencies: + glob "^7.0.5" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" + dependencies: + hash-base "^2.0.0" + inherits "^2.0.1" + +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1, safe-buffer@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +sass-graph@^2.1.1: + version "2.2.4" + resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" + dependencies: + glob "^7.0.0" + lodash "^4.0.0" + scss-tokenizer "^0.2.3" + yargs "^7.0.0" + +sass-loader@^6.0.5: + version "6.0.6" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-6.0.6.tgz#e9d5e6c1f155faa32a4b26d7a9b7107c225e40f9" + dependencies: + async "^2.1.5" + clone-deep "^0.3.0" + loader-utils "^1.0.1" + lodash.tail "^4.1.1" + pify "^3.0.0" + +sax@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + +schema-utils@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" + dependencies: + ajv "^5.0.0" + +scss-tokenizer@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" + dependencies: + js-base64 "^2.1.8" + source-map "^0.4.2" + +seek-bzip@^1.0.3: + version "1.0.5" + resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.5.tgz#cfe917cb3d274bcffac792758af53173eb1fabdc" + dependencies: + commander "~2.8.1" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + +selfsigned@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.9.1.tgz#cdda4492d70d486570f87c65546023558e1dfa5a" + dependencies: + node-forge "0.6.33" + +semver-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-1.0.0.tgz#92a4969065f9c70c694753d55248fc68f8f652c9" + +semver-truncate@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/semver-truncate/-/semver-truncate-1.1.2.tgz#57f41de69707a62709a7e0104ba2117109ea47e8" + dependencies: + semver "^5.3.0" + +semver@^4.0.3: + version "4.3.6" + resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" + +semver@^5.1.0, semver@^5.3.0, semver@~5.3.0, "semver@2 || 3 || 4 || 5": + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + +send@0.15.3: + version "0.15.3" + resolved "https://registry.yarnpkg.com/send/-/send-0.15.3.tgz#5013f9f99023df50d1bd9892c19e3defd1d53309" + dependencies: + debug "2.6.7" + depd "~1.1.0" + destroy "~1.0.4" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.0" + fresh "0.5.0" + http-errors "~1.6.1" + mime "1.3.4" + ms "2.0.0" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.3.1" + +serve-index@^1.7.2: + version "1.9.0" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.0.tgz#d2b280fc560d616ee81b48bf0fa82abed2485ce7" + dependencies: + accepts "~1.3.3" + batch "0.6.1" + debug "2.6.8" + escape-html "~1.0.3" + http-errors "~1.6.1" + mime-types "~2.1.15" + parseurl "~1.3.1" + +serve-static@1.12.3: + version "1.12.3" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.3.tgz#9f4ba19e2f3030c547f8af99107838ec38d5b1e2" + dependencies: + encodeurl "~1.0.1" + escape-html "~1.0.3" + parseurl "~1.3.1" + send "0.15.3" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +set-immediate-shim@^1.0.0, set-immediate-shim@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + +setimmediate@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + +setprototypeof@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" + +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.8" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.8.tgz#37068c2c476b6baf402d14a49c67f597921f634f" + dependencies: + inherits "^2.0.1" + +shallow-clone@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060" + dependencies: + is-extendable "^0.1.1" + kind-of "^2.0.1" + lazy-cache "^0.2.3" + mixin-object "^2.0.1" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + +shellwords@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.0.tgz#66afd47b6a12932d9071cbfd98a52e785cd0ba14" + +sigmund@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" + +signal-exit@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + dependencies: + hoek "2.x.x" + +sockjs-client@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.2.tgz#f0212a8550e4c9468c8cceaeefd2e3493c033ad5" + dependencies: + debug "^2.2.0" + eventsource "0.1.6" + faye-websocket "~0.11.0" + inherits "^2.0.1" + json3 "^3.3.2" + url-parse "^1.1.1" + +sockjs@0.3.18: + version "0.3.18" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.18.tgz#d9b289316ca7df77595ef299e075f0f937eb4207" + dependencies: + faye-websocket "^0.10.0" + uuid "^2.0.2" + +sort-keys@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" + dependencies: + is-plain-obj "^1.0.0" + +source-list-map@^0.1.7: + version "0.1.8" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106" + +source-list-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" + +source-map-resolve@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.3.1.tgz#610f6122a445b8dd51535a2a71b783dfc1248761" + dependencies: + atob "~1.1.0" + resolve-url "~0.2.1" + source-map-url "~0.3.0" + urix "~0.1.0" + +source-map-support@^0.4.2: + version "0.4.15" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.15.tgz#03202df65c06d2bd8c7ec2362a193056fef8d3b1" + dependencies: + source-map "^0.5.6" + +source-map-url@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.3.0.tgz#7ecaf13b57bcd09da8a40c5d269db33799d4aaf9" + +source-map@^0.1.38: + version "0.1.43" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" + dependencies: + amdefine ">=0.0.4" + +source-map@^0.4.2: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" + dependencies: + amdefine ">=0.0.4" + +source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3, source-map@0.5.x: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + +sparkles@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.0.tgz#1acbbfb592436d10bbe8f785b7cc6f82815012c3" + +spdx-correct@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" + dependencies: + spdx-license-ids "^1.0.2" + +spdx-expression-parse@~1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" + +spdx-license-ids@^1.0.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" + +spdy-transport@^2.0.18: + version "2.0.20" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-2.0.20.tgz#735e72054c486b2354fe89e702256004a39ace4d" + dependencies: + debug "^2.6.8" + detect-node "^2.0.3" + hpack.js "^2.1.6" + obuf "^1.1.1" + readable-stream "^2.2.9" + safe-buffer "^5.0.1" + wbuf "^1.7.2" + +spdy@^3.4.1: + version "3.4.7" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-3.4.7.tgz#42ff41ece5cc0f99a3a6c28aabb73f5c3b03acbc" + dependencies: + debug "^2.6.8" + handle-thing "^1.2.5" + http-deceiver "^1.2.7" + safe-buffer "^5.0.1" + select-hose "^2.0.0" + spdy-transport "^2.0.18" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +squeak@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/squeak/-/squeak-1.3.0.tgz#33045037b64388b567674b84322a6521073916c3" + dependencies: + chalk "^1.0.0" + console-stream "^0.1.1" + lpad-align "^1.0.1" + +sshpk@^1.7.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +stackframe@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.3.tgz#fe64ab20b170e4ce49044b126c119dfa0e5dc7cc" + +stat-mode@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-0.2.2.tgz#e6c80b623123d7d80cf132ce538f346289072502" + +"statuses@>= 1.3.1 < 2", statuses@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" + +stdout-stream@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b" + dependencies: + readable-stream "^2.0.1" + +stream-browserify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-combiner2@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stream-combiner2/-/stream-combiner2-1.1.1.tgz#fb4d8a1420ea362764e21ad4780397bebcb41cbe" + dependencies: + duplexer2 "~0.1.0" + readable-stream "^2.0.2" + +stream-http@^2.3.1: + version "2.7.2" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.2.tgz#40a050ec8dc3b53b33d9909415c02c0bf1abfbad" + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.2.6" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +stream-shift@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + +string_decoder@^0.10.25, string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + +string_decoder@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" + dependencies: + safe-buffer "~5.1.0" + +string-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-1.0.1.tgz#56970fb1c38558e9e70b728bf3de269ac45adfac" + dependencies: + strip-ansi "^3.0.0" + +string-width@^1.0.1, string-width@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.0.tgz#030664561fc146c9423ec7d978fe2457437fe6d0" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string.prototype.codepointat@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.0.tgz#6b26e9bd3afcaa7be3b4269b526de1b82000ac78" + +stringstream@~0.0.4: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-bom-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz#e7144398577d51a6bed0fa1994fa05f43fd988ee" + dependencies: + first-chunk-stream "^1.0.0" + strip-bom "^2.0.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + dependencies: + is-utf8 "^0.2.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-dirs@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-1.1.1.tgz#960bbd1287844f3975a4558aa103a8255e2456a0" + dependencies: + chalk "^1.0.0" + get-stdin "^4.0.1" + is-absolute "^0.1.5" + is-natural-number "^2.0.0" + minimist "^1.1.0" + sum-up "^1.0.1" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + dependencies: + get-stdin "^4.0.1" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +strip-outer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-outer/-/strip-outer-1.0.0.tgz#aac0ba60d2e90c5d4f275fd8869fd9a2d310ffb8" + dependencies: + escape-string-regexp "^1.0.2" + +style-loader@^0.18.1: + version "0.18.2" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.18.2.tgz#cc31459afbcd6d80b7220ee54b291a9fd66ff5eb" + dependencies: + loader-utils "^1.0.2" + schema-utils "^0.3.0" + +sum-up@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sum-up/-/sum-up-1.0.3.tgz#1c661f667057f63bcb7875aa1438bc162525156e" + dependencies: + chalk "^1.0.0" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^3.1.0, supports-color@^3.1.1, supports-color@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" + dependencies: + has-flag "^1.0.0" + +supports-color@^4.0.0, supports-color@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.0.tgz#ad986dc7eb2315d009b4d77c8169c2231a684037" + dependencies: + has-flag "^2.0.0" + +svgo@^0.7.0: + version "0.7.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" + dependencies: + coa "~1.0.1" + colors "~1.1.2" + csso "~2.3.1" + js-yaml "~3.7.0" + mkdirp "~0.5.1" + sax "~1.2.1" + whet.extend "~0.9.9" + +tapable@^0.2.5, tapable@~0.2.5: + version "0.2.6" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.6.tgz#206be8e188860b514425375e6f1ae89bfb01fd8d" + +tar-pack@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984" + dependencies: + debug "^2.2.0" + fstream "^1.0.10" + fstream-ignore "^1.0.5" + once "^1.3.3" + readable-stream "^2.1.4" + rimraf "^2.5.1" + tar "^2.2.1" + uid-number "^0.0.6" + +tar-stream@^1.1.1: + version "1.5.4" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.5.4.tgz#36549cf04ed1aee9b2a30c0143252238daf94016" + dependencies: + bl "^1.0.0" + end-of-stream "^1.0.0" + readable-stream "^2.0.0" + xtend "^4.0.0" + +tar@^2.0.0, tar@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" + dependencies: + block-stream "*" + fstream "^1.0.2" + inherits "2" + +temp-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" + +tempfile@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/tempfile/-/tempfile-1.1.1.tgz#5bcc4eaecc4ab2c707d8bc11d99ccc9a2cb287f2" + dependencies: + os-tmpdir "^1.0.0" + uuid "^2.0.1" + +tempfile@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tempfile/-/tempfile-2.0.0.tgz#6b0446856a9b1114d1856ffcbe509cccb0977265" + dependencies: + temp-dir "^1.0.0" + uuid "^3.0.1" + +through@~2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +through2-filter@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-2.0.0.tgz#60bc55a0dacb76085db1f9dae99ab43f83d622ec" + dependencies: + through2 "~2.0.0" + xtend "~4.0.0" + +through2@^0.6.0, through2@^0.6.1: + version "0.6.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48" + dependencies: + readable-stream ">=1.0.33-1 <1.1.0-0" + xtend ">=4.0.0 <4.1.0-0" + +through2@^2.0.0, through2@~2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" + dependencies: + readable-stream "^2.1.5" + xtend "~4.0.1" + +thunky@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-0.1.0.tgz#bf30146824e2b6e67b0f2d7a4ac8beb26908684e" + +time-stamp@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" + +timed-out@^3.0.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-3.1.3.tgz#95860bfcc5c76c277f8f8326fd0f5b2e20eba217" + +timers-browserify@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.2.tgz#ab4883cf597dcd50af211349a00fbca56ac86b86" + dependencies: + setimmediate "^1.0.4" + +to-absolute-glob@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz#1cdfa472a9ef50c239ee66999b662ca0eb39937f" + dependencies: + extend-shallow "^2.0.1" + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + +to-fast-properties@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" + +tough-cookie@~2.3.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" + dependencies: + punycode "^1.4.1" + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + +trim-repeated@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21" + dependencies: + escape-string-regexp "^1.0.2" + +trim-right@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + +tunnel-agent@^0.4.0: + version "0.4.3" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +type-is@~1.6.15: + version "1.6.15" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.15" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +uglify-js@^2.8.28, uglify-js@^2.8.29: + version "2.8.29" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" + dependencies: + source-map "~0.5.1" + yargs "~3.10.0" + optionalDependencies: + uglify-to-browserify "~1.0.0" + +uglify-js@3.0.x: + version "3.0.24" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.0.24.tgz#ee93400ad9857fb7a1671778db83f6a23f033121" + dependencies: + commander "~2.9.0" + source-map "~0.5.1" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" + +uglifyjs-webpack-plugin@^0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309" + dependencies: + source-map "^0.5.6" + uglify-js "^2.8.29" + webpack-sources "^1.0.1" + +uid-number@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" + +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + +uniqid@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/uniqid/-/uniqid-4.1.1.tgz#89220ddf6b751ae52b5f72484863528596bb84c1" + dependencies: + macaddress "^0.2.8" + +uniqs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" + +unique-stream@^2.0.2: + version "2.2.1" + resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.2.1.tgz#5aa003cfbe94c5ff866c4e7d668bb1c4dbadb369" + dependencies: + json-stable-stringify "^1.0.0" + through2-filter "^2.0.0" + +universalify@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.0.tgz#9eb1c4651debcc670cc94f1a75762332bb967778" + +unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + +unzip-response@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe" + +upper-case@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" + +urix@^0.1.0, urix@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + +url-parse-lax@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" + dependencies: + prepend-http "^1.0.1" + +url-parse@^1.1.1: + version "1.1.9" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.9.tgz#c67f1d775d51f0a18911dd7b3ffad27bb9e5bd19" + dependencies: + querystringify "~1.0.0" + requires-port "1.0.x" + +url-parse@1.0.x: + version "1.0.5" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.0.5.tgz#0854860422afdcfefeb6c965c662d4800169927b" + dependencies: + querystringify "0.0.x" + requires-port "1.0.x" + +url-regex@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/url-regex/-/url-regex-3.2.0.tgz#dbad1e0c9e29e105dd0b1f09f6862f7fdb482724" + dependencies: + ip-regex "^1.0.1" + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +util@^0.10.3, util@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + dependencies: + inherits "2.0.1" + +utils-merge@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" + +uuid@^2.0.1, uuid@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" + +uuid@^3.0.0, uuid@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" + +vali-date@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/vali-date/-/vali-date-1.0.0.tgz#1b904a59609fb328ef078138420934f6b86709a6" + +validate-npm-package-license@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + dependencies: + spdx-correct "~1.0.0" + spdx-expression-parse "~1.0.0" + +vary@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" + +vendors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22" + +verror@1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" + dependencies: + extsprintf "1.0.2" + +vinyl-assign@^1.0.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/vinyl-assign/-/vinyl-assign-1.2.1.tgz#4d198891b5515911d771a8cd9c5480a46a074a45" + dependencies: + object-assign "^4.0.1" + readable-stream "^2.0.0" + +vinyl-fs@^2.2.0: + version "2.4.4" + resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-2.4.4.tgz#be6ff3270cb55dfd7d3063640de81f25d7532239" + dependencies: + duplexify "^3.2.0" + glob-stream "^5.3.2" + graceful-fs "^4.0.0" + gulp-sourcemaps "1.6.0" + is-valid-glob "^0.3.0" + lazystream "^1.0.0" + lodash.isequal "^4.0.0" + merge-stream "^1.0.0" + mkdirp "^0.5.0" + object-assign "^4.0.0" + readable-stream "^2.0.4" + strip-bom "^2.0.0" + strip-bom-stream "^1.0.0" + through2 "^2.0.0" + through2-filter "^2.0.0" + vali-date "^1.0.0" + vinyl "^1.0.0" + +vinyl@^0.4.3: + version "0.4.6" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.4.6.tgz#2f356c87a550a255461f36bbeb2a5ba8bf784847" + dependencies: + clone "^0.2.0" + clone-stats "^0.0.1" + +vinyl@^0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.5.3.tgz#b0455b38fc5e0cf30d4325132e461970c2091cde" + dependencies: + clone "^1.0.0" + clone-stats "^0.0.1" + replace-ext "0.0.1" + +vinyl@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884" + dependencies: + clone "^1.0.0" + clone-stats "^0.0.1" + replace-ext "0.0.1" + +vm-browserify@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" + dependencies: + indexof "0.0.1" + +vue-hot-reload-api@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.1.0.tgz#9ca58a6e0df9078554ce1708688b6578754d86de" + +vue-loader@^12.1.1: + version "12.2.2" + resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-12.2.2.tgz#2b3a764f27018f975bc78cb8b1f55137548ee2d7" + dependencies: + consolidate "^0.14.0" + hash-sum "^1.0.2" + js-beautify "^1.6.3" + loader-utils "^1.1.0" + lru-cache "^4.0.1" + postcss "^5.0.21" + postcss-load-config "^1.1.0" + postcss-selector-parser "^2.0.0" + resolve "^1.3.3" + source-map "^0.5.6" + vue-hot-reload-api "^2.1.0" + vue-style-loader "^3.0.0" + vue-template-es2015-compiler "^1.2.2" + +vue-style-loader@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-3.0.1.tgz#c8b639bb2f24baf9d78274dc17e4f264c1deda08" + dependencies: + hash-sum "^1.0.2" + loader-utils "^1.0.2" + +vue-template-compiler@^2.3.3: + version "2.4.1" + resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.4.1.tgz#20115cf8714f222f9be4111ec75b079a1c9b8197" + dependencies: + de-indent "^1.0.2" + he "^1.1.0" + +vue-template-es2015-compiler@^1.2.2: + version "1.5.3" + resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.5.3.tgz#22787de4e37ebd9339b74223bc467d1adee30545" + +vue@^2.1.10: + version "2.4.1" + resolved "https://registry.yarnpkg.com/vue/-/vue-2.4.1.tgz#76e0b8eee614613532216b7bfe784e0b5695b160" + +ware@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ware/-/ware-1.3.0.tgz#d1b14f39d2e2cb4ab8c4098f756fe4b164e473d4" + dependencies: + wrap-fn "^0.1.0" + +watchpack@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.3.1.tgz#7d8693907b28ce6013e7f3610aa2a1acf07dad87" + dependencies: + async "^2.1.2" + chokidar "^1.4.3" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.2.tgz#d697b99f1f59512df2751be42769c1580b5801fe" + dependencies: + minimalistic-assert "^1.0.0" + +webpack-chunk-hash@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/webpack-chunk-hash/-/webpack-chunk-hash-0.4.0.tgz#6b40c3070fbc9ff0cfe0fe781c7174af6c7c16a4" + +webpack-dev-middleware@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.11.0.tgz#09691d0973a30ad1f82ac73a12e2087f0a4754f9" + dependencies: + memory-fs "~0.4.1" + mime "^1.3.4" + path-is-absolute "^1.0.0" + range-parser "^1.0.3" + +webpack-dev-server@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.5.1.tgz#a02e726a87bb603db5d71abb7d6d2649bf10c769" + dependencies: + ansi-html "0.0.7" + bonjour "^3.5.0" + chokidar "^1.6.0" + compression "^1.5.2" + connect-history-api-fallback "^1.3.0" + del "^3.0.0" + express "^4.13.3" + html-entities "^1.2.0" + http-proxy-middleware "~0.17.4" + internal-ip "^1.2.0" + opn "4.0.2" + portfinder "^1.0.9" + selfsigned "^1.9.1" + serve-index "^1.7.2" + sockjs "0.3.18" + sockjs-client "1.1.2" + spdy "^3.4.1" + strip-ansi "^3.0.0" + supports-color "^3.1.1" + webpack-dev-middleware "^1.11.0" + yargs "^6.0.0" + +webpack-merge@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.1.0.tgz#6ad72223b3e0b837e531e4597c199f909361511e" + dependencies: + lodash "^4.17.4" + +webpack-notifier@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/webpack-notifier/-/webpack-notifier-1.5.0.tgz#c010007d448cebc34defc99ecf288fa5e8c6baf6" + dependencies: + node-notifier "^4.1.0" + object-assign "^4.1.0" + strip-ansi "^3.0.1" + +webpack-sources@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf" + dependencies: + source-list-map "^2.0.0" + source-map "~0.5.3" + +webpack@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.2.0.tgz#8b0cae0e1a9fd76bfbf0eab61a8c2ada848c312f" + dependencies: + acorn "^5.0.0" + acorn-dynamic-import "^2.0.0" + ajv "^5.1.5" + ajv-keywords "^2.0.0" + async "^2.1.2" + enhanced-resolve "^3.3.0" + escope "^3.6.0" + interpret "^1.0.0" + json-loader "^0.5.4" + json5 "^0.5.1" + loader-runner "^2.3.0" + loader-utils "^1.1.0" + memory-fs "~0.4.1" + mkdirp "~0.5.0" + node-libs-browser "^2.0.0" + source-map "^0.5.3" + supports-color "^3.1.0" + tapable "~0.2.5" + uglifyjs-webpack-plugin "^0.4.6" + watchpack "^1.3.1" + webpack-sources "^1.0.1" + yargs "^6.0.0" + +websocket-driver@>=0.5.1: + version "0.6.5" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36" + dependencies: + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.1.tgz#76899499c184b6ef754377c2dbb0cd6cb55d29e7" + +whet.extend@~0.9.9: + version "0.9.9" + resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1" + +which-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + +which@^1.0.5, which@^1.2.9, which@1: + version "1.2.14" + resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5" + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" + dependencies: + string-width "^1.0.2" + +window-size@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrap-fn@^0.1.0: + version "0.1.5" + resolved "https://registry.yarnpkg.com/wrap-fn/-/wrap-fn-0.1.5.tgz#f21b6e41016ff4a7e31720dbc63a09016bdf9845" + dependencies: + co "3.1.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +xml-char-classes@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/xml-char-classes/-/xml-char-classes-1.0.0.tgz#64657848a20ffc5df583a42ad8a277b4512bbc4d" + +xmlhttprequest@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" + +xtend@^4.0.0, "xtend@>=4.0.0 <4.1.0-0", xtend@~4.0.0, xtend@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + +yargs-parser@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c" + dependencies: + camelcase "^3.0.0" + +yargs-parser@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" + dependencies: + camelcase "^3.0.0" + +yargs-parser@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" + dependencies: + camelcase "^4.1.0" + +yargs@^6.0.0: + version "6.6.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208" + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "^4.2.0" + +yargs@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "^5.0.0" + +yargs@^8.0.1: + version "8.0.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360" + dependencies: + camelcase "^4.1.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + read-pkg-up "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^7.0.0" + +yargs@~3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "0.1.0" + +yauzl@^2.2.1: + version "2.8.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.8.0.tgz#79450aff22b2a9c5a41ef54e02db907ccfbf9ee2" + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.0.1" +