Add web cron ability #821 (#1073)

* Use database for kvp storage

* Read kvpstore

* Add web cron ability through API #821

* Style fixes

* Fix text
This commit is contained in:
Nabeel S 2021-03-09 11:36:56 -05:00 committed by GitHub
parent f1c54bcc7c
commit e70ee5aa6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 243 additions and 3 deletions

View File

@ -0,0 +1,19 @@
<?php
use App\Contracts\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Add a hub to the subfleet is
*/
class AddKvpTable extends Migration
{
public function up()
{
Schema::create('kvp', function (Blueprint $table) {
$table->string('key')->index();
$table->string('value');
});
}
}

View File

@ -361,3 +361,9 @@
options: '' options: ''
type: 'text' type: 'text'
description: 'Discord public channel ID for broadcasat notifications' description: 'Discord public channel ID for broadcasat notifications'
- key: 'cron.random_id'
name: 'Cron Randomized ID'
group: 'cron'
value: ''
type: 'hidden'
description: ''

View File

@ -0,0 +1,37 @@
<?php
namespace App\Exceptions;
class CronInvalid extends AbstractHttpException
{
public const MESSAGE = 'Cron ID is disabled or invalid';
public function __construct()
{
parent::__construct(400, static::MESSAGE);
}
/**
* Return the RFC 7807 error type (without the URL root)
*/
public function getErrorType(): string
{
return 'cron-invalid';
}
/**
* Get the detailed error string
*/
public function getErrorDetails(): string
{
return $this->getMessage();
}
/**
* Return an array with the error details, merged with the RFC7807 response
*/
public function getErrorMetadata(): array
{
return [];
}
}

View File

