Remove nests

This commit is contained in:
Lance Pioch 2024-03-14 01:27:50 -04:00
parent 05681641d3
commit a296084d6e
79 changed files with 365 additions and 1720 deletions

View File

@ -1,9 +1,6 @@
name: Tests
on:
push:
branches:
- '**'
pull_request:
branches:
- '**'

View File

@ -30,9 +30,4 @@ interface EggRepositoryInterface extends RepositoryInterface
* @throws \App\Exceptions\Repository\RecordNotFoundException
*/
public function getWithExportAttributes(int $id): Egg;
/**
* Confirm a copy script belongs to the same nest as the item trying to use it.
*/
public function isCopyableScript(int $copyFromId, int $service): bool;
}

View File

@ -1,30 +0,0 @@
<?php
namespace App\Contracts\Repository;
use App\Models\Nest;
use Illuminate\Database\Eloquent\Collection;
interface NestRepositoryInterface extends RepositoryInterface
{
/**
* Return a nest or all nests with their associated eggs and variables.
*
* @throws \App\Exceptions\Repository\RecordNotFoundException
*/
public function getWithEggs(int $id = null): Collection|Nest;
/**
* Return a nest or all nests and the count of eggs and servers for that nest.
*
* @throws \App\Exceptions\Repository\RecordNotFoundException
*/
public function getWithCounts(int $id = null): Collection|Nest;
/**
* Return a nest along with its associated eggs and the servers relation on those eggs.
*
* @throws \App\Exceptions\Repository\RecordNotFoundException
*/
public function getWithEggServers(int $id): Nest;
}

View File

