diff --git a/composer.json b/composer.json index c58c0f8..96f319d 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,8 @@ "php": ">=7.1", "ddtraceweb/monolog-parser": "^1.2", "kg-bot/laravel-localization-to-vue": "1.*", - "laravel/framework": "5.*" + "laravel/framework": "5.*", + "predis/predis": "^1.1" }, "keywords": [ "laravel", diff --git a/package.json b/package.json index 52b8f4f..0089468 100755 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "vue-spinner": "^1.0.3", "vue-toasted": "^1.1.24", "vue2-dropzone": "^3.1.0", - "vuejs-datepicker": "^0.9.29" + "vuejs-datepicker": "^0.9.29", + "vuex": "^3.0.1" } } diff --git a/routes.php b/routes.php index 7af3d2b..24e9f54 100644 --- a/routes.php +++ b/routes.php @@ -37,7 +37,7 @@ Route::group( [ 'prefix' => config( 'laravel-deploy.routes.prefix' ) . '/ajax', - 'middleware' => array_merge( [ 'web' ], config( 'laravel-deploy.front.routes.ajax.middleware' ) ), + 'middleware' => array_merge( [ 'web', 'auth' ], config( 'laravel-deploy.front.routes.ajax.middleware' ) ), 'namespace' => config( 'laravel-deploy.front.routes.ajax.namespace' ), ], function () { @@ -71,7 +71,20 @@ /** * Deployments routes */ - Route::post( 'deploy/now/{client}', 'SettingsController@deployNow' ) - ->name( 'laravel-deploy.ajax.settings.deployments.deploy_now' ); + Route::group( [ 'prefix' => 'deployments' ], function () { + + Route::post( 'deploy/now/{client}', 'SettingsController@deployNow' ) + ->name( 'laravel-deploy.ajax.settings.deployments.deploy_now' ); + + /** + * Deployment script routes + */ + Route::get( 'scripts/{client}', 'ClientScriptController@fetch' ) + ->name( 'laravel-deploy.ajax.settings.deployments.scripts.fetch' ); + + Route::post( 'scripts/{client}', 'ClientScriptController@save' ) + ->name( 'laravel-deploy.ajax.settings.deployments.scripts.save' ); + } ); + } ); } ); diff --git a/src/Http/Controllers/Front/Ajax/ClientScriptController.php b/src/Http/Controllers/Front/Ajax/ClientScriptController.php new file mode 100644 index 0000000..894ae65 --- /dev/null +++ b/src/Http/Controllers/Front/Ajax/ClientScriptController.php @@ -0,0 +1,51 @@ +script_source ); + + if ( file_exists( $filepath ) ) { + + $content = file_get_contents( $filepath ); + + return response()->json( compact( 'content' ) ); + + } else { + + throw new FileNotFoundException( 'We can\'t find deploy script defined for this client' ); + } + } + + public function save( Client $client, Request $request ) + { + if ( $request->has( 'content' ) && $content = $request->get( 'content' ) ) { + + $filepath = base_path( DIRECTORY_SEPARATOR . $client->script_source ); + + if ( file_exists( $filepath ) ) { + + file_put_contents( $filepath, $content ); + + return response()->json( 'success' ); + + } else { + + throw new FileNotFoundException( 'We can\'t find deploy script defined for this client' ); + } + + } else { + + throw new BadRequestHttpException( 'Parameter content is required.' ); + } + } +} diff --git a/src/Http/Controllers/Front/Ajax/ClientsController.php b/src/Http/Controllers/Front/Ajax/ClientsController.php index ee645a2..7b71c2d 100644 --- a/src/Http/Controllers/Front/Ajax/ClientsController.php +++ b/src/Http/Controllers/Front/Ajax/ClientsController.php @@ -74,6 +74,6 @@ public function changeAutoDeploy( Client $client ) { $client->changeAutoDeploy(); - return response()->json( 'success' ); + return response()->json( compact( $client ) ); } } diff --git a/src/Http/Controllers/Front/Ajax/SettingsController.php b/src/Http/Controllers/Front/Ajax/SettingsController.php index cf570d6..166f2a5 100644 --- a/src/Http/Controllers/Front/Ajax/SettingsController.php +++ b/src/Http/Controllers/Front/Ajax/SettingsController.php @@ -2,43 +2,33 @@ namespace KgBot\LaravelDeploy\Http\Controllers\Front\Ajax; -use Dubture\Monolog\Reader\LogReader; use KgBot\LaravelDeploy\Http\Controllers\BaseController; use KgBot\LaravelDeploy\Jobs\DeployJob; use KgBot\LaravelDeploy\Models\Client; +use KgBot\LaravelDeploy\Utils\LogParser; class SettingsController extends BaseController { - protected $lf_characters = [ '\r\n', '\n\r', '\r', '\n' ]; /** * Return last deploy log * * @return \Illuminate\Http\JsonResponse + * @throws \Exception */ public function lastLog() { - $log_file_name = config( 'laravel-deploy.log_file_name', 'laravel-log' ); - $path = storage_path( 'logs/' . $log_file_name ); - - if ( file_exists( $path ) ) { - - $reader = new LogReader( $path ); - $pattern = - '/\[(?P.*)\] (?P[\w-\s]+).(?P\w+): (?P[^\[\{]+) (?P[\[\{].*[\]\}]) (?P[\[\{].*[\]\}])/'; - $reader->getParser()->registerPattern( 'newPatternName', $pattern ); - $reader->setPattern( 'newPatternName' ); + $reader = $this->getLogs(); - } else { + if ( is_null( $reader ) ) { return response()->json( 'Log file path does not exist, check your configuration and try again.', 404 ); } if ( count( $reader ) ) { - $log = $reader[ count( $reader ) - 2 ]; - $log[ 'message' ] = str_replace( $this->lf_characters, '
', $log[ 'message' ] ); + $log = $reader[ 0 ]; return response()->json( [ 'log' => $log ] ); @@ -49,25 +39,51 @@ public function lastLog() } /** - * Return collection of all logs + * Read deployment log file and return it's content * - * @return \Illuminate\Http\JsonResponse + * @return array|\KgBot\LaravelDeploy\Utils\LogParser|null + * @throws \Exception */ - public function allLogs() + protected function getLogs() { $log_file_name = config( 'laravel-deploy.log_file_name', 'laravel-log' ); $path = storage_path( 'logs/' . $log_file_name ); if ( file_exists( $path ) ) { - $reader = new LogReader( $path ); - $pattern = - '/\[(?P.*)\] (?P[\w-\s]+).(?P\w+): (?P[^\[\{]+) (?P[\[\{].*[\]\}]) (?P[\[\{].*[\]\}])/'; - $reader->getParser()->registerPattern( 'newPatternName', $pattern ); - $reader->setPattern( 'newPatternName' ); + $file = file_get_contents( $path ); + if ( $file ) { + + $reader = new LogParser(); + $reader = $reader->parse( $file ); + + return $reader; + + } else { + + throw new \Exception( 'Couldn\'t read deployment log file.' ); + } + } else { + return null; + + } + } + + /** + * Return collection of all deploy logs + * + * @return \Illuminate\Http\JsonResponse + * @throws \Exception + */ + public function allLogs() + { + $reader = $this->getLogs(); + + if ( is_null( $reader ) ) { + return response()->json( 'Log file path does not exist, check your configuration and try again.', 404 ); } @@ -81,17 +97,41 @@ public function allLogs() } } + /** + * Start deployment from web dashboard + * + * @param \KgBot\LaravelDeploy\Models\Client $client + * + * @return \Illuminate\Http\JsonResponse + */ public function deployNow( Client $client ) { - dispatch( new DeployJob( $client, base_path( $client->script_source ) ) ); + dispatch( new DeployJob( $client, + base_path( $client->script_source ) ) )->onQueue( config( 'laravel-deploy.queue' ) ); return response()->json( 'success' ); } + /** + * Open settings page of web dashboard + * + * @return \Illuminate\Http\JsonResponse + */ public function index() { - $clients = Client::Active()->get(); + /** + * @var \Illuminate\Support\Collection + */ + $clients = Client::Active()->get(); + + $clients = $clients->each( function ( $client ) { + + $enabled = ( $client->active ) ? 'Enabled' : 'Disabled'; + + return $client->text = $client->name . ' - ' . $enabled; + } ); + $settings = [ 'quick_deploy' => config( 'laravel-deploy.run_deploy' ), diff --git a/src/Jobs/DeployJob.php b/src/Jobs/DeployJob.php index c9056c3..5212564 100644 --- a/src/Jobs/DeployJob.php +++ b/src/Jobs/DeployJob.php @@ -11,6 +11,7 @@ use KgBot\LaravelDeploy\Events\LaravelDeployFinished; use KgBot\LaravelDeploy\Events\LaravelDeployStarted; use KgBot\LaravelDeploy\Models\Client; +use Monolog\Formatter\LineFormatter; use Monolog\Handler\StreamHandler; use Monolog\Logger; use Symfony\Component\Process\Process; @@ -25,6 +26,21 @@ class DeployJob implements ShouldQueue public $client; public $script_file; + /** + * @var Logger + */ + protected $logger; + + /** + * @var Process + */ + protected $process; + + /** + * @var string $command Command to be executed + */ + protected $command; + /** * Create a new job instance. @@ -35,64 +51,66 @@ public function __construct( Client $client, string $script_file ) { $this->client = $client; $this->script_file = $script_file; + $this->setLogger(); + $this->setProcess(); } - /** - * Execute the job. - * - * @return void - */ - public function handle() + protected function setLogger() { - $logger = new Logger( 'laravel-deploy-logger' ); + $this->logger = new Logger( 'laravel-deploy-logger' ); $log_file_name = config( 'laravel-deploy.log_file_name', 'laravel-log' ); $stream = new StreamHandler( storage_path( '/logs/' . $log_file_name ), Logger::DEBUG ); + $formatter = tap( new LineFormatter( null, null, true, true ), function ( $formatter ) { + $formatter->includeStacktraces(); + } ); + $stream->setFormatter( $formatter ); - $logger->pushHandler( $stream ); + $this->logger->pushHandler( $stream ); + } - $command = 'echo \'' . config( 'laravel-deploy.user.password' ); - $command .= '\' | sudo -S -u ' . config( 'laravel-deploy.user.username' ); + protected function setProcess() + { + $command = 'echo ' . config( 'laravel-deploy.user.password' ); + $command .= ' | sudo -S -u ' . config( 'laravel-deploy.user.username' ); $command .= ' sh ' . $this->script_file; - $command = 'sh ' . $this->script_file; - echo $command; + $this->command = $command; $process = new Process( $command ); - $process->setTimeout( 300 ); - $message = ''; - $error = ''; - event( new LaravelDeployStarted( $this->client, $command ) ); - - ini_set( 'max_execution_time', 200 ); - $process->run( function ( $type, $buffer ) use ( &$message, &$error ) { + $process->setTimeout( 500 ); + $process->setIdleTimeout( 100 ); - if ( Process::ERR === $type ) { - - $error .= $buffer . '\r\n'; + $this->process = $process; + } - } else { + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + event( new LaravelDeployStarted( $this->client, $this->command ) ); - $message .= $buffer . '\r\n'; - } - } ); + $this->process->run(); - if ( $message !== '' ) { + if ( $this->process->isSuccessful() ) { - $logger->info( $message, [ + $this->logger->info( PHP_EOL . $this->process->getOutput(), [ 'client' => json_encode( $this->client ), 'script' => $this->script_file, ] ); - event( new LaravelDeployFinished( $this->client, $message ) ); + event( new LaravelDeployFinished( $this->client, $this->process->getOutput() ) ); - } else if ( $error !== '' ) { + } else { - $logger->critical( $error, [ + $this->logger->critical( PHP_EOL . $this->process->getOutput(), [ 'client' => json_encode( $this->client ), 'script' => $this->script_file, ] ); - event( new LaravelDeployFailed( $this->client, $error ) ); + event( new LaravelDeployFailed( $this->client, $this->process->getOutput() ) ); } } diff --git a/src/LaravelDeployServiceProvider.php b/src/LaravelDeployServiceProvider.php index d6bd9c9..0136499 100644 --- a/src/LaravelDeployServiceProvider.php +++ b/src/LaravelDeployServiceProvider.php @@ -9,16 +9,30 @@ namespace KgBot\LaravelDeploy; -use ExportLocalization; use Illuminate\Support\Facades\View; use Illuminate\Support\ServiceProvider; use KgBot\LaravelDeploy\Console\Commands\NewClient; +use KgBot\LaravelLocalization\Facades\ExportLocalizations; class LaravelDeployServiceProvider extends ServiceProvider { public function register() { - + /** + * We define those globals because we need them in LogParser + */ + if ( !defined( 'REGEX_DATE_PATTERN' ) ) { + define( 'REGEX_DATE_PATTERN', '\d{4}(-\d{2}){2}' ); // YYYY-MM-DD + } + if ( !defined( 'REGEX_TIME_PATTERN' ) ) { + define( 'REGEX_TIME_PATTERN', '\d{2}(:\d{2}){2}' ); // HH:MM:SS + } + if ( !defined( 'REGEX_DATETIME_PATTERN' ) ) { + define( + 'REGEX_DATETIME_PATTERN', + REGEX_DATE_PATTERN . ' ' . REGEX_TIME_PATTERN // YYYY-MM-DD HH:MM:SS + ); + } } public function boot() @@ -27,7 +41,7 @@ public function boot() return $view->with( [ 'user' => auth()->user(), - 'messages' => ExportLocalization::export()->toFlat(), + 'messages' => ExportLocalizations::export()->toFlat(), ] ); } ); diff --git a/src/Utils/LogParser.php b/src/Utils/LogParser.php new file mode 100644 index 0000000..2571526 --- /dev/null +++ b/src/Utils/LogParser.php @@ -0,0 +1,142 @@ +getConstants() as $level ) { + + array_push( $this->levels, $level ); + } + } + + /** + * Parse file content. + * + * @param string $raw + * + * @return array + */ + public function parse( $raw ) + { + $this->parsed = []; + list( $headings, $data ) = $this->parseRawData( $raw ); + // @codeCoverageIgnoreStart + if ( !is_array( $headings ) ) { + return $this->parsed; + } + // @codeCoverageIgnoreEnd + foreach ( $headings as $heading ) { + for ( $i = 0, $j = count( $heading ); $i < $j; $i++ ) { + $this->populateEntries( $heading, $data, $i ); + } + }; + unset( $headings, $data ); + + return array_reverse( $this->parsed ); + } + /* ----------------------------------------------------------------- + | Other Methods + | ----------------------------------------------------------------- + */ + /** + * Parse raw data. + * + * @param string $raw + * + * @return array + */ + private function parseRawData( $raw ) + { + preg_match_all( $this->heading_pattern, $raw, $headings ); + $data = preg_split( $this->heading_pattern, $raw ); + if ( $data[ 0 ] < 1 ) { + $trash = array_shift( $data ); + unset( $trash ); + } + + return [ $headings, $data ]; + } + + /** + * Populate entries. + * + * @param array $heading + * @param array $data + * @param int $key + */ + private function populateEntries( $heading, $data, $key ) + { + foreach ( $this->levels as $level ) { + if ( self::hasLogLevel( $heading[ $key ], $level ) ) { + // We use this to get the "extra" part from Monolog logger and provide it to user + preg_match( $this->extra_patter, $data[ $key ], $extra, null, 0 ); + + // Here we just remove Monolog "extra" argument from string + $without_extra = preg_split( $this->extra_patter, $data[ $key ] )[ 0 ]; + + preg_match( $this->date_patern, $heading[ $key ], $created_at ); + + $this->parsed[] = [ + 'date' => Carbon::parse( $created_at[ 0 ] ), + 'level' => strtoupper( $level ), + 'header' => $heading[ $key ], + 'message' => $without_extra, + 'extra' => $extra ? json_decode( $extra[ 0 ] ) : [], + ]; + } + } + } + + /** + * Check if header has a log level. + * + * @param string $heading + * @param string $level + * + * @return bool + */ + private function hasLogLevel( $heading, $level ) + { + return Str::contains( strtolower( $heading ), strtolower( '.' . $level ) ); + } +} \ No newline at end of file diff --git a/src/resources/assets/js/components/Settings/Components/DeploymentScripts.vue b/src/resources/assets/js/components/Settings/Components/DeploymentScripts.vue new file mode 100644 index 0000000..9c42763 --- /dev/null +++ b/src/resources/assets/js/components/Settings/Components/DeploymentScripts.vue @@ -0,0 +1,136 @@ + + + + + \ No newline at end of file diff --git a/src/resources/assets/js/components/Settings/Components/Deployments.vue b/src/resources/assets/js/components/Settings/Components/Deployments.vue index 9865def..fec83a6 100644 --- a/src/resources/assets/js/components/Settings/Components/Deployments.vue +++ b/src/resources/assets/js/components/Settings/Components/Deployments.vue @@ -16,17 +16,11 @@ -

Here you can view last deployment log, all deployment logs. Also you can enable or disable - quick deploy.

+

Here you can view last deployment log, all deployment logs, manually deploy for each + client, etc.