@ -6,6 +6,7 @@ use App\Contracts\Controller;
use App\Repositories\KvpRepository; use App\Repositories\KvpRepository;
use App\Services\CronService; use App\Services\CronService;
use App\Services\VersionService; use App\Services\VersionService;
use App\Support\Utils;
use Codedge\Updater\UpdaterManager; use Codedge\Updater\UpdaterManager;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
@ -34,7 +35,12 @@ class MaintenanceController extends Controller
public function index() public function index()
{ {
// Get the cron URL
$cron_id = setting('cron.random_id');
$cron_url = empty($cron_id) ? 'Not enabled' : url(route('api.maintenance.cron', $cron_id));
return view('admin.maintenance.index', [ return view('admin.maintenance.index', [
'cron_url' => $cron_url,
'cron_path' => $this->cronSvc->getCronExecString(), 'cron_path' => $this->cronSvc->getCronExecString(),
'cron_problem_exists' => $this->cronSvc->cronProblemExists(), 'cron_problem_exists' => $this->cronSvc->cronProblemExists(),
'new_version' => $this->kvpRepo->get('new_version_available', false), 'new_version' => $this->kvpRepo->get('new_version_available', false),
@ -117,4 +123,33 @@ class MaintenanceController extends Controller
return redirect('/update/downloader'); return redirect('/update/downloader');
} }
/**
* Enable the cron, or if it's enabled, change the ID that is used
*
* @param Request $request
*/
public function cron_enable(Request $request)
{
$id = Utils::generateNewId(24);
setting_save('cron.random_id', $id);
Flash::success('Web cron refreshed!');
return redirect(route('admin.maintenance.index'));
}
/**
* Disable the web cron
*
* @param Request $request
*
* @return mixed
*/
public function cron_disable(Request $request)
{
setting_save('cron.random_id', '');
Flash::success('Web cron disabled!');
return redirect(route('admin.maintenance.index'));
}
} }

View File

@ -62,7 +62,7 @@ class SettingsController extends Controller
*/ */
public function index() public function index()
{ {
$settings = Setting::orderBy('order', 'asc')->get(); $settings = Setting::where('type', '!=', 'hidden')->orderBy('order')->get();
$settings = $settings->groupBy('group'); $settings = $settings->groupBy('group');
return view('admin.settings.index', [ return view('admin.settings.index', [

View File

@ -4,7 +4,6 @@ namespace App\Http\Controllers\Api;
use App\Contracts\Controller; use App\Contracts\Controller;
use App\Exceptions\AssetNotFound; use App\Exceptions\AssetNotFound;
use App\Exceptions\Unauthorized;
use App\Http\Resources\Flight as FlightResource; use App\Http\Resources\Flight as FlightResource;
use App\Http\Resources\Navdata as NavdataResource; use App\Http\Resources\Navdata as NavdataResource;
use App\Models\SimBrief; use App\Models\SimBrief;

View File

@ -0,0 +1,35 @@
<?php
namespace App\Http\Controllers\Api;
use App\Contracts\Controller;
use App\Exceptions\CronInvalid;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
class MaintenanceController extends Controller
{
/**
* Run the cron job from the web
*
* @param Request $request
* @param string $id The ID passed in for the cron
*
* @return mixed
*/
public function cron(Request $request, string $id)
{
$cron_id = setting('cron.random_id');
if (empty($cron_id) || $id !== $cron_id) {
throw new CronInvalid();
}
$output = '';
Artisan::call('schedule:run');
$output .= trim(Artisan::output());
return response([
'content' => $output,
]);
}
}

23
app/Models/Kvp.php Normal file
View File

@ -0,0 +1,23 @@
<?php
namespace App\Models;
use App\Contracts\Model;
/**
* @property string key
* @property string value
*/
class Kvp extends Model
{
public $table = 'kvp';
public $timestamps = false;
public $incrementing = false;
protected $keyType = 'string';
public $fillable = [
'key',
'value',
];
}

View File

@ -10,6 +10,7 @@ use Illuminate\Support\Collection;
* @property int $user_id The user that generated this * @property int $user_id The user that generated this
* @property string $flight_id Optional, if attached to a flight, removed if attached to PIREP * @property string $flight_id Optional, if attached to a flight, removed if attached to PIREP
* @property string $pirep_id Optional, if attached to a PIREP, removed if attached to flight * @property string $pirep_id Optional, if attached to a PIREP, removed if attached to flight
* @property string $aircraft_id The aircraft this is for
* @property string $acars_xml * @property string $acars_xml
* @property string $ofp_xml * @property string $ofp_xml
* @property string $ofp_html * @property string $ofp_html

View File

@ -392,6 +392,12 @@ class RouteServiceProvider extends ServiceProvider
Route::match(['post'], 'maintenance/forcecheck', 'MaintenanceController@forcecheck') Route::match(['post'], 'maintenance/forcecheck', 'MaintenanceController@forcecheck')
->name('maintenance.forcecheck')->middleware('ability:admin,maintenance'); ->name('maintenance.forcecheck')->middleware('ability:admin,maintenance');
Route::match(['post'], 'maintenance/cron_enable', 'MaintenanceController@cron_enable')
->name('maintenance.cron_enable')->middleware('ability:admin,maintenance');
Route::match(['post'], 'maintenance/cron_disable', 'MaintenanceController@cron_disable')
->name('maintenance.cron_disable')->middleware('ability:admin,maintenance');
// subfleet // subfleet
Route::get('subfleets/export', 'SubfleetController@export') Route::get('subfleets/export', 'SubfleetController@export')
->name('subfleets.export')->middleware('ability:admin,fleet'); ->name('subfleets.export')->middleware('ability:admin,fleet');
@ -508,6 +514,8 @@ class RouteServiceProvider extends ServiceProvider
Route::get('pireps/{pirep_id}', 'PirepController@get'); Route::get('pireps/{pirep_id}', 'PirepController@get');
Route::get('pireps/{pirep_id}/acars/geojson', 'AcarsController@acars_geojson'); Route::get('pireps/{pirep_id}/acars/geojson', 'AcarsController@acars_geojson');
Route::get('cron/{id}', 'MaintenanceController@cron')->name('maintenance.cron');
Route::get('news', 'NewsController@index'); Route::get('news', 'NewsController@index');
Route::get('status', 'StatusController@status'); Route::get('status', 'StatusController@status');
Route::get('version', 'StatusController@status'); Route::get('version', 'StatusController@status');

View File

@ -6,7 +6,8 @@
</h6> </h6>
<div class="row" style="padding-top: 5px"> <div class="row" style="padding-top: 5px">
<div class="col-sm-12"> <div class="col-sm-12">
<p>A cron must be created that runs every minute calling artisan. Example:</p> <p>A cron must be created that runs every minute calling artisan. An example is below.
<strong><a href="{{ docs_link('cron') }}" target="_blank">See the docs</a></strong></p>
<label style="width: 100%"> <label style="width: 100%">
<input type="text" value="{{ $cron_path }}" class="form-control" style="width: 100%"/> <input type="text" value="{{ $cron_path }}" class="form-control" style="width: 100%"/>
</label> </label>
@ -20,6 +21,45 @@
@endif @endif
</div> </div>
</div> </div>
<hr>
<div class="row" style="padding-top: 5px">
<div class="col-sm-12">
<h5>Web Cron</h5>
</div>
<div class="col-sm-6">
<p>
If you don't have cron access on your server, you can use a web-cron service to
access this URL every minute. Keep it disabled if you're not using it. It's a
unique ID that can be reset/changed if needed for security.
</p>
</div>
<div class="col-sm-6 pull-right">
<table class="table-condensed">
<tr class="text-right">
<td style="padding-right: 10px;" class="text-right">
{{ Form::open(['url' => route('admin.maintenance.cron_enable'),
'method' => 'post']) }}
{{ Form::button('Enable/Change ID', ['type' => 'submit', 'class' => 'btn btn-success']) }}
{{ Form::close() }}
</td>
<td class="text-right">
{{ Form::open(['url' => route('admin.maintenance.cron_disable'),
'method' => 'post']) }}
{{ Form::button('Disable', ['type' => 'submit', 'class' => 'btn btn-warning']) }}
{{ Form::close() }}
</td>
</tr>
</table>
</div>
<div class="col-sm-12">
<label style="width: 100%">
<input type="text" value="{{ $cron_url }}" class="form-control" style="width: 100%"/>
</label>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -12,6 +12,7 @@ use App\Models\News;
use App\Models\Subfleet; use App\Models\Subfleet;
use App\Models\User; use App\Models\User;
use App\Services\FareService; use App\Services\FareService;
use App\Support\Utils;
use Exception; use Exception;
use function random_int; use function random_int;
@ -312,4 +313,21 @@ class ApiTest extends TestCase
$this->assertNotNull($user); $this->assertNotNull($user);
$this->assertTrue(strpos($user['avatar'], 'gravatar') !== -1); $this->assertTrue(strpos($user['avatar'], 'gravatar') !== -1);
} }
/**
* Test that the web cron runs
*/
public function testWebCron()
{
$this->updateSetting('cron.random_id', '');
$this->get('/api/cron/sdf')->assertStatus(400);
$id = Utils::generateNewId(24);
$this->updateSetting('cron.random_id', $id);
$this->get('/api/cron/sdf')->assertStatus(400);
$res = $this->get('/api/cron/'.$id);
$res->assertStatus(200);
}
} }

View File

@ -2,6 +2,7 @@
namespace Tests; namespace Tests;
use App\Repositories\KvpRepository;
use App\Support\ICAO; use App\Support\ICAO;
use App\Support\Units\Time; use App\Support\Units\Time;
use App\Support\Utils; use App\Support\Utils;
@ -15,6 +16,24 @@ class UtilsTest extends TestCase
$this->assertNotNull($carbon); $this->assertNotNull($carbon);
} }
/**
* Simple test for KVP
*/
public function testKvp()
{
/** @var KvpRepository $kvpRepo */
$kvpRepo = app(KvpRepository::class);
$kvpRepo->save('testkey', 'some value');
$this->assertEquals('some value', $kvpRepo->get('testkey'));
// test that default value is working
$this->assertEquals('default value', $kvpRepo->get('unknownkey', 'default value'));
// try saving an integer
$kvpRepo->save('intval', 1);
$this->assertEquals(1, $kvpRepo->get('intval'));
}
/** /**
* @throws \Exception * @throws \Exception
*/ */