@ -1,9 +0,0 @@
<?php
namespace App\Exceptions\Service\Egg;
use App\Exceptions\DisplayException;
class InvalidCopyFromException extends DisplayException
{
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Controllers\Admin\Nests;
namespace App\Http\Controllers\Admin\Eggs;
use Illuminate\View\View;
use App\Models\Egg;
@ -13,7 +13,6 @@ use App\Services\Eggs\EggCreationService;
use App\Services\Eggs\EggDeletionService;
use App\Http\Requests\Admin\Egg\EggFormRequest;
use App\Contracts\Repository\EggRepositoryInterface;
use App\Contracts\Repository\NestRepositoryInterface;
class EggController extends Controller
{
@ -26,11 +25,22 @@ class EggController extends Controller
protected EggDeletionService $deletionService,
protected EggRepositoryInterface $repository,
protected EggUpdateService $updateService,
protected NestRepositoryInterface $nestRepository,
protected ViewFactory $view
) {
}
/**
* Render eggs listing page.
*
* @throws \App\Exceptions\Repository\RecordNotFoundException
*/
public function index(): View
{
return $this->view->make('admin.eggs.index', [
'eggs' => Egg::all(),
]);
}
/**
* Handle a request to display the Egg creation page.
*
@ -38,10 +48,10 @@ class EggController extends Controller
*/
public function create(): View
{
$nests = $this->nestRepository->getWithEggs();
\JavaScript::put(['nests' => $nests->keyBy('id')]);
$eggs = Egg::all();
\JavaScript::put(['eggs' => $eggs->keyBy('id')]);
return $this->view->make('admin.eggs.new', ['nests' => $nests]);
return $this->view->make('admin.eggs.new', ['eggs' => $eggs]);
}
/**
@ -56,9 +66,9 @@ class EggController extends Controller
$data['docker_images'] = $this->normalizeDockerImages($data['docker_images'] ?? null);
$egg = $this->creationService->handle($data);
$this->alert->success(trans('admin/nests.eggs.notices.egg_created'))->flash();
$this->alert->success(trans('admin/eggs.notices.egg_created'))->flash();
return redirect()->route('admin.nests.egg.view', $egg->id);
return redirect()->route('admin.eggs.view', $egg->id);
}
/**
@ -89,9 +99,9 @@ class EggController extends Controller
$data['docker_images'] = $this->normalizeDockerImages($data['docker_images'] ?? null);
$this->updateService->handle($egg, $data);
$this->alert->success(trans('admin/nests.eggs.notices.updated'))->flash();
$this->alert->success(trans('admin/eggs.notices.updated'))->flash();
return redirect()->route('admin.nests.egg.view', $egg->id);
return redirect()->route('admin.eggs.view', $egg->id);
}
/**
@ -103,9 +113,9 @@ class EggController extends Controller
public function destroy(Egg $egg): RedirectResponse
{
$this->deletionService->handle($egg->id);
$this->alert->success(trans('admin/nests.eggs.notices.deleted'))->flash();
$this->alert->success(trans('admin/eggs.notices.deleted'))->flash();
return redirect()->route('admin.nests.view', $egg->nest_id);
return redirect()->route('admin.eggs.view', $egg->id);
}
/**

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Controllers\Admin\Nests;
namespace App\Http\Controllers\Admin\Eggs;
use Illuminate\View\View;
use App\Models\Egg;
@ -33,7 +33,6 @@ class EggScriptController extends Controller
$egg = $this->repository->getWithCopyAttributes($egg);
$copy = $this->repository->findWhere([
['copy_script_from', '=', null],
['nest_id', '=', $egg->nest_id],
['id', '!=', $egg],
]);
@ -53,13 +52,12 @@ class EggScriptController extends Controller
*
* @throws \App\Exceptions\Model\DataValidationException
* @throws \App\Exceptions\Repository\RecordNotFoundException
* @throws \App\Exceptions\Service\Egg\InvalidCopyFromException
*/
public function update(EggScriptFormRequest $request, Egg $egg): RedirectResponse
{
$this->installScriptService->handle($egg, $request->normalize());
$this->alert->success(trans('admin/nests.eggs.notices.script_updated'))->flash();
$this->alert->success(trans('admin/eggs.notices.script_updated'))->flash();
return redirect()->route('admin.nests.egg.scripts', $egg);
return redirect()->route('admin.eggs.scripts', $egg);
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Controllers\Admin\Nests;
namespace App\Http\Controllers\Admin\Eggs;
use App\Models\Egg;
use Illuminate\Http\RedirectResponse;
@ -41,7 +41,7 @@ class EggShareController extends Controller
}
/**
* Import a new service option using an XML file.
* Import a new egg using an XML file.
*
* @throws \App\Exceptions\Model\DataValidationException
* @throws \App\Exceptions\Repository\RecordNotFoundException
@ -50,10 +50,10 @@ class EggShareController extends Controller
*/
public function import(EggImportFormRequest $request): RedirectResponse
{
$egg = $this->importerService->handle($request->file('import_file'), $request->input('import_to_nest'));
$this->alert->success(trans('admin/nests.eggs.notices.imported'))->flash();
$egg = $this->importerService->handle($request->file('import_file'));
$this->alert->success(trans('admin/eggs.notices.imported'))->flash();
return redirect()->route('admin.nests.egg.view', ['egg' => $egg->id]);
return redirect()->route('admin.eggs.view', ['egg' => $egg->id]);
}
/**
@ -67,8 +67,8 @@ class EggShareController extends Controller
public function update(EggImportFormRequest $request, Egg $egg): RedirectResponse
{
$this->updateImporterService->handle($egg, $request->file('import_file'));
$this->alert->success(trans('admin/nests.eggs.notices.updated_via_import'))->flash();
$this->alert->success(trans('admin/eggs.notices.updated_via_import'))->flash();
return redirect()->route('admin.nests.egg.view', ['egg' => $egg]);
return redirect()->route('admin.eggs.view', ['egg' => $egg]);
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Controllers\Admin\Nests;
namespace App\Http\Controllers\Admin\Eggs;
use Illuminate\View\View;
use App\Models\Egg;
@ -52,9 +52,9 @@ class EggVariableController extends Controller
public function store(EggVariableFormRequest $request, Egg $egg): RedirectResponse
{
$this->creationService->handle($egg->id, $request->normalize());
$this->alert->success(trans('admin/nests.variables.notices.variable_created'))->flash();
$this->alert->success(trans('admin/eggs.variables.notices.variable_created'))->flash();
return redirect()->route('admin.nests.egg.variables', $egg->id);
return redirect()->route('admin.eggs.variables', $egg->id);
}
/**
@ -68,11 +68,11 @@ class EggVariableController extends Controller
public function update(EggVariableFormRequest $request, Egg $egg, EggVariable $variable): RedirectResponse
{
$this->updateService->handle($variable, $request->normalize());
$this->alert->success(trans('admin/nests.variables.notices.variable_updated', [
$this->alert->success(trans('admin/eggs.variables.notices.variable_updated', [
'variable' => $variable->name,
]))->flash();
return redirect()->route('admin.nests.egg.variables', $egg->id);
return redirect()->route('admin.eggs.variables', $egg->id);
}
/**
@ -81,10 +81,10 @@ class EggVariableController extends Controller
public function destroy(int $egg, EggVariable $variable): RedirectResponse
{
$this->variableRepository->delete($variable->id);
$this->alert->success(trans('admin/nests.variables.notices.variable_deleted', [
$this->alert->success(trans('admin/eggs.variables.notices.variable_deleted', [
'variable' => $variable->name,
]))->flash();
return redirect()->route('admin.nests.egg.variables', $egg);
return redirect()->route('admin.eggs.variables', $egg);
}
}

View File

@ -2,10 +2,10 @@
namespace App\Http\Controllers\Admin;
use App\Models\Egg;
use Ramsey\Uuid\Uuid;
use Illuminate\View\View;
use Illuminate\Http\Request;
use App\Models\Nest;
use Illuminate\Http\Response;
use App\Models\Mount;
use App\Models\Location;
@ -15,7 +15,6 @@ use Illuminate\View\Factory as ViewFactory;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\MountFormRequest;
use App\Repositories\Eloquent\MountRepository;
use App\Contracts\Repository\NestRepositoryInterface;
use App\Contracts\Repository\LocationRepositoryInterface;
class MountController extends Controller
@ -25,7 +24,6 @@ class MountController extends Controller
*/
public function __construct(
protected AlertsMessageBag $alert,
protected NestRepositoryInterface $nestRepository,
protected LocationRepositoryInterface $locationRepository,
protected MountRepository $repository,
protected ViewFactory $view
@ -49,12 +47,12 @@ class MountController extends Controller
*/
public function view(string $id): View
{
$nests = Nest::query()->with('eggs')->get();
$eggs = Egg::all();
$locations = Location::query()->with('nodes')->get();
return $this->view->make('admin.mounts.view', [
'mount' => $this->repository->getWithRelations($id),
'nests' => $nests,
'eggs' => $eggs,
'locations' => $locations,
]);
}

View File

@ -1,102 +0,0 @@
<?php
namespace App\Http\Controllers\Admin\Nests;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
use App\Http\Controllers\Controller;
use App\Services\Nests\NestUpdateService;
use App\Services\Nests\NestCreationService;
use App\Services\Nests\NestDeletionService;
use App\Contracts\Repository\NestRepositoryInterface;
use App\Http\Requests\Admin\Nest\StoreNestFormRequest;
class NestController extends Controller
{
/**
* NestController constructor.
*/
public function __construct(
protected AlertsMessageBag $alert,
protected NestCreationService $nestCreationService,
protected NestDeletionService $nestDeletionService,
protected NestRepositoryInterface $repository,
protected NestUpdateService $nestUpdateService,
protected ViewFactory $view
) {
}
/**
* Render nest listing page.
*
* @throws \App\Exceptions\Repository\RecordNotFoundException
*/
public function index(): View
{
return $this->view->make('admin.nests.index', [
'nests' => $this->repository->getWithCounts(),
]);
}
/**
* Render nest creation page.
*/
public function create(): View
{
return $this->view->make('admin.nests.new');
}
/**
* Handle the storage of a new nest.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function store(StoreNestFormRequest $request): RedirectResponse
{
$nest = $this->nestCreationService->handle($request->normalize());
$this->alert->success(trans('admin/nests.notices.created', ['name' => $nest->name]))->flash();
return redirect()->route('admin.nests.view', $nest->id);
}
/**
* Return details about a nest including all the eggs and servers per egg.
*
* @throws \App\Exceptions\Repository\RecordNotFoundException
*/
public function view(int $nest): View
{
return $this->view->make('admin.nests.view', [
'nest' => $this->repository->getWithEggServers($nest),
]);
}
/**
* Handle request to update a nest.
*
* @throws \App\Exceptions\Model\DataValidationException
* @throws \App\Exceptions\Repository\RecordNotFoundException
*/
public function update(StoreNestFormRequest $request, int $nest): RedirectResponse
{
$this->nestUpdateService->handle($nest, $request->normalize());
$this->alert->success(trans('admin/nests.notices.updated'))->flash();
return redirect()->route('admin.nests.view', $nest);
}
/**
* Handle request to delete a nest.
*
* @throws \App\Exceptions\Service\HasActiveServersException
*/
public function destroy(int $nest): RedirectResponse
{
$this->nestDeletionService->handle($nest);
$this->alert->success(trans('admin/nests.notices.deleted'))->flash();
return redirect()->route('admin.nests');
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Admin\Servers;
use App\Models\Egg;
use Illuminate\View\View;
use App\Models\Node;
use App\Models\Location;
@ -9,7 +10,6 @@ use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
use App\Http\Controllers\Controller;
use App\Repositories\Eloquent\NestRepository;
use App\Repositories\Eloquent\NodeRepository;
use App\Http\Requests\Admin\ServerFormRequest;
use App\Services\Servers\ServerCreationService;
@ -21,7 +21,6 @@ class CreateServerController extends Controller
*/
public function __construct(
private AlertsMessageBag $alert,
private NestRepository $nestRepository,
private NodeRepository $nodeRepository,
private ServerCreationService $creationService,
private ViewFactory $view
@ -42,20 +41,16 @@ class CreateServerController extends Controller
return redirect()->route('admin.nodes');
}
$nests = $this->nestRepository->getWithEggs();
$eggs = Egg::with('variables')->get();
\JavaScript::put([
'nodeData' => $this->nodeRepository->getNodesForServerCreation(),
'nests' => $nests->map(function ($item) {
return array_merge($item->toArray(), [
'eggs' => $item->eggs->keyBy('id')->toArray(),
]);
})->keyBy('id'),
'eggs' => $eggs->keyBy('id'),
]);
return $this->view->make('admin.servers.new', [
'locations' => Location::all(),
'nests' => $nests,
'eggs' => $eggs,
]);
}

View File

@ -2,15 +2,15 @@
namespace App\Http\Controllers\Admin\Servers;
use App\Models\Egg;
use App\Repositories\Eloquent\EggRepository;
use Illuminate\View\View;
use Illuminate\Http\Request;
use App\Models\Nest;
use App\Models\Server;
use App\Exceptions\DisplayException;
use App\Http\Controllers\Controller;
use App\Services\Servers\EnvironmentService;
use Illuminate\Contracts\View\Factory as ViewFactory;
use App\Repositories\Eloquent\NestRepository;
use App\Repositories\Eloquent\NodeRepository;
use App\Repositories\Eloquent\MountRepository;
use App\Repositories\Eloquent\ServerRepository;
@ -29,7 +29,7 @@ class ServerViewController extends Controller
private DatabaseHostRepository $databaseHostRepository,
private LocationRepository $locationRepository,
private MountRepository $mountRepository,
private NestRepository $nestRepository,
private EggRepository $eggRepository,
private NodeRepository $nodeRepository,
private ServerRepository $repository,
private EnvironmentService $environmentService,
@ -74,20 +74,16 @@ class ServerViewController extends Controller
*/
public function startup(Request $request, Server $server): View
{
$nests = $this->nestRepository->getWithEggs();
$variables = $this->environmentService->handle($server);
$eggs = Egg::all()->keyBy('id');
$this->plainInject([
'server' => $server,
'server_variables' => $variables,
'nests' => $nests->map(function (Nest $item) {
return array_merge($item->toArray(), [
'eggs' => $item->eggs->keyBy('id')->toArray(),
]);
})->keyBy('id'),
'eggs' => $eggs,
]);
return $this->view->make('admin.servers.view.startup', compact('server', 'nests'));
return $this->view->make('admin.servers.view.startup', compact('server', 'eggs'));
}
/**

View File

@ -24,7 +24,6 @@ use App\Services\Servers\BuildModificationService;
use App\Services\Databases\DatabasePasswordService;
use App\Services\Servers\DetailsModificationService;
use App\Services\Servers\StartupModificationService;
use App\Contracts\Repository\NestRepositoryInterface;
use App\Repositories\Eloquent\DatabaseHostRepository;
use App\Services\Databases\DatabaseManagementService;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
@ -54,7 +53,6 @@ class ServersController extends Controller
protected ReinstallServerService $reinstallService,
protected ServerRepositoryInterface $repository,
protected MountRepository $mountRepository,
protected NestRepositoryInterface $nestRepository,
protected ServerConfigurationStructureService $serverConfigurationStructureService,
protected StartupModificationService $startupModificationService,
protected SuspensionService $suspensionService

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers\Api\Application\Eggs;
use App\Http\Controllers\Api\Application\ApplicationApiController;
use App\Http\Requests\Api\Application\Eggs\GetEggRequest;
use App\Http\Requests\Api\Application\Eggs\GetEggsRequest;
use App\Models\Egg;
use App\Transformers\Api\Application\EggTransformer;
class EggController extends ApplicationApiController
{
/**
* Return all eggs
*/
public function index(GetEggsRequest $request): array
{
return $this->fractal->collection(Egg::all())
->transformWith($this->getTransformer(EggTransformer::class))
->toArray();
}
/**
* Return a single egg that exists
*/
public function view(GetEggRequest $request, Egg $egg): array
{
return $this->fractal->item($egg)
->transformWith($this->getTransformer(EggTransformer::class))
->toArray();
}
}

View File

@ -1,33 +0,0 @@
<?php
namespace App\Http\Controllers\Api\Application\Nests;
use App\Models\Egg;
use App\Models\Nest;
use App\Transformers\Api\Application\EggTransformer;
use App\Http\Requests\Api\Application\Nests\Eggs\GetEggRequest;
use App\Http\Requests\Api\Application\Nests\Eggs\GetEggsRequest;
use App\Http\Controllers\Api\Application\ApplicationApiController;
class EggController extends ApplicationApiController
{
/**
* Return all eggs that exist for a given nest.
*/
public function index(GetEggsRequest $request, Nest $nest): array
{
return $this->fractal->collection($nest->eggs)
->transformWith($this->getTransformer(EggTransformer::class))
->toArray();
}
/**
* Return a single egg that exists on the specified nest.
*/
public function view(GetEggRequest $request, Nest $nest, Egg $egg): array
{
return $this->fractal->item($egg)
->transformWith($this->getTransformer(EggTransformer::class))
->toArray();
}
}

View File

@ -1,42 +0,0 @@
<?php
namespace App\Http\Controllers\Api\Application\Nests;
use App\Models\Nest;
use App\Contracts\Repository\NestRepositoryInterface;
use App\Transformers\Api\Application\NestTransformer;
use App\Http\Requests\Api\Application\Nests\GetNestsRequest;
use App\Http\Controllers\Api\Application\ApplicationApiController;
class NestController extends ApplicationApiController
{
/**
* NestController constructor.
*/
public function __construct(private NestRepositoryInterface $repository)
{
parent::__construct();
}
/**
* Return all Nests that exist on the Panel.
*/
public function index(GetNestsRequest $request): array
{
$nests = $this->repository->paginated($request->query('per_page') ?? 50);
return $this->fractal->collection($nests)
->transformWith($this->getTransformer(NestTransformer::class))
->toArray();
}
/**
* Return information about a single Nest model.
*/
public function view(GetNestsRequest $request, Nest $nest): array
{
return $this->fractal->item($nest)
->transformWith($this->getTransformer(NestTransformer::class))
->toArray();
}
}

View File

@ -54,7 +54,6 @@ class StartupController extends ClientApiController
{
/** @var \App\Models\EggVariable $variable */
$variable = $server->variables()->where('env_variable', $request->input('key'))->first();
$original = $variable->server_value;
if (is_null($variable) || !$variable->user_viewable) {
throw new BadRequestHttpException('The environment variable you are trying to edit does not exist.');
@ -62,6 +61,8 @@ class StartupController extends ClientApiController
throw new BadRequestHttpException('The environment variable you are trying to edit is read-only.');
}
$original = $variable->server_value;
// Revalidate the variable value using the egg variable specific validation rules for it.
$this->validate($request, ['value' => $variable->rules]);

View File

@ -22,10 +22,6 @@ class EggFormRequest extends AdminFormRequest
'config_files' => 'required_without:config_from|nullable|json',
];
if ($this->method() === 'POST') {
$rules['nest_id'] = 'required|numeric|exists:nests,id';
}
return $rules;
}

View File

@ -8,14 +8,8 @@ class EggImportFormRequest extends AdminFormRequest
{
public function rules(): array
{
$rules = [
return [
'import_file' => 'bail|required|file|max:1000|mimetypes:application/json,text/plain',
];
if ($this->method() !== 'PUT') {
$rules['import_to_nest'] = 'bail|required|integer|exists:nests,id';
}
return $rules;
}
}

View File

@ -1,16 +0,0 @@
<?php
namespace App\Http\Requests\Admin\Nest;
use App\Http\Requests\Admin\AdminFormRequest;
class StoreNestFormRequest extends AdminFormRequest
{
public function rules(): array
{
return [
'name' => 'required|string|min:1|max:191',
'description' => 'string|nullable',
];
}
}

View File

@ -1,9 +1,9 @@
<?php
namespace App\Http\Requests\Api\Application\Nests\Eggs;
namespace App\Http\Requests\Api\Application\Eggs;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Services\Acl\Api\AdminAcl;
class GetEggRequest extends ApplicationApiRequest
{

View File

@ -1,9 +1,9 @@
<?php
namespace App\Http\Requests\Api\Application\Nests\Eggs;
namespace App\Http\Requests\Api\Application\Eggs;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Services\Acl\Api\AdminAcl;
class GetEggsRequest extends ApplicationApiRequest
{

View File

@ -1,13 +0,0 @@
<?php
namespace App\Http\Requests\Api\Application\Nests;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
class GetNestsRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_NESTS;
protected int $permission = AdminAcl::READ;
}

View File

@ -26,7 +26,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property int $r_allocations
* @property int $r_users
* @property int $r_locations
* @property int $r_nests
* @property int $r_eggs
* @property int $r_database_hosts
* @property int $r_server_databases
@ -48,7 +47,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @method static \Illuminate\Database\Eloquent\Builder|ApiKey whereRDatabaseHosts($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApiKey whereREggs($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApiKey whereRLocations($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApiKey whereRNests($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApiKey whereRNodes($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApiKey whereRServerDatabases($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApiKey whereRServers($value)
@ -108,7 +106,7 @@ class ApiKey extends Model
'r_' . AdminAcl::RESOURCE_SERVER_DATABASES => 'int',
'r_' . AdminAcl::RESOURCE_EGGS => 'int',
'r_' . AdminAcl::RESOURCE_LOCATIONS => 'int',
'r_' . AdminAcl::RESOURCE_NESTS => 'int',
'r_' . AdminAcl::RESOURCE_EGGS => 'int',
'r_' . AdminAcl::RESOURCE_NODES => 'int',
'r_' . AdminAcl::RESOURCE_SERVERS => 'int',
];
@ -150,7 +148,7 @@ class ApiKey extends Model
'r_' . AdminAcl::RESOURCE_SERVER_DATABASES => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_EGGS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_LOCATIONS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_NESTS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_EGGS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_NODES => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_SERVERS => 'integer|min:0|max:3',
];

View File

@ -8,7 +8,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property string $uuid
* @property int $nest_id
* @property string $author
* @property string $name
* @property string|null $description
@ -40,7 +39,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property string|null $inherit_config_stop
* @property string $inherit_file_denylist
* @property array|null $inherit_features
* @property \App\Models\Nest $nest
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\Server[] $servers
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\EggVariable[] $variables
* @property \App\Models\Egg|null $scriptFrom
@ -103,7 +101,6 @@ class Egg extends Model
* Cast values to correct type.
*/
protected $casts = [
'nest_id' => 'integer',
'config_from' => 'integer',
'script_is_privileged' => 'boolean',
'force_outgoing_ip' => 'boolean',
@ -114,7 +111,6 @@ class Egg extends Model
];
public static array $validationRules = [
'nest_id' => 'required|bail|numeric|exists:nests,id',
'uuid' => 'required|string|size:36',
'name' => 'required|string|max:191',
'description' => 'string|nullable',
@ -257,14 +253,6 @@ class Egg extends Model
return $this->configFrom->file_denylist;
}
/**
* Gets nest associated with an egg.
*/
public function nest(): BelongsTo
{
return $this->belongsTo(Nest::class);
}
/**
* Gets all servers associated with this egg.
*/

View File

@ -1,60 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property int $id
* @property string $uuid
* @property string $author
* @property string $name
* @property string|null $description
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\Server[] $servers
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\Egg[] $eggs
*/
class Nest extends Model
{
/**
* The resource name for this model when it is transformed into an
* API representation using fractal.
*/
public const RESOURCE_NAME = 'nest';
/**
* The table associated with the model.
*/
protected $table = 'nests';
/**
* Fields that are mass assignable.
*/
protected $fillable = [
'name',
'description',
];
public static array $validationRules = [
'author' => 'required|string|email',
'name' => 'required|string|max:191',
'description' => 'nullable|string',
];
/**
* Gets all eggs associated with this service.
*/
public function eggs(): HasMany
{
return $this->hasMany(Egg::class);
}
/**
* Gets all servers associated with this nest.
*/
public function servers(): HasMany
{
return $this->hasMany(Server::class);
}
}

View File

@ -33,7 +33,6 @@ use App\Exceptions\Http\Server\ServerStateConflictException;
* @property string|null $threads
* @property bool $oom_disabled
* @property int $allocation_id
* @property int $nest_id
* @property int $egg_id
* @property string $startup
* @property string $image
@ -55,7 +54,6 @@ use App\Exceptions\Http\Server\ServerStateConflictException;
* @property \App\Models\Egg|null $egg
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\Mount[] $mounts
* @property int|null $mounts_count
* @property \App\Models\Nest $nest
* @property \App\Models\Node $node
* @property \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $notifications
* @property int|null $notifications_count
@ -87,7 +85,6 @@ use App\Exceptions\Http\Server\ServerStateConflictException;
* @method static \Illuminate\Database\Eloquent\Builder|Server whereIo($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereMemory($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereNestId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereNodeId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereOomDisabled($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereOwnerId($value)
@ -159,7 +156,6 @@ class Server extends Model
'oom_disabled' => 'sometimes|boolean',
'disk' => 'required|numeric|min:0',
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
'nest_id' => 'required|exists:nests,id',
'egg_id' => 'required|exists:eggs,id',
'startup' => 'required|string',
'skip_scripts' => 'sometimes|boolean',
@ -183,7 +179,6 @@ class Server extends Model
'cpu' => 'integer',
'oom_disabled' => 'boolean',
'allocation_id' => 'integer',
'nest_id' => 'integer',
'egg_id' => 'integer',
'database_limit' => 'integer',
'allocation_limit' => 'integer',
@ -246,14 +241,6 @@ class Server extends Model
return $this->hasMany(Allocation::class, 'server_id');
}
/**
* Gets information for the nest associated with this server.
*/
public function nest(): BelongsTo
{
return $this->belongsTo(Nest::class);
}
/**
* Gets information for the egg associated with this server.
*/
@ -263,7 +250,7 @@ class Server extends Model
}
/**
* Gets information for the service variables associated with this server.
* Gets information for the egg variables associated with this server.
*/
public function variables(): HasMany
{

View File

@ -4,7 +4,6 @@ namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Repositories\Eloquent\EggRepository;
use App\Repositories\Eloquent\NestRepository;
use App\Repositories\Eloquent\NodeRepository;
use App\Repositories\Eloquent\TaskRepository;
use App\Repositories\Eloquent\UserRepository;
@ -19,7 +18,6 @@ use App\Repositories\Eloquent\SettingsRepository;
use App\Repositories\Eloquent\AllocationRepository;
use App\Contracts\Repository\EggRepositoryInterface;
use App\Repositories\Eloquent\EggVariableRepository;
use App\Contracts\Repository\NestRepositoryInterface;
use App\Contracts\Repository\NodeRepositoryInterface;
use App\Contracts\Repository\TaskRepositoryInterface;
use App\Contracts\Repository\UserRepositoryInterface;
@ -53,7 +51,6 @@ class RepositoryServiceProvider extends ServiceProvider
$this->app->bind(EggRepositoryInterface::class, EggRepository::class);
$this->app->bind(EggVariableRepositoryInterface::class, EggVariableRepository::class);
$this->app->bind(LocationRepositoryInterface::class, LocationRepository::class);
$this->app->bind(NestRepositoryInterface::class, NestRepository::class);
$this->app->bind(NodeRepositoryInterface::class, NodeRepository::class);
$this->app->bind(ScheduleRepositoryInterface::class, ScheduleRepository::class);
$this->app->bind(ServerRepositoryInterface::class, ServerRepository::class);

View File

@ -72,15 +72,4 @@ class EggRepository extends EloquentRepository implements EggRepositoryInterface
throw new RecordNotFoundException();
}
}
/**
* Confirm a copy script belongs to the same nest as the item trying to use it.
*/
public function isCopyableScript(int $copyFromId, int $service): bool
{
return $this->getBuilder()->whereNull('copy_script_from')
->where('id', '=', $copyFromId)
->where('nest_id', '=', $service)
->exists();
}
}

View File

@ -1,77 +0,0 @@
<?php
namespace App\Repositories\Eloquent;
use App\Models\Nest;
use Illuminate\Database\Eloquent\Collection;
use App\Contracts\Repository\NestRepositoryInterface;
use App\Exceptions\Repository\RecordNotFoundException;
class NestRepository extends EloquentRepository implements NestRepositoryInterface
{
/**
* Return the model backing this repository.
*/
public function model(): string
{
return Nest::class;
}
/**
* Return a nest or all nests with their associated eggs and variables.
*
* @throws \App\Exceptions\Repository\RecordNotFoundException
*/
public function getWithEggs(int $id = null): Collection|Nest
{
$instance = $this->getBuilder()->with('eggs', 'eggs.variables');
if (!is_null($id)) {
$instance = $instance->find($id, $this->getColumns());
if (!$instance) {
throw new RecordNotFoundException();
}
return $instance;
}
return $instance->get($this->getColumns());
}
/**
* Return a nest or all nests and the count of eggs and servers for that nest.
*
* @throws \App\Exceptions\Repository\RecordNotFoundException
*/
public function getWithCounts(int $id = null): Collection|Nest
{
$instance = $this->getBuilder()->withCount(['eggs', 'servers']);
if (!is_null($id)) {
$instance = $instance->find($id, $this->getColumns());
if (!$instance) {
throw new RecordNotFoundException();
}
return $instance;
}
return $instance->get($this->getColumns());
}
/**
* Return a nest along with its associated eggs and the servers relation on those eggs.
*
* @throws \App\Exceptions\Repository\RecordNotFoundException
*/
public function getWithEggServers(int $id): Nest
{
$instance = $this->getBuilder()->with('eggs.servers')->find($id, $this->getColumns());
if (!$instance) {
throw new RecordNotFoundException();
}
/* @var Nest $instance */
return $instance;
}
}

View File

@ -146,7 +146,7 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
try {
/** @var \App\Models\Server $model */
$model = $this->getBuilder()
->with('nest', 'node')
->with('egg', 'node')
->where(function (Builder $query) use ($uuid) {
$query->where('uuidShort', $uuid)->orWhere('uuid', $uuid);
})
@ -172,7 +172,7 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
public function loadAllServersForNode(int $node, int $limit): LengthAwarePaginator
{
return $this->getBuilder()
->with(['user', 'nest', 'egg'])
->with(['user', 'egg'])
->where('node_id', '=', $node)
->paginate($limit);
}

View File

@ -29,7 +29,6 @@ class AdminAcl
public const RESOURCE_ALLOCATIONS = 'allocations';
public const RESOURCE_USERS = 'users';
public const RESOURCE_LOCATIONS = 'locations';
public const RESOURCE_NESTS = 'nests';
public const RESOURCE_EGGS = 'eggs';
public const RESOURCE_DATABASE_HOSTS = 'database_hosts';
public const RESOURCE_SERVER_DATABASES = 'server_databases';

View File

@ -18,7 +18,7 @@ class EggCreationService
}
/**
* Create a new service option and assign it to the given service.
* Create a new egg.
*
* @throws \App\Exceptions\Model\DataValidationException
* @throws \App\Exceptions\Service\Egg\NoParentConfigurationFoundException
@ -28,12 +28,11 @@ class EggCreationService
$data['config_from'] = array_get($data, 'config_from');
if (!is_null($data['config_from'])) {
$results = $this->repository->findCountWhere([
['nest_id', '=', array_get($data, 'nest_id')],
['id', '=', array_get($data, 'config_from')],
]);
if ($results !== 1) {
throw new NoParentConfigurationFoundException(trans('exceptions.nest.egg.must_be_child'));
throw new NoParentConfigurationFoundException(trans('exceptions.egg.invalid_copy_id'));
}
}

View File

@ -28,12 +28,12 @@ class EggDeletionService
{
$servers = $this->serverRepository->findCountWhere([['egg_id', '=', $egg]]);
if ($servers > 0) {
throw new HasActiveServersException(trans('exceptions.nest.egg.delete_has_servers'));
throw new HasActiveServersException(trans('exceptions.egg.delete_has_servers'));
}
$children = $this->repository->findCountWhere([['config_from', '=', $egg]]);
if ($children > 0) {
throw new HasChildrenException(trans('exceptions.nest.egg.has_children'));
throw new HasChildrenException(trans('exceptions.egg.has_children'));
}
return $this->repository->delete($egg);

View File

@ -16,7 +16,7 @@ class EggUpdateService
}
/**
* Update a service option.
* Update an egg.
*
* @throws \App\Exceptions\Model\DataValidationException
* @throws \App\Exceptions\Repository\RecordNotFoundException
@ -26,12 +26,11 @@ class EggUpdateService
{
if (!is_null(array_get($data, 'config_from'))) {
$results = $this->repository->findCountWhere([
['nest_id', '=', $egg->nest_id],
['id', '=', array_get($data, 'config_from')],
]);
if ($results !== 1) {
throw new NoParentConfigurationFoundException(trans('exceptions.nest.egg.must_be_child'));
throw new NoParentConfigurationFoundException(trans('exceptions.egg.invalid_copy_id'));
}
}

View File

@ -4,7 +4,6 @@ namespace App\Services\Eggs\Scripts;
use App\Models\Egg;
use App\Contracts\Repository\EggRepositoryInterface;
use App\Exceptions\Service\Egg\InvalidCopyFromException;
class InstallScriptService
{
@ -20,16 +19,9 @@ class InstallScriptService
*
* @throws \App\Exceptions\Model\DataValidationException
* @throws \App\Exceptions\Repository\RecordNotFoundException
* @throws \App\Exceptions\Service\Egg\InvalidCopyFromException
*/
public function handle(Egg $egg, array $data): void
{
if (!is_null(array_get($data, 'copy_script_from'))) {
if (!$this->repository->isCopyableScript(array_get($data, 'copy_script_from'), $egg->nest_id)) {
throw new InvalidCopyFromException(trans('exceptions.nest.egg.invalid_copy_id'));
}
}
$this->repository->withoutFreshModel()->update($egg->id, [
'script_install' => array_get($data, 'script_install'),
'script_is_privileged' => array_get($data, 'script_is_privileged', 1),

View File

@ -5,7 +5,6 @@ namespace App\Services\Eggs\Sharing;
use Ramsey\Uuid\Uuid;
use Illuminate\Support\Arr;
use App\Models\Egg;
use App\Models\Nest;
use Illuminate\Http\UploadedFile;
use App\Models\EggVariable;
use Illuminate\Database\ConnectionInterface;
@ -22,17 +21,13 @@ class EggImporterService
*
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
*/
public function handle(UploadedFile $file, int $nest): Egg
public function handle(UploadedFile $file): Egg
{
$parsed = $this->parser->handle($file);
/** @var \App\Models\Nest $nest */
$nest = Nest::query()->with('eggs', 'eggs.variables')->findOrFail($nest);
return $this->connection->transaction(function () use ($nest, $parsed) {
return $this->connection->transaction(function () use ($parsed) {
$egg = (new Egg())->forceFill([
'uuid' => Uuid::uuid4()->toString(),
'nest_id' => $nest->id,
'author' => Arr::get($parsed, 'author'),
'copy_script_from' => null,
]);

View File

@ -1,33 +0,0 @@
<?php
namespace App\Services\Nests;
use Ramsey\Uuid\Uuid;
use App\Models\Nest;
use App\Contracts\Repository\NestRepositoryInterface;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
class NestCreationService
{
/**
* NestCreationService constructor.
*/
public function __construct(private ConfigRepository $config, private NestRepositoryInterface $repository)
{
}
/**
* Create a new nest on the system.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function handle(array $data, string $author = null): Nest
{
return $this->repository->create([
'uuid' => Uuid::uuid4()->toString(),
'author' => $author ?? $this->config->get('panel.service.author'),
'name' => array_get($data, 'name'),
'description' => array_get($data, 'description'),
], true, true);
}
}

View File

@ -1,34 +0,0 @@
<?php
namespace App\Services\Nests;
use App\Contracts\Repository\NestRepositoryInterface;
use App\Exceptions\Service\HasActiveServersException;
use App\Contracts\Repository\ServerRepositoryInterface;
class NestDeletionService
{
/**
* NestDeletionService constructor.
*/
public function __construct(
protected ServerRepositoryInterface $serverRepository,
protected NestRepositoryInterface $repository
) {
}
/**
* Delete a nest from the system only if there are no servers attached to it.
*
* @throws \App\Exceptions\Service\HasActiveServersException
*/
public function handle(int $nest): int
{
$count = $this->serverRepository->findCountWhere([['nest_id', '=', $nest]]);
if ($count > 0) {
throw new HasActiveServersException(trans('exceptions.nest.delete_has_servers'));
}
return $this->repository->delete($nest);
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace App\Services\Nests;
use App\Contracts\Repository\NestRepositoryInterface;
class NestUpdateService
{
/**
* NestUpdateService constructor.
*/
public function __construct(protected NestRepositoryInterface $repository)
{
}
/**
* Update a nest and prevent changing the author once it is set.
*
* @throws \App\Exceptions\Model\DataValidationException
* @throws \App\Exceptions\Repository\RecordNotFoundException
*/
public function handle(int $nest, array $data): void
{
if (!is_null(array_get($data, 'author'))) {
unset($data['author']);
}
$this->repository->withoutFreshModel()->update($nest, $data);
}
}

View File

@ -4,7 +4,6 @@ namespace App\Services\Servers;
use Ramsey\Uuid\Uuid;
use Illuminate\Support\Arr;
use App\Models\Egg;
use App\Models\User;
use Webmozart\Assert\Assert;
use App\Models\Server;
@ -67,12 +66,6 @@ class ServerCreationService
$data['node_id'] = Allocation::query()->findOrFail($data['allocation_id'])->node_id;
}
if (empty($data['nest_id'])) {
Assert::false(empty($data['egg_id']), 'Expected a non-empty egg_id in server creation data.');
$data['nest_id'] = Egg::query()->findOrFail($data['egg_id'])->nest_id;
}
$eggVariableData = $this->validatorService
->setUserLevel(User::USER_LEVEL_ADMIN)
->handle(Arr::get($data, 'egg_id'), Arr::get($data, 'environment', []));
@ -155,7 +148,6 @@ class ServerCreationService
'threads' => Arr::get($data, 'threads'),
'oom_disabled' => Arr::get($data, 'oom_disabled') ?? true,
'allocation_id' => Arr::get($data, 'allocation_id'),
'nest_id' => Arr::get($data, 'nest_id'),
'egg_id' => Arr::get($data, 'egg_id'),
'startup' => Arr::get($data, 'startup'),
'image' => Arr::get($data, 'image'),

View File

@ -75,7 +75,6 @@ class StartupModificationService
$server = $server->forceFill([
'egg_id' => $egg->id,
'nest_id' => $egg->nest_id,
]);
}

View File

@ -21,7 +21,7 @@ class VariableValidatorService
}
/**
* Validate all of the passed data against the given service option variables.
* Validate all of the passed data against the given egg variables.
*
* @throws \Illuminate\Validation\ValidationException
*/

View File

@ -23,7 +23,7 @@ trait ValidatesValidationRules
} catch (\BadMethodCallException $exception) {
$matches = [];
if (preg_match('/Method \[(.+)\] does not exist\./', $exception->getMessage(), $matches)) {
throw new BadValidationRuleException(trans('exceptions.nest.variables.bad_validation_rule', ['rule' => Str::snake(str_replace('validate', '', array_get($matches, 1, 'unknownRule')))]), $exception);
throw new BadValidationRuleException(trans('exceptions.variables.bad_validation_rule', ['rule' => Str::snake(str_replace('validate', '', array_get($matches, 1, 'unknownRule')))]), $exception);
}
throw $exception;

View File

@ -4,7 +4,6 @@ namespace App\Transformers\Api\Application;
use Illuminate\Support\Arr;
use App\Models\Egg;
use App\Models\Nest;
use App\Models\Server;
use League\Fractal\Resource\Item;
use App\Models\EggVariable;
@ -18,7 +17,6 @@ class EggTransformer extends BaseTransformer
* Relationships that can be loaded onto this transformation.
*/
protected array $availableIncludes = [
'nest',
'servers',
'config',
'script',
@ -42,15 +40,11 @@ class EggTransformer extends BaseTransformer
public function transform(Egg $model): array
{
$files = json_decode($model->config_files, true, 512, JSON_THROW_ON_ERROR);
if (empty($files)) {
$files = new \stdClass();
}
return [
'id' => $model->id,
'uuid' => $model->uuid,
'name' => $model->name,
'nest' => $model->nest_id,
'author' => $model->author,
'description' => $model->description,
// "docker_image" is deprecated, but left here to avoid breaking too many things at once
@ -79,22 +73,6 @@ class EggTransformer extends BaseTransformer
];
}
/**
* Include the Nest relationship for the given Egg in the transformation.
*
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
*/
public function includeNest(Egg $model): Item|NullResource
{
if (!$this->authorize(AdminAcl::RESOURCE_NESTS)) {
return $this->null();
}
$model->loadMissing('nest');
return $this->item($model->getRelation('nest'), $this->makeTransformer(NestTransformer::class), Nest::RESOURCE_NAME);
}
/**
* Include the Servers relationship for the given Egg in the transformation.
*

View File

@ -1,74 +0,0 @@
<?php
namespace App\Transformers\Api\Application;
use App\Models\Egg;
use App\Models\Nest;
use App\Models\Server;
use League\Fractal\Resource\Collection;
use League\Fractal\Resource\NullResource;
use App\Services\Acl\Api\AdminAcl;
class NestTransformer extends BaseTransformer
{
/**
* Relationships that can be loaded onto this transformation.
*/
protected array $availableIncludes = [
'eggs', 'servers',
];
/**
* Return the resource name for the JSONAPI output.
*/
public function getResourceName(): string
{
return Nest::RESOURCE_NAME;
}
/**
* Transform a Nest model into a representation that can be consumed by the
* application API.
*/
public function transform(Nest $model): array
{
$response = $model->toArray();
$response[$model->getUpdatedAtColumn()] = $this->formatTimestamp($model->updated_at);
$response[$model->getCreatedAtColumn()] = $this->formatTimestamp($model->created_at);
return $response;
}
/**
* Include the Eggs relationship on the given Nest model transformation.
*
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
*/
public function includeEggs(Nest $model): Collection|NullResource
{
if (!$this->authorize(AdminAcl::RESOURCE_EGGS)) {
return $this->null();
}
$model->loadMissing('eggs');
return $this->collection($model->getRelation('eggs'), $this->makeTransformer(EggTransformer::class), Egg::RESOURCE_NAME);
}
/**
* Include the servers relationship on the given Nest model.
*
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
*/
public function includeServers(Nest $model): Collection|NullResource
{
if (!$this->authorize(AdminAcl::RESOURCE_SERVERS)) {
return $this->null();
}
$model->loadMissing('servers');
return $this->collection($model->getRelation('servers'), $this->makeTransformer(ServerTransformer::class), Server::RESOURCE_NAME);
}
}

View File

@ -20,7 +20,6 @@ class ServerTransformer extends BaseTransformer
'allocations',
'user',
'subusers',
'nest',
'egg',
'variables',
'location',
@ -77,7 +76,6 @@ class ServerTransformer extends BaseTransformer
'user' => $server->owner_id,
'node' => $server->node_id,
'allocation' => $server->allocation_id,
'nest' => $server->nest_id,
'egg' => $server->egg_id,
'container' => [
'startup_command' => $server->startup,
@ -139,22 +137,6 @@ class ServerTransformer extends BaseTransformer
return $this->item($server->getRelation('user'), $this->makeTransformer(UserTransformer::class), 'user');
}
/**
* Return a generic array with nest information for this server.
*
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
*/
public function includeNest(Server $server): Item|NullResource
{
if (!$this->authorize(AdminAcl::RESOURCE_NESTS)) {
return $this->null();
}
$server->loadMissing('nest');
return $this->item($server->getRelation('nest'), $this->makeTransformer(NestTransformer::class), 'nest');
}
/**
* Return a generic array with egg information for this server.
*

View File

@ -1,30 +0,0 @@
<?php
namespace Database\Factories;
use Ramsey\Uuid\Uuid;
use App\Models\Nest;
use Illuminate\Database\Eloquent\Factories\Factory;
class NestFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Nest::class;
/**
* Define the model's default state.
*/
public function definition(): array
{
return [
'uuid' => Uuid::uuid4()->toString(),
'author' => 'testauthor@example.com',
'name' => $this->faker->word,
'description' => null,
];
}
}

View File

@ -11,7 +11,6 @@ class DatabaseSeeder extends Seeder
*/
public function run()
{
$this->call(NestSeeder::class);
$this->call(EggSeeder::class);
}
}

View File

@ -3,7 +3,6 @@
namespace Database\Seeders;
use App\Models\Egg;
use App\Models\Nest;
use Exception;
use Illuminate\Database\Seeder;
use Illuminate\Http\UploadedFile;
@ -19,7 +18,7 @@ class EggSeeder extends Seeder
/**
* @var string[]
*/
public static array $import = [
public static array $imports = [
'Minecraft',
'Source Engine',
'Voice Servers',
@ -42,22 +41,20 @@ class EggSeeder extends Seeder
*/
public function run()
{
foreach (static::$import as $nest) {
foreach (static::$imports as $import) {
/* @noinspection PhpParamsInspection */
$this->parseEggFiles(
Nest::query()->where('author', 'panel@example.com')->where('name', $nest)->firstOrFail()
);
$this->parseEggFiles($import);
}
}
/**
* Loop through the list of egg files and import them.
*/
protected function parseEggFiles(Nest $nest)
protected function parseEggFiles($name)
{
$files = new \DirectoryIterator(database_path('Seeders/eggs/' . kebab_case($nest->name)));
$files = new \DirectoryIterator(database_path('Seeders/eggs/' . kebab_case($name)));
$this->command->alert('Updating Eggs for Nest: ' . $nest->name);
$this->command->alert('Updating Eggs for: ' . $name);
/** @var \DirectoryIterator $file */
foreach ($files as $file) {
if (!$file->isFile() || !$file->isReadable()) {
@ -72,7 +69,7 @@ class EggSeeder extends Seeder
$file = new UploadedFile($file->getPathname(), $file->getFilename(), 'application/json');
$egg = $nest->eggs()
$egg = Egg::query()
->where('author', $decoded['author'])
->where('name', $decoded['name'])
->first();
@ -81,7 +78,7 @@ class EggSeeder extends Seeder
$this->updateImporterService->handle($egg, $file);
$this->command->info('Updated ' . $decoded['name']);
} else {
$this->importerService->handle($file, $nest->id);
$this->importerService->handle($file);
$this->command->comment('Created ' . $decoded['name']);
}
}

View File

@ -1,108 +0,0 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Services\Nests\NestCreationService;
use App\Contracts\Repository\NestRepositoryInterface;
class NestSeeder extends Seeder
{
/**
* @var \App\Services\Nests\NestCreationService
*/
private $creationService;
/**
* @var \App\Contracts\Repository\NestRepositoryInterface
*/
private $repository;
/**
* NestSeeder constructor.
*/
public function __construct(
NestCreationService $creationService,
NestRepositoryInterface $repository
) {
$this->creationService = $creationService;
$this->repository = $repository;
}
/**
* Run the seeder to add missing nests to the Panel.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function run()
{
$items = $this->repository->findWhere([
'author' => 'panel@example.com',
])->keyBy('name')->toArray();
$this->createMinecraftNest(array_get($items, 'Minecraft'));
$this->createSourceEngineNest(array_get($items, 'Source Engine'));
$this->createVoiceServersNest(array_get($items, 'Voice Servers'));
$this->createRustNest(array_get($items, 'Rust'));
}
/**
* Create the Minecraft nest to be used later on.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
private function createMinecraftNest(array $nest = null)
{
if (is_null($nest)) {
$this->creationService->handle([
'name' => 'Minecraft',
'description' => 'Minecraft - the classic game from Mojang. With support for Vanilla MC, Spigot, and many others!',
], 'panel@example.com');
}
}
/**
* Create the Source Engine Games nest to be used later on.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
private function createSourceEngineNest(array $nest = null)
{
if (is_null($nest)) {
$this->creationService->handle([
'name' => 'Source Engine',
'description' => 'Includes support for most Source Dedicated Server games.',
], 'panel@example.com');
}
}
/**
* Create the Voice Servers nest to be used later on.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
private function createVoiceServersNest(array $nest = null)
{
if (is_null($nest)) {
$this->creationService->handle([
'name' => 'Voice Servers',
'description' => 'Voice servers such as Mumble and Teamspeak 3.',
], 'panel@example.com');
}
}
/**
* Create the Rust nest to be used later on.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
private function createRustNest(array $nest = null)
{
if (is_null($nest)) {
$this->creationService->handle([
'name' => 'Rust',
'description' => 'Rust - A game where you must fight to survive.',
], 'panel@example.com');
}
}
}

View File

@ -0,0 +1,60 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('eggs', function (Blueprint $table) {
$table->dropForeign('service_options_nest_id_foreign');
$table->dropColumn('nest_id');
});
Schema::table('servers', function (Blueprint $table) {
$table->dropForeign('servers_nest_id_foreign');
$table->dropColumn('nest_id');
});
Schema::drop('nests');
Schema::table('api_keys', function (Blueprint $table) {
$table->dropColumn('r_nests');
});
}
public function down(): void
{
Schema::table('api_keys', function (Blueprint $table) {
$table->unsignedTinyInteger('r_nests')->default(0);
});
Schema::create('nests', function (Blueprint $table) {
$table->increments('id');
$table->char('uuid', 36)->unique();
$table->string('author');
$table->string('name');
$table->text('description')->nullable();
$table->timestamps();
});
Schema::table('eggs', function (Blueprint $table) {
$table->mediumInteger('nest_id')->unsigned();
$table->foreign(['nest_id'], 'service_options_nest_id_foreign');
});
Schema::table('servers', function (Blueprint $table) {
$table->mediumInteger('nest_id')->unsigned();
$table->foreign(['nest_id'], 'servers_nest_id_foreign');
});
if (class_exists('Database\Seeders\NestSeeder')) {
Artisan::call('db:seed', [
'--class' => 'NestSeeder',
]);
}
}
};

File diff suppressed because one or more lines are too long

View File

@ -1,11 +1,7 @@
$(document).ready(function() {
$('#pNestId').select2({
placeholder: 'Select a Nest',
}).change();
$('#pEggId').select2({
placeholder: 'Select a Nest Egg',
});
placeholder: 'Select an Egg',
}).change();
$('#pPackId').select2({
placeholder: 'Select a Service Pack',
@ -48,20 +44,8 @@ $('#pNodeId').on('change', function () {
});
});
$('#pNestId').on('change', function (event) {
$('#pEggId').html('').select2({
data: $.map(_.get(Panel.nests, $(this).val() + '.eggs', []), function (item) {
return {
id: item.id,
text: item.name,
};
}),
}).change();
});
$('#pEggId').on('change', function (event) {
let parentChain = _.get(Panel.nests, $('#pNestId').val(), null);
let objectChain = _.get(parentChain, 'eggs.' + $(this).val(), null);
let objectChain = _.get(Panel.eggs, $('#pEggId').val(), null);
const images = _.get(objectChain, 'docker_images', {})
$('#pDefaultContainer').html('');
@ -73,11 +57,7 @@ $('#pEggId').on('change', function (event) {
$('#pDefaultContainer').append(opt);
}
if (!_.get(objectChain, 'startup', false)) {
$('#pStartup').val(_.get(parentChain, 'startup', 'ERROR: Startup Not Defined!'));
} else {
$('#pStartup').val(_.get(objectChain, 'startup'));
}
$('#pPackId').html('').select2({
data: [{ id: 0, text: 'No Service Pack' }].concat(
@ -110,7 +90,7 @@ $('#pEggId').on('change', function (event) {
// If you receive a warning on this line, it should be fine to ignore. this function is
// defined in "resources/views/admin/servers/new.blade.php" near the bottom of the file.
serviceVariablesUpdated($('#pEggId').val(), variableIds);
eggVariablesUpdated($('#pEggId').val(), variableIds);
});
$('#pAllocation').on('change', function () {

View File

@ -0,0 +1,19 @@
<?php
return [
'notices' => [
'imported' => 'Successfully imported this Egg and its associated variables.',
'updated_via_import' => 'This Egg has been updated using the file provided.',
'deleted' => 'Successfully deleted the requested egg from the Panel.',
'updated' => 'Egg configuration has been updated successfully.',
'script_updated' => 'Egg install script has been updated and will run whenever servers are installed.',
'egg_created' => 'A new egg was laid successfully. You will need to restart any running daemons to apply this new egg.',
],
'variables' => [
'notices' => [
'variable_deleted' => 'The variable ":variable" has been deleted and will no longer be available to servers once rebuilt.',
'variable_updated' => 'The variable ":variable" has been updated. You will need to rebuild any servers using this variable in order to apply changes.',
'variable_created' => 'New variable has successfully been created and assigned to this egg.',
],
],
];

View File

@ -1,26 +0,0 @@
<?php
return [
'notices' => [
'created' => 'A new nest, :name, has been successfully created.',
'deleted' => 'Successfully deleted the requested nest from the Panel.',
'updated' => 'Successfully updated the nest configuration options.',
],
'eggs' => [
'notices' => [
'imported' => 'Successfully imported this Egg and its associated variables.',
'updated_via_import' => 'This Egg has been updated using the file provided.',
'deleted' => 'Successfully deleted the requested egg from the Panel.',
'updated' => 'Egg configuration has been updated successfully.',
'script_updated' => 'Egg install script has been updated and will run whenever servers are installed.',
'egg_created' => 'A new egg was laid successfully. You will need to restart any running daemons to apply this new egg.',
],
],
'variables' => [
'notices' => [
'variable_deleted' => 'The variable ":variable" has been deleted and will no longer be available to servers once rebuilt.',
'variable_updated' => 'The variable ":variable" has been updated. You will need to rebuild any servers using this variable in order to apply changes.',
'variable_created' => 'New variable has successfully been created and assigned to this egg.',
],
],
];

View File

@ -9,7 +9,7 @@ return [
'default_allocation_not_found' => 'The requested default allocation was not found in this server\'s allocations.',
],
'alerts' => [
'startup_changed' => 'The startup configuration for this server has been updated. If this server\'s nest or egg was changed a reinstall will be occurring now.',
'startup_changed' => 'The startup configuration for this server has been updated. If this server\'s egg was changed a reinstall will be occurring now.',
'server_deleted' => 'Server has successfully been deleted from the system.',
'server_created' => 'Server was successfully created on the panel. Please allow the daemon a few minutes to completely install this server.',
'build_updated' => 'The build details for this server have been updated. Some changes may require a restart to take effect.',

View File

@ -13,12 +13,9 @@ return [
'cidr_out_of_range' => 'CIDR notation only allows masks between /25 and /32.',
'port_out_of_range' => 'Ports in an allocation must be greater than 1024 and less than or equal to 65535.',
],
'nest' => [
'delete_has_servers' => 'A Nest with active servers attached to it cannot be deleted from the Panel.',
'egg' => [
'delete_has_servers' => 'An Egg with active servers attached to it cannot be deleted from the Panel.',
'invalid_copy_id' => 'The Egg selected for copying a script from either does not exist, or is copying a script itself.',
'must_be_child' => 'The "Copy Settings From" directive for this Egg must be a child option for the selected Nest.',
'has_children' => 'This Egg is a parent to one or more other Eggs. Please delete those Eggs before deleting this Egg.',
],
'variables' => [
@ -31,7 +28,6 @@ return [
'file_error' => 'The JSON file provided was not valid.',
'invalid_json_provided' => 'The JSON file provided is not in a format that can be recognized.',
],
],
'subusers' => [
'editing_self' => 'Editing your own subuser account is not permitted.',
'user_is_owner' => 'You cannot add the server owner as a subuser for this server.',

View File

@ -1,33 +1,33 @@
@extends('layouts.admin')
@section('title')
Nests
Eggs
@endsection
@section('content-header')
<h1>Nests<small>All nests currently available on this system.</small></h1>
<h1>Eggs</h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li class="active">Nests</li>
<li class="active">Eggs</li>
</ol>
@endsection
@section('content')
<div class="row">
<div class="col-xs-12">
<div class="alert alert-danger">
Eggs are a powerful feature of Panel that allow for extreme flexibility and configuration. Please note that while powerful, modifying an egg wrongly can very easily brick your servers and cause more problems. Please avoid editing our default eggs those provided by <code>panel@example.com</code> unless you are absolutely sure of what you are doing.
<div class="alert alert-warning">
Eggs allow extreme flexibility and configuration. Please note that modifying an egg can cause issues with your server may brick it.
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Configured Nests</h3>
<h3 class="box-title">Eggs</h3>
<div class="box-tools">
<a href="#" class="btn btn-sm btn-success" data-toggle="modal" data-target="#importServiceOptionModal" role="button"><i class="fa fa-upload"></i> Import Egg</a>
<a href="{{ route('admin.nests.new') }}" class="btn btn-primary btn-sm">Create New</a>
<a href="{{ route('admin.eggs.new') }}" class="btn btn-primary btn-sm">Create New</a>
</div>
</div>
<div class="box-body table-responsive no-padding">
@ -36,16 +36,18 @@
<th>ID</th>
<th>Name</th>
<th>Description</th>
<th class="text-center">Eggs</th>
<th class="text-center">Servers</th>
<th class="text-center"></th>
</tr>
@foreach($nests as $nest)
@foreach($eggs as $egg)
<tr>
<td class="middle"><code>{{ $nest->id }}</code></td>
<td class="middle"><a href="{{ route('admin.nests.view', $nest->id) }}" data-toggle="tooltip" data-placement="right" title="{{ $nest->author }}">{{ $nest->name }}</a></td>
<td class="col-xs-6 middle">{{ $nest->description }}</td>
<td class="text-center middle">{{ $nest->eggs_count }}</td>
<td class="text-center middle">{{ $nest->servers_count }}</td>
<td class="align-middle"><code>{{ $egg->id }}</code></td>
<td class="align-middle"><a href="{{ route('admin.eggs.view', $egg->id) }}" data-toggle="tooltip" data-placement="right" title="{{ $egg->author }}">{{ $egg->name }}</a></td>
<td class="col-xs-8 align-middle">{{ $egg->description }}</td>
<td class="text-center align-middle"><code>{{ $egg->servers->count() }}</code></td>
<td class="align-middle">
<a href="{{ route('admin.eggs.export', ['egg' => $egg->id]) }}"><i class="fa fa-download"></i></a>
</td>
</tr>
@endforeach
</table>
@ -53,6 +55,7 @@
</div>
</div>
</div>
<div class="modal fade" tabindex="-1" role="dialog" id="importServiceOptionModal">
<div class="modal-dialog" role="document">
<div class="modal-content">
@ -60,7 +63,7 @@
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Import an Egg</h4>
</div>
<form action="{{ route('admin.nests.egg.import') }}" enctype="multipart/form-data" method="POST">
<form action="{{ route('admin.eggs.import') }}" enctype="multipart/form-data" method="POST">
<div class="modal-body">
<div class="form-group">
<label class="control-label" for="pImportFile">Egg File <span class="field-required"></span></label>
@ -69,17 +72,6 @@
<p class="small text-muted">Select the <code>.json</code> file for the new egg that you wish to import.</p>
</div>
</div>
<div class="form-group">
<label class="control-label" for="pImportToNest">Associated Nest <span class="field-required"></span></label>
<div>
<select id="pImportToNest" name="import_to_nest">
@foreach($nests as $nest)
<option value="{{ $nest->id }}">{{ $nest->name }} &lt;{{ $nest->author }}&gt;</option>
@endforeach
</select>
<p class="small text-muted">Select the nest that this egg will be associated with from the dropdown. If you wish to associate it with a new nest you will need to create that nest before continuing.</p>
</div>
</div>
</div>
<div class="modal-footer">
{{ csrf_field() }}
@ -91,12 +83,3 @@
</div>
</div>
@endsection
@section('footer-scripts')
@parent
<script>
$(document).ready(function() {
$('#pImportToNest').select2();
});
</script>
@endsection

View File

@ -1,20 +1,20 @@
@extends('layouts.admin')
@section('title')
Nests &rarr; New Egg
Eggs &rarr; New Egg
@endsection
@section('content-header')
<h1>New Egg<small>Create a new Egg to assign to servers.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li><a href="{{ route('admin.nests') }}">Nests</a></li>
<li><a href="{{ route('admin.eggs') }}">Eggs</a></li>
<li class="active">New Egg</li>
</ol>
@endsection
@section('content')
<form action="{{ route('admin.nests.egg.new') }}" method="POST">
<form action="{{ route('admin.eggs.new') }}" method="POST">
<div class="row">
<div class="col-xs-12">
<div class="box">
@ -24,17 +24,6 @@
<div class="box-body">
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label for="pNestId" class="form-label">Associated Nest</label>
<div>
<select name="nest_id" id="pNestId">
@foreach($nests as $nest)
<option value="{{ $nest->id }}" {{ old('nest_id') != $nest->id ?: 'selected' }}>{{ $nest->name }} &lt;{{ $nest->author }}&gt;</option>
@endforeach
</select>
<p class="text-muted small">Think of a Nest as a category. You can put multiple Eggs in a nest, but consider putting only Eggs that are related to each other in each Nest.</p>
</div>
</div>
<div class="form-group">
<label for="pName" class="form-label">Name</label>
<input type="text" id="pName" name="name" value="{{ old('name') }}" class="form-control" />
@ -63,7 +52,7 @@
</div>
<div class="col-sm-6">
<div class="form-group">
<label for="pDockerImage" class="control-label">Docker Images</label>
<label for="pDockerImages" class="control-label">Docker Images</label>
<textarea id="pDockerImages" name="docker_images" rows="4" placeholder="quay.io/panel/service" class="form-control">{{ old('docker_images') }}</textarea>
<p class="text-muted small">The docker images available to servers using this egg. Enter one per line. Users will be able to select from this list of images if more than one value is provided.</p>
</div>
@ -137,19 +126,8 @@
{!! Theme::js('vendor/lodash/lodash.js') !!}
<script>
$(document).ready(function() {
$('#pNestId').select2().change();
$('#pConfigFrom').select2();
});
$('#pNestId').on('change', function (event) {
$('#pConfigFrom').html('<option value="">None</option>').select2({
data: $.map(_.get(Panel.nests, $(this).val() + '.eggs', []), function (item) {
return {
id: item.id,
text: item.name + ' <' + item.author + '>',
};
}),
});
});
$('textarea[data-action="handle-tabs"]').on('keydown', function(event) {
if (event.keyCode === 9) {
event.preventDefault();

View File

@ -1,16 +1,15 @@
@extends('layouts.admin')
@section('title')
Nests &rarr; Egg: {{ $egg->name }} &rarr; Install Script
Eggs &rarr; Egg: {{ $egg->name }} &rarr; Install Script
@endsection
@section('content-header')
<h1>{{ $egg->name }}<small>Manage the install script for this Egg.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li><a href="{{ route('admin.nests') }}">Nests</a></li>
<li><a href="{{ route('admin.nests.view', $egg->nest->id) }}">{{ $egg->nest->name }}</a></li>
<li><a href="{{ route('admin.nests.egg.view', $egg->id) }}">{{ $egg->name }}</a></li>
<li><a href="{{ route('admin.eggs') }}">Eggs</a></li>
<li><a href="{{ route('admin.eggs.view', $egg->id) }}">{{ $egg->name }}</a></li>
<li class="active">{{ $egg->name }}</li>
</ol>
@endsection
@ -20,14 +19,14 @@
<div class="col-xs-12">
<div class="nav-tabs-custom nav-tabs-floating">
<ul class="nav nav-tabs">
<li><a href="{{ route('admin.nests.egg.view', $egg->id) }}">Configuration</a></li>
<li><a href="{{ route('admin.nests.egg.variables', $egg->id) }}">Variables</a></li>
<li class="active"><a href="{{ route('admin.nests.egg.scripts', $egg->id) }}">Install Script</a></li>
<li><a href="{{ route('admin.eggs.view', $egg->id) }}">Configuration</a></li>
<li><a href="{{ route('admin.eggs.variables', $egg->id) }}">Variables</a></li>
<li class="active"><a href="{{ route('admin.eggs.scripts', $egg->id) }}">Install Script</a></li>
</ul>
</div>
</div>
</div>
<form action="{{ route('admin.nests.egg.scripts', $egg->id) }}" method="POST">
<form action="{{ route('admin.eggs.scripts', $egg->id) }}" method="POST">
<div class="row">
<div class="col-xs-12">
<div class="box">
@ -37,7 +36,7 @@
@if(! is_null($egg->copyFrom))
<div class="box-body">
<div class="callout callout-warning no-margin">
This service option is copying installation scripts and container options from <a href="{{ route('admin.nests.egg.view', $egg->copyFrom->id) }}">{{ $egg->copyFrom->name }}</a>. Any changes you make to this script will not apply unless you select "None" from the dropdown box below.
This egg option is copying installation scripts and container options from <a href="{{ route('admin.eggs.view', $egg->copyFrom->id) }}">{{ $egg->copyFrom->name }}</a>. Any changes you make to this script will not apply unless you select "None" from the dropdown box below.
</div>
</div>
@endif
@ -69,10 +68,10 @@
</div>
<div class="row">
<div class="col-xs-12 text-muted">
The following service options rely on this script:
The following eggs rely on this script:
@if(count($relyOnScript) > 0)
@foreach($relyOnScript as $rely)
<a href="{{ route('admin.nests.egg.view', $rely->id) }}">
<a href="{{ route('admin.eggs.view', $rely->id) }}">
<code>{{ $rely->name }}</code>@if(!$loop->last),&nbsp;@endif
</a>
@endforeach

View File

@ -8,9 +8,8 @@
<h1>{{ $egg->name }}<small>Managing variables for this Egg.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li><a href="{{ route('admin.nests') }}">Nests</a></li>
<li><a href="{{ route('admin.nests.view', $egg->nest->id) }}">{{ $egg->nest->name }}</a></li>
<li><a href="{{ route('admin.nests.egg.view', $egg->id) }}">{{ $egg->name }}</a></li>
<li><a href="{{ route('admin.eggs') }}">Eggs</a></li>
<li><a href="{{ route('admin.eggs.view', $egg->id) }}">{{ $egg->name }}</a></li>
<li class="active">Variables</li>
</ol>
@endsection
@ -20,9 +19,9 @@
<div class="col-xs-12">
<div class="nav-tabs-custom nav-tabs-floating">
<ul class="nav nav-tabs">
<li><a href="{{ route('admin.nests.egg.view', $egg->id) }}">Configuration</a></li>
<li class="active"><a href="{{ route('admin.nests.egg.variables', $egg->id) }}">Variables</a></li>
<li><a href="{{ route('admin.nests.egg.scripts', $egg->id) }}">Install Script</a></li>
<li><a href="{{ route('admin.eggs.view', $egg->id) }}">Configuration</a></li>
<li class="active"><a href="{{ route('admin.eggs.variables', $egg->id) }}">Variables</a></li>
<li><a href="{{ route('admin.eggs.scripts', $egg->id) }}">Install Script</a></li>
</ul>
</div>
</div>
@ -43,7 +42,7 @@
<div class="box-header with-border">
<h3 class="box-title">{{ $variable->name }}</h3>
</div>
<form action="{{ route('admin.nests.egg.variables.edit', ['egg' => $egg->id, 'variable' => $variable->id]) }}" method="POST">
<form action="{{ route('admin.eggs.variables.edit', ['egg' => $egg->id, 'variable' => $variable->id]) }}" method="POST">
<div class="box-body">
<div class="form-group">
<label class="form-label">Name</label>
@ -96,7 +95,7 @@
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Create New Egg Variable</h4>
</div>
<form action="{{ route('admin.nests.egg.variables', $egg->id) }}" method="POST">
<form action="{{ route('admin.eggs.variables', $egg->id) }}" method="POST">
<div class="modal-body">
<div class="form-group">
<label class="control-label">Name <span class="field-required"></span></label>

View File

@ -1,15 +1,14 @@
@extends('layouts.admin')
@section('title')
Nests &rarr; Egg: {{ $egg->name }}
Eggs &rarr; Egg: {{ $egg->name }}
@endsection
@section('content-header')
<h1>{{ $egg->name }}<small>{{ str_limit($egg->description, 50) }}</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li><a href="{{ route('admin.nests') }}">Nests</a></li>
<li><a href="{{ route('admin.nests.view', $egg->nest->id) }}">{{ $egg->nest->name }}</a></li>
<li><a href="{{ route('admin.eggs') }}">Eggs</a></li>
<li class="active">{{ $egg->name }}</li>
</ol>
@endsection
@ -19,14 +18,14 @@
<div class="col-xs-12">
<div class="nav-tabs-custom nav-tabs-floating">
<ul class="nav nav-tabs">
<li class="active"><a href="{{ route('admin.nests.egg.view', $egg->id) }}">Configuration</a></li>
<li><a href="{{ route('admin.nests.egg.variables', $egg->id) }}">Variables</a></li>
<li><a href="{{ route('admin.nests.egg.scripts', $egg->id) }}">Install Script</a></li>
<li class="active"><a href="{{ route('admin.eggs.view', $egg->id) }}">Configuration</a></li>
<li><a href="{{ route('admin.eggs.variables', $egg->id) }}">Variables</a></li>
<li><a href="{{ route('admin.eggs.scripts', $egg->id) }}">Install Script</a></li>
</ul>
</div>
</div>
</div>
<form action="{{ route('admin.nests.egg.view', $egg->id) }}" enctype="multipart/form-data" method="POST">
<form action="{{ route('admin.eggs.view', $egg->id) }}" enctype="multipart/form-data" method="POST">
<div class="row">
<div class="col-xs-12">
<div class="box box-danger">
@ -51,7 +50,7 @@
</div>
</div>
</form>
<form action="{{ route('admin.nests.egg.view', $egg->id) }}" method="POST">
<form action="{{ route('admin.eggs.view', $egg->id) }}" method="POST">
<div class="row">
<div class="col-xs-12">
<div class="box">
@ -137,7 +136,7 @@
<label for="pConfigFrom" class="form-label">Copy Settings From</label>
<select name="config_from" id="pConfigFrom" class="form-control">
<option value="">None</option>
@foreach($egg->nest->eggs as $o)
@foreach($egg->newQuery()->get() as $o)
<option value="{{ $o->id }}" {{ ($egg->config_from !== $o->id) ?: 'selected' }}>{{ $o->name }} &lt;{{ $o->author }}&gt;</option>
@endforeach
</select>
@ -171,7 +170,7 @@
<div class="box-footer">
{!! csrf_field() !!}
<button type="submit" name="_method" value="PATCH" class="btn btn-primary btn-sm pull-right">Save</button>
<a href="{{ route('admin.nests.egg.export', $egg->id) }}" class="btn btn-sm btn-info pull-right" style="margin-right:10px;">Export</a>
<a href="{{ route('admin.eggs.export', $egg->id) }}" class="btn btn-sm btn-info pull-right" style="margin-right:10px;">Export</a>
<button id="deleteButton" type="submit" name="_method" value="DELETE" class="btn btn-danger btn-sm muted muted-hover">
<i class="fa fa-trash-o"></i>
</button>

View File

@ -118,7 +118,7 @@
@foreach ($mount->eggs as $egg)
<tr>
<td class="col-sm-2 middle"><code>{{ $egg->id }}</code></td>
<td class="middle"><a href="{{ route('admin.nests.egg.view', $egg->id) }}">{{ $egg->name }}</a></td>
<td class="middle"><a href="{{ route('admin.eggs.view', $egg->id) }}">{{ $egg->name }}</a></td>
<td class="col-sm-1 middle">
<button data-action="detach-egg" data-id="{{ $egg->id }}" class="btn btn-sm btn-danger"><i class="fa fa-trash-o"></i></button>
</td>
@ -179,16 +179,10 @@
<div class="form-group col-md-12">
<label for="pEggs">Eggs</label>
<select id="pEggs" name="eggs[]" class="form-control" multiple>
@foreach ($nests as $nest)
<optgroup label="{{ $nest->name }}">
@foreach ($nest->eggs as $egg)
@foreach ($eggs as $egg)
@if (! in_array($egg->id, $mount->eggs->pluck('id')->toArray()))
<option value="{{ $egg->id }}">{{ $egg->name }}</option>
@endif
@endforeach
</optgroup>
@endforeach
</select>
</div>

View File

@ -1,47 +0,0 @@
@extends('layouts.admin')
@section('title')
New Nest
@endsection
@section('content-header')
<h1>New Nest<small>Configure a new nest to deploy to all nodes.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li><a href="{{ route('admin.nests') }}">Nests</a></li>
<li class="active">New</li>
</ol>
@endsection
@section('content')
<form action="{{ route('admin.nests.new') }}" method="POST">
<div class="row">
<div class="col-md-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">New Nest</h3>
</div>
<div class="box-body">
<div class="form-group">
<label class="control-label">Name</label>
<div>
<input type="text" name="name" class="form-control" value="{{ old('name') }}" />
<p class="text-muted"><small>This should be a descriptive category name that encompasses all of the eggs within the nest.</small></p>
</div>
</div>
<div class="form-group">
<label class="control-label">Description</label>
<div>
<textarea name="description" class="form-control" rows="6">{{ old('description') }}</textarea>
</div>
</div>
</div>
<div class="box-footer">
{!! csrf_field() !!}
<button type="submit" class="btn btn-primary pull-right">Save</button>
</div>
</div>
</div>
</div>
</form>
@endsection

View File

@ -1,117 +0,0 @@
@extends('layouts.admin')
@section('title')
Nests &rarr; {{ $nest->name }}
@endsection
@section('content-header')
<h1>{{ $nest->name }}<small>{{ str_limit($nest->description, 50) }}</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li><a href="{{ route('admin.nests') }}">Nests</a></li>
<li class="active">{{ $nest->name }}</li>
</ol>
@endsection
@section('content')
<div class="row">
<form action="{{ route('admin.nests.view', $nest->id) }}" method="POST">
<div class="col-md-6">
<div class="box">
<div class="box-body">
<div class="form-group">
<label class="control-label">Name <span class="field-required"></span></label>
<div>
<input type="text" name="name" class="form-control" value="{{ $nest->name }}" />
<p class="text-muted"><small>This should be a descriptive category name that encompasses all of the options within the service.</small></p>
</div>
</div>
<div class="form-group">
<label class="control-label">Description</label>
<div>
<textarea name="description" class="form-control" rows="7">{{ $nest->description }}</textarea>
</div>
</div>
</div>
<div class="box-footer">
{!! csrf_field() !!}
<button type="submit" name="_method" value="PATCH" class="btn btn-primary btn-sm pull-right">Save</button>
<button id="deleteButton" type="submit" name="_method" value="DELETE" class="btn btn-sm btn-danger muted muted-hover"><i class="fa fa-trash-o"></i></button>
</div>
</div>
</div>
</form>
<div class="col-md-6">
<div class="box">
<div class="box-body">
<div class="form-group">
<label class="control-label">Nest ID</label>
<div>
<input type="text" readonly class="form-control" value="{{ $nest->id }}" />
<p class="text-muted small">A unique ID used for identification of this nest internally and through the API.</p>
</div>
</div>
<div class="form-group">
<label class="control-label">Author</label>
<div>
<input type="text" readonly class="form-control" value="{{ $nest->author }}" />
<p class="text-muted small">The author of this service option. Please direct questions and issues to them unless this is an official option authored by <code>panel@example.com</code>.</p>
</div>
</div>
<div class="form-group">
<label class="control-label">UUID</label>
<div>
<input type="text" readonly class="form-control" value="{{ $nest->uuid }}" />
<p class="text-muted small">A UUID that all servers using this option are assigned for identification purposes.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Nest Eggs</h3>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover">
<tr>
<th>ID</th>
<th>Name</th>
<th>Description</th>
<th class="text-center">Servers</th>
<th class="text-center"></th>
</tr>
@foreach($nest->eggs as $egg)
<tr>
<td class="align-middle"><code>{{ $egg->id }}</code></td>
<td class="align-middle"><a href="{{ route('admin.nests.egg.view', $egg->id) }}" data-toggle="tooltip" data-placement="right" title="{{ $egg->author }}">{{ $egg->name }}</a></td>
<td class="col-xs-8 align-middle">{{ $egg->description }}</td>
<td class="text-center align-middle"><code>{{ $egg->servers->count() }}</code></td>
<td class="align-middle">
<a href="{{ route('admin.nests.egg.export', ['egg' => $egg->id]) }}"><i class="fa fa-download"></i></a>
</td>
</tr>
@endforeach
</table>
</div>
<div class="box-footer">
<a href="{{ route('admin.nests.egg.new') }}"><button class="btn btn-success btn-sm pull-right">New Egg</button></a>
</div>
</div>
</div>
</div>
@endsection
@section('footer-scripts')
@parent
<script>
$('#deleteButton').on('mouseenter', function (event) {
$(this).find('i').html(' Delete Nest');
}).on('mouseleave', function (event) {
$(this).find('i').html('');
});
</script>
@endsection

View File

@ -40,14 +40,14 @@
<th>ID</th>
<th>Server Name</th>
<th>Owner</th>
<th>Service</th>
<th>Egg</th>
</tr>
@foreach($servers as $server)
<tr data-server="{{ $server->uuid }}">
<td><code>{{ $server->uuidShort }}</code></td>
<td><a href="{{ route('admin.servers.view', $server->id) }}">{{ $server->name }}</a></td>
<td><a href="{{ route('admin.users.view', $server->owner_id) }}">{{ $server->user->username }}</a></td>
<td>{{ $server->nest->name }} ({{ $server->egg->name }})</td>
<td><a href="{{ route('admin.eggs.view', $server->egg) }}">{{ $server->egg->name }}</a></td>
</tr>
@endforeach
</table>

View File

@ -226,31 +226,25 @@
<div class="col-md-6">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Nest Configuration</h3>
<h3 class="box-title">Egg Configuration</h3>
</div>
<div class="box-body row">
<div class="form-group col-xs-12">
<label for="pNestId">Nest</label>
<select id="pNestId" name="nest_id" class="form-control">
@foreach($nests as $nest)
<option value="{{ $nest->id }}"
@if($nest->id === old('nest_id'))
<label for="pEggId">Egg</label>
<select id="pEggId" name="egg_id" class="form-control">
@foreach($eggs as $egg)
<option value="{{ $egg->id }}"
@if($egg->id === old('egg_id'))
selected="selected"
@endif
>{{ $nest->name }}</option>
>{{ $egg->name }}</option>
@endforeach
</select>
<p class="small text-muted no-margin">Select the Nest that this server will be grouped under.</p>
</div>
<div class="form-group col-xs-12">
<label for="pEggId">Egg</label>
<select id="pEggId" name="egg_id" class="form-control"></select>
<p class="small text-muted no-margin">Select the Egg that will define how this server should operate.</p>
</div>
<div class="form-group col-xs-12">
<div class="checkbox checkbox-primary no-margin-bottom">
<input type="checkbox" id="pSkipScripting" name="skip_scripts" value="1" {{ \App\Helpers\Utilities::checked('skip_scripts', 0) }} />
@ -297,7 +291,7 @@
</div>
<div class="box-header with-border" style="margin-top:-10px;">
<h3 class="box-title">Service Variables</h3>
<h3 class="box-title">Egg Variables</h3>
</div>
<div class="box-body row" id="appendVariablesTo"></div>
@ -317,8 +311,8 @@
{!! Theme::js('vendor/lodash/lodash.js') !!}
<script type="application/javascript">
// Persist 'Service Variables'
function serviceVariablesUpdated(eggId, ids) {
// Persist 'Egg Variables'
function eggVariablesUpdated(eggId, ids) {
@if (old('egg_id'))
// Check if the egg id matches.
if (eggId != '{{ old('egg_id') }}') {
@ -335,7 +329,7 @@
$('#pDefaultContainer').val('{{ old('image') }}');
@endif
}
// END Persist 'Service Variables'
// END Persist 'Egg Variables'
</script>
{!! Theme::js('js/admin/new-server.js?v=20220530') !!}
@ -379,17 +373,11 @@
@endif
// END Persist 'Node' select2
// Persist 'Nest' select2
@if (old('nest_id'))
$('#pNestId').val('{{ old('nest_id') }}').change();
// Persist 'Egg' select2
@if (old('egg_id'))
$('#pEggId').val('{{ old('egg_id') }}').change();
@endif
// END Persist 'Egg' select2
@endif
// END Persist 'Nest' select2
});
</script>
@endsection

View File

@ -44,8 +44,7 @@
<tr>
<td>Current Egg</td>
<td>
<a href="{{ route('admin.nests.view', $server->nest_id) }}">{{ $server->nest->name }}</a> ::
<a href="{{ route('admin.nests.egg.view', $server->egg_id) }}">{{ $server->egg->name }}</a>
<a href="{{ route('admin.eggs.view', $server->egg_id) }}">{{ $server->egg->name }}</a>
</td>
</tr>
<tr>

View File

@ -23,7 +23,7 @@
<h3 class="box-title">Reinstall Server</h3>
</div>
<div class="box-body">
<p>This will reinstall the server with the assigned service scripts. <strong>Danger!</strong> This could overwrite server data.</p>
<p>This will reinstall the server with the assigned egg scripts. <strong>Danger!</strong> This could overwrite server data.</p>
</div>
<div class="box-footer">
@if($server->isInstalled())

View File

@ -43,34 +43,29 @@
<div class="col-md-6">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Service Configuration</h3>
<h3 class="box-title">Egg Configuration</h3>
</div>
<div class="box-body row">
<div class="col-xs-12">
<p class="small text-danger">
Changing any of the below values will result in the server processing a re-install command. The server will be stopped and will then proceed.
If you would like the service scripts to not run, ensure the box is checked at the bottom.
If you would like the egg scripts to not run, ensure the box is checked at the bottom.
</p>
<p class="small text-danger">
<strong>This is a destructive operation in many cases. This server will be stopped immediately in order for this action to proceed.</strong>
</p>
</div>
<div class="form-group col-xs-12">
<label for="pNestId">Nest</label>
<select name="nest_id" id="pNestId" class="form-control">
@foreach($nests as $nest)
<option value="{{ $nest->id }}"
@if($nest->id === $server->nest_id)
<label for="pEggId">Egg</label>
<select name="egg_id" id="pEggId" class="form-control">
@foreach($eggs as $egg)
<option value="{{ $egg->id }}"
@if($egg->id === $server->egg_id)
selected
@endif
>{{ $nest->name }}</option>
>{{ $egg->name }}</option>
@endforeach
</select>
<p class="small text-muted no-margin">Select the Nest that this server will be grouped into.</p>
</div>
<div class="form-group col-xs-12">
<label for="pEggId">Egg</label>
<select name="egg_id" id="pEggId" class="form-control"></select>
<p class="small text-muted no-margin">Select the Egg that will provide processing data for this server.</p>
</div>
<div class="form-group col-xs-12">
@ -82,6 +77,8 @@
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Docker Image Configuration</h3>
@ -108,10 +105,8 @@
{!! Theme::js('vendor/lodash/lodash.js') !!}
<script>
$(document).ready(function () {
$('#pEggId').select2({placeholder: 'Select a Nest Egg'}).on('change', function () {
var selectedEgg = _.isNull($(this).val()) ? $(this).find('option').first().val() : $(this).val();
var parentChain = _.get(Panel.nests, $("#pNestId").val());
var objectChain = _.get(parentChain, 'eggs.' + selectedEgg);
$('#pEggId').select2({placeholder: 'Select an Egg'}).on('change', function () {
var objectChain = _.get(Panel.eggs, $("#pEggId").val());
const images = _.get(objectChain, 'docker_images', [])
$('#pDockerImage').html('');
@ -135,11 +130,7 @@
}
}
if (!_.get(objectChain, 'startup', false)) {
$('#pDefaultStartupCommand').val(_.get(parentChain, 'startup', 'ERROR: Startup Not Defined!'));
} else {
$('#pDefaultStartupCommand').val(_.get(objectChain, 'startup'));
}
$('#appendVariablesTo').html('');
$.each(_.get(objectChain, 'variables', []), function (i, item) {
@ -163,23 +154,6 @@
</div>';
$('#appendVariablesTo').append(dataAppend).find('#egg_variable_' + item.env_variable).val(setValue);
});
});
$('#pNestId').select2({placeholder: 'Select a Nest'}).on('change', function () {
$('#pEggId').html('').select2({
data: $.map(_.get(Panel.nests, $(this).val() + '.eggs', []), function (item) {
return {
id: item.id,
text: item.name,
};
}),
});
if (_.isObject(_.get(Panel.nests, $(this).val() + '.eggs.' + Panel.server.egg_id))) {
$('#pEggId').val(Panel.server.egg_id);
}
$('#pEggId').change();
}).change();
});
</script>

View File

@ -69,23 +69,12 @@
<aside class="main-sidebar">
<section class="sidebar">
<ul class="sidebar-menu">
<li class="header">BASIC ADMINISTRATION</li>
<li class="header">MANAGEMENT</li>
<li class="{{ Route::currentRouteName() !== 'admin.index' ?: 'active' }}">
<a href="{{ route('admin.index') }}">
<i class="fa fa-home"></i> <span>Overview</span>
</a>
</li>
<li class="{{ ! starts_with(Route::currentRouteName(), 'admin.settings') ?: 'active' }}">
<a href="{{ route('admin.settings')}}">
<i class="fa fa-wrench"></i> <span>Settings</span>
</a>
</li>
<li class="{{ ! starts_with(Route::currentRouteName(), 'admin.api') ?: 'active' }}">
<a href="{{ route('admin.api.index')}}">
<i class="fa fa-gamepad"></i> <span>Application API</span>
</a>
</li>
<li class="header">MANAGEMENT</li>
<li class="{{ ! starts_with(Route::currentRouteName(), 'admin.databases') ?: 'active' }}">
<a href="{{ route('admin.databases') }}">
<i class="fa fa-database"></i> <span>Databases</span>
@ -111,15 +100,26 @@
<i class="fa fa-users"></i> <span>Users</span>
</a>
</li>
<li class="header">SERVICE MANAGEMENT</li>
<li class="header">SERVICES</li>
<li class="{{ ! starts_with(Route::currentRouteName(), 'admin.eggs') ?: 'active' }}">
<a href="{{ route('admin.eggs') }}">
<i class="fa fa-th-large"></i> <span>Eggs</span>
</a>
</li>
<li class="{{ ! starts_with(Route::currentRouteName(), 'admin.mounts') ?: 'active' }}">
<a href="{{ route('admin.mounts') }}">
<i class="fa fa-magic"></i> <span>Mounts</span>
</a>
</li>
<li class="{{ ! starts_with(Route::currentRouteName(), 'admin.nests') ?: 'active' }}">
<a href="{{ route('admin.nests') }}">
<i class="fa fa-th-large"></i> <span>Nests</span>
<li class="header">OTHER</li>
<li class="{{ ! starts_with(Route::currentRouteName(), 'admin.settings') ?: 'active' }}">
<a href="{{ route('admin.settings')}}">
<i class="fa fa-wrench"></i> <span>Settings</span>
</a>
</li>
<li class="{{ ! starts_with(Route::currentRouteName(), 'admin.api') ?: 'active' }}">
<a href="{{ route('admin.api.index')}}">
<i class="fa fa-gamepad"></i> <span>Application API</span>
</a>
</li>
</ul>

View File

@ -194,35 +194,37 @@ Route::group(['prefix' => 'mounts'], function () {
/*
|--------------------------------------------------------------------------
| Nest Controller Routes
| Egg Controller Routes
|--------------------------------------------------------------------------
|
| Endpoint: /admin/nests
| Endpoint: /admin/eggs
|
*/
Route::group(['prefix' => 'nests'], function () {
Route::get('/', [Admin\Nests\NestController::class, 'index'])->name('admin.nests');
Route::get('/new', [Admin\Nests\NestController::class, 'create'])->name('admin.nests.new');
Route::get('/view/{nest:id}', [Admin\Nests\NestController::class, 'view'])->name('admin.nests.view');
Route::get('/egg/new', [Admin\Nests\EggController::class, 'create'])->name('admin.nests.egg.new');
Route::get('/egg/{egg:id}', [Admin\Nests\EggController::class, 'view'])->name('admin.nests.egg.view');
Route::get('/egg/{egg:id}/export', [Admin\Nests\EggShareController::class, 'export'])->name('admin.nests.egg.export');
Route::get('/egg/{egg:id}/variables', [Admin\Nests\EggVariableController::class, 'view'])->name('admin.nests.egg.variables');
Route::get('/egg/{egg:id}/scripts', [Admin\Nests\EggScriptController::class, 'index'])->name('admin.nests.egg.scripts');
Route::group(['prefix' => 'eggs'], function () {
Route::controller(Admin\Eggs\EggController::class)->group(function () {
Route::get('/', 'index')->name('admin.eggs');
Route::get('/new', 'create')->name('admin.eggs.new');
Route::post('/new', 'store');
Route::patch('/{egg:id}', 'update');
Route::get('/{egg:id}', 'view')->name('admin.eggs.view');
Route::delete('/{egg:id}', 'destroy');
});
Route::post('/new', [Admin\Nests\NestController::class, 'store']);
Route::post('/import', [Admin\Nests\EggShareController::class, 'import'])->name('admin.nests.egg.import');
Route::post('/egg/new', [Admin\Nests\EggController::class, 'store']);
Route::post('/egg/{egg:id}/variables', [Admin\Nests\EggVariableController::class, 'store']);
Route::controller(Admin\Eggs\EggShareController::class)->group(function () {
Route::put('/{egg:id}', 'update');
Route::post('/import', 'import')->name('admin.eggs.import');
Route::get('/{egg:id}/export', 'export')->name('admin.eggs.export');
});
Route::put('/egg/{egg:id}', [Admin\Nests\EggShareController::class, 'update']);
Route::controller(Admin\Eggs\EggScriptController::class)->group(function () {
Route::get('/{egg:id}/scripts', 'index')->name('admin.eggs.scripts');
Route::patch('/{egg:id}/scripts', 'update');
});
Route::patch('/view/{nest:id}', [Admin\Nests\NestController::class, 'update']);
Route::patch('/egg/{egg:id}', [Admin\Nests\EggController::class, 'update']);
Route::patch('/egg/{egg:id}/scripts', [Admin\Nests\EggScriptController::class, 'update']);
Route::patch('/egg/{egg:id}/variables/{variable:id}', [Admin\Nests\EggVariableController::class, 'update'])->name('admin.nests.egg.variables.edit');
Route::delete('/view/{nest:id}', [Admin\Nests\NestController::class, 'destroy']);
Route::delete('/egg/{egg:id}', [Admin\Nests\EggController::class, 'destroy']);
Route::delete('/egg/{egg:id}/variables/{variable:id}', [Admin\Nests\EggVariableController::class, 'destroy']);
Route::controller(Admin\Eggs\EggVariableController::class)->group(function () {
Route::patch('/{egg:id}/variables/{variable:id}', 'update')->name('admin.eggs.variables.edit');
Route::get('/{egg:id}/variables', 'view')->name('admin.eggs.variables');
Route::post('/{egg:id}/variables', 'store');
Route::delete('/{egg:id}/variables/{variable:id}', 'destroy');
});
});

View File

@ -11,7 +11,6 @@ use App\Http\Controllers\Api\Application;
| Endpoint: /api/application/users
|
*/
Route::group(['prefix' => '/users'], function () {
Route::get('/', [Application\Users\UserController::class, 'index'])->name('api.application.users');
Route::get('/{user:id}', [Application\Users\UserController::class, 'view'])->name('api.application.users.view');
@ -106,19 +105,13 @@ Route::group(['prefix' => '/servers'], function () {
/*
|--------------------------------------------------------------------------
| Nest Controller Routes
| Egg Controller Routes
|--------------------------------------------------------------------------
|
| Endpoint: /api/application/nests
| Endpoint: /api/application/eggs
|
*/
Route::group(['prefix' => '/nests'], function () {
Route::get('/', [Application\Nests\NestController::class, 'index'])->name('api.application.nests');
Route::get('/{nest:id}', [Application\Nests\NestController::class, 'view'])->name('api.application.nests.view');
// Egg Management Endpoint
Route::group(['prefix' => '/{nest:id}/eggs'], function () {
Route::get('/', [Application\Nests\EggController::class, 'index'])->name('api.application.nests.eggs');
Route::get('/{egg:id}', [Application\Nests\EggController::class, 'view'])->name('api.application.nests.eggs.view');
});
Route::group(['prefix' => '/eggs'], function () {
Route::get('/', [Application\Eggs\EggController::class, 'index'])->name('api.application.eggs.eggs');
Route::get('/{egg:id}', [Application\Eggs\EggController::class, 'view'])->name('api.application.eggs.eggs.view');
});

View File

@ -85,7 +85,6 @@ abstract class ApplicationApiIntegrationTestCase extends IntegrationTestCase
'r_allocations' => AdminAcl::READ | AdminAcl::WRITE,
'r_users' => AdminAcl::READ | AdminAcl::WRITE,
'r_locations' => AdminAcl::READ | AdminAcl::WRITE,
'r_nests' => AdminAcl::READ | AdminAcl::WRITE,
'r_eggs' => AdminAcl::READ | AdminAcl::WRITE,
'r_database_hosts' => AdminAcl::READ | AdminAcl::WRITE,
'r_server_databases' => AdminAcl::READ | AdminAcl::WRITE,

View File

@ -1,23 +1,22 @@
<?php
namespace App\Tests\Integration\Api\Application\Nests;
namespace App\Tests\Integration\Api\Application;
use Illuminate\Support\Arr;
use App\Models\Egg;
use Illuminate\Http\Response;
use App\Transformers\Api\Application\EggTransformer;
use App\Tests\Integration\Api\Application\ApplicationApiIntegrationTestCase;
use Illuminate\Http\Response;
use Illuminate\Support\Arr;
class EggControllerTest extends ApplicationApiIntegrationTestCase
{
/**
* Test that all the eggs belonging to a given nest can be returned.
* Test that all the eggs can be returned.
*/
public function testListAllEggsInNest()
public function testListAllEggs()
{
$eggs = Egg::query()->where('nest_id', 1)->get();
$eggs = Egg::query()->get();
$response = $this->getJson('/api/application/nests/' . $eggs->first()->nest_id . '/eggs');
$response = $this->getJson('/api/application/eggs');
$response->assertStatus(Response::HTTP_OK);
$response->assertJsonCount(count($eggs), 'data');
$response->assertJsonStructure([
@ -26,7 +25,7 @@ class EggControllerTest extends ApplicationApiIntegrationTestCase
[
'object',
'attributes' => [
'id', 'uuid', 'nest', 'author', 'description', 'docker_image', 'startup', 'created_at', 'updated_at',
'id', 'uuid', 'author', 'description', 'docker_image', 'startup', 'created_at', 'updated_at',
'script' => ['privileged', 'install', 'entry', 'container', 'extends'],
'config' => [
'files' => [],
@ -61,12 +60,12 @@ class EggControllerTest extends ApplicationApiIntegrationTestCase
{
$egg = Egg::query()->findOrFail(1);
$response = $this->getJson('/api/application/nests/' . $egg->nest_id . '/eggs/' . $egg->id);
$response = $this->getJson('/api/application/eggs/' . $egg->id);
$response->assertStatus(Response::HTTP_OK);
$response->assertJsonStructure([
'object',
'attributes' => [
'id', 'uuid', 'nest', 'author', 'description', 'docker_image', 'startup', 'script' => [], 'config' => [], 'created_at', 'updated_at',
'id', 'uuid', 'author', 'description', 'docker_image', 'startup', 'script' => [], 'config' => [], 'created_at', 'updated_at',
],
]);
@ -83,13 +82,12 @@ class EggControllerTest extends ApplicationApiIntegrationTestCase
{
$egg = Egg::query()->findOrFail(1);
$response = $this->getJson('/api/application/nests/' . $egg->nest_id . '/eggs/' . $egg->id . '?include=servers,variables,nest');
$response = $this->getJson('/api/application/eggs/' . $egg->id . '?include=servers,variables');
$response->assertStatus(Response::HTTP_OK);
$response->assertJsonStructure([
'object',
'attributes' => [
'relationships' => [
'nest' => ['object', 'attributes'],
'servers' => ['object', 'data' => []],
'variables' => ['object', 'data' => []],
],
@ -102,9 +100,7 @@ class EggControllerTest extends ApplicationApiIntegrationTestCase
*/
public function testGetMissingEgg()
{
$egg = Egg::query()->findOrFail(1);
$response = $this->getJson('/api/application/nests/' . $egg->nest_id . '/eggs/nil');
$response = $this->getJson('/api/application/eggs/nil');
$this->assertNotFoundJson($response);
}
@ -117,7 +113,7 @@ class EggControllerTest extends ApplicationApiIntegrationTestCase
$egg = Egg::query()->findOrFail(1);
$this->createNewDefaultApiKey($this->getApiUser(), ['r_eggs' => 0]);
$response = $this->getJson('/api/application/nests/' . $egg->nest_id . '/eggs');
$response = $this->getJson('/api/application/eggs');
$this->assertAccessDeniedJson($response);
}
}

View File

@ -1,127 +0,0 @@
<?php
namespace App\Tests\Integration\Api\Application\Nests;
use Illuminate\Http\Response;
use App\Contracts\Repository\NestRepositoryInterface;
use App\Transformers\Api\Application\NestTransformer;
use App\Tests\Integration\Api\Application\ApplicationApiIntegrationTestCase;
class NestControllerTest extends ApplicationApiIntegrationTestCase
{
private NestRepositoryInterface $repository;
/**
* Setup tests.
*/
public function setUp(): void
{
parent::setUp();
$this->repository = $this->app->make(NestRepositoryInterface::class);
}
/**
* Test that the expected nests are returned by the request.
*/
public function testNestResponse()
{
/** @var \App\Models\Nest[] $nests */
$nests = $this->repository->all();
$response = $this->getJson('/api/application/nests');
$response->assertStatus(Response::HTTP_OK);
$response->assertJsonCount(count($nests), 'data');
$response->assertJsonStructure([
'object',
'data' => [['object', 'attributes' => ['id', 'uuid', 'author', 'name', 'description', 'created_at', 'updated_at']]],
'meta' => ['pagination' => ['total', 'count', 'per_page', 'current_page', 'total_pages']],
]);
$response->assertJson([
'object' => 'list',
'data' => [],
'meta' => [
'pagination' => [
'total' => 4,
'count' => 4,
'per_page' => 50,
'current_page' => 1,
'total_pages' => 1,
],
],
]);
foreach ($nests as $nest) {
$response->assertJsonFragment([
'object' => 'nest',
'attributes' => $this->getTransformer(NestTransformer::class)->transform($nest),
]);
}
}
/**
* Test that getting a single nest returns the expected result.
*/
public function testSingleNestResponse()
{
$nest = $this->repository->find(1);
$response = $this->getJson('/api/application/nests/' . $nest->id);
$response->assertStatus(Response::HTTP_OK);
$response->assertJsonStructure([
'object',
'attributes' => ['id', 'uuid', 'author', 'name', 'description', 'created_at', 'updated_at'],
]);
$response->assertJson([
'object' => 'nest',
'attributes' => $this->getTransformer(NestTransformer::class)->transform($nest),
]);
}
/**
* Test that including eggs in the response works as expected.
*/
public function testSingleNestWithEggsIncluded()
{
$nest = $this->repository->find(1);
$nest->loadMissing('eggs');
$response = $this->getJson('/api/application/nests/' . $nest->id . '?include=servers,eggs');
$response->assertStatus(Response::HTTP_OK);
$response->assertJsonStructure([
'object',
'attributes' => [
'relationships' => [
'eggs' => ['object', 'data' => []],
'servers' => ['object', 'data' => []],
],
],
]);
$response->assertJsonCount(count($nest->getRelation('eggs')), 'attributes.relationships.eggs.data');
}
/**
* Test that a missing nest returns a 404 error.
*/
public function testGetMissingNest()
{
$response = $this->getJson('/api/application/nests/nil');
$this->assertNotFoundJson($response);
}
/**
* Test that an authentication error occurs if a key does not have permission
* to access a resource.
*/
public function testErrorReturnedIfNoPermission()
{
$nest = $this->repository->find(1);
$this->createNewDefaultApiKey($this->getApiUser(), ['r_nests' => 0]);
$response = $this->getJson('/api/application/nests/' . $nest->id);
$this->assertAccessDeniedJson($response);
}
}

View File

@ -2,8 +2,7 @@
namespace App\Tests\Integration\Services\Servers;
use Exception;
use App\Models\Nest;
use App\Models\Egg;
use App\Models\User;
use App\Models\Server;
use App\Models\ServerVariable;
@ -16,7 +15,7 @@ class StartupModificationServiceTest extends IntegrationTestCase
{
/**
* Test that a non-admin request to modify the server startup parameters does
* not perform any egg or nest updates. This also attempts to pass through an
* not perform any egg updates. This also attempts to pass through an
* egg_id variable which should have no impact if the request is coming from
* a non-admin entity.
*/
@ -71,12 +70,11 @@ class StartupModificationServiceTest extends IntegrationTestCase
public function testServerIsProperlyModifiedAsAdminUser()
{
/** @var \App\Models\Egg $nextEgg */
$nextEgg = Nest::query()->findOrFail(2)->eggs()->firstOrFail();
$nextEgg = Egg::query()->findOrFail(6);
$server = $this->createServerModel(['egg_id' => 1]);
$this->assertNotSame($nextEgg->id, $server->egg_id);
$this->assertNotSame($nextEgg->nest_id, $server->nest_id);
$response = $this->getService()
->setUserLevel(User::USER_LEVEL_ADMIN)
@ -89,7 +87,6 @@ class StartupModificationServiceTest extends IntegrationTestCase
$this->assertInstanceOf(Server::class, $response);
$this->assertSame($nextEgg->id, $response->egg_id);
$this->assertSame($nextEgg->nest_id, $response->nest_id);
$this->assertSame('sample startup', $response->startup);
$this->assertSame('docker/hodor', $response->image);
$this->assertTrue($response->skip_scripts);

View File

@ -51,16 +51,9 @@ trait CreatesTestModels
}
if (empty($attributes['egg_id'])) {
$egg = !empty($attributes['nest_id'])
? Egg::query()->where('nest_id', $attributes['nest_id'])->firstOrFail()
: $this->getBungeecordEgg();
$egg = $this->getBungeecordEgg();
$attributes['egg_id'] = $egg->id;
$attributes['nest_id'] = $egg->nest_id;
}
if (empty($attributes['nest_id'])) {
$attributes['nest_id'] = Egg::query()->findOrFail($attributes['egg_id'])->nest_id;
}
unset($attributes['user_id'], $attributes['location_id']);
@ -71,7 +64,7 @@ trait CreatesTestModels
Allocation::query()->where('id', $server->allocation_id)->update(['server_id' => $server->id]);
return $server->fresh([
'location', 'user', 'node', 'allocation', 'nest', 'egg',
'location', 'user', 'node', 'allocation', 'egg',
]);
}