diff --git a/.env.example b/.env.example index 0c210be..1928a26 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -APP_NAME=Laravel +APP_NAME=Ptah.sh APP_ENV=local APP_KEY= APP_DEBUG=true diff --git a/app/Events/NodeTasks/ConfirmAgentUpgrade/ConfirmAgentUpgradeCompleted.php b/app/Events/NodeTasks/ConfirmAgentUpgrade/ConfirmAgentUpgradeCompleted.php new file mode 100644 index 0000000..ba12733 --- /dev/null +++ b/app/Events/NodeTasks/ConfirmAgentUpgrade/ConfirmAgentUpgradeCompleted.php @@ -0,0 +1,9 @@ + $node, 'initTaskGroup' => $initTaskGroup]); + $lastAgentVersion = AgentRelease::latest()->sole()->tag_name; + + $taskGroup = $node->actualTaskGroup(NodeTaskGroupType::SelfUpgrade); + + return Inertia::render('Nodes/Show', [ + 'node' => $node, + 'initTaskGroup' => $initTaskGroup, + 'lastAgentVersion' => $lastAgentVersion, + 'agentUpgradeTaskGroup' => $taskGroup?->is_completed ? null : $taskGroup, + ]); } /** @@ -74,4 +86,17 @@ public function destroy(Node $node) { // } + + public function upgradeAgent(Node $node, Request $request) + { + $req = $request->validate([ + 'targetVersion' => ['required', 'exists:agent_releases,tag_name'], + ]); + + DB::transaction(function () use ($node, $req) { + $node->upgradeAgent($req['targetVersion']); + }); + + return to_route('nodes.show', ['node' => $node->id]); + } } diff --git a/app/Jobs/CheckAgentUpdates.php b/app/Jobs/CheckAgentUpdates.php new file mode 100644 index 0000000..9753404 --- /dev/null +++ b/app/Jobs/CheckAgentUpdates.php @@ -0,0 +1,47 @@ +json(); + + foreach ($json['assets'] as $asset) { + preg_match('/^ptah-agent-(?.+)-(?.+).bin$/', $asset['name'], $matches); + if (empty($matches)) { + continue; + } + + $attrs = [ + 'tag_name' => $json['tag_name'], + 'download_url' => $asset['browser_download_url'], + 'os' => $matches['os'], + 'arch' => $matches['arch'], + ]; + + AgentRelease::firstOrCreate($attrs, $attrs); + } + } +} diff --git a/app/Models/AgentRelease.php b/app/Models/AgentRelease.php new file mode 100644 index 0000000..63a6b9f --- /dev/null +++ b/app/Models/AgentRelease.php @@ -0,0 +1,18 @@ +orderByDesc('id')->take(1)->get()[0] ?? null; } + + public function upgradeAgent($targetVersion): void + { + $release = AgentRelease::where('tag_name', $targetVersion)->sole(); + + $taskGroup = NodeTaskGroup::create([ + 'swarm_id' => $this->swarm_id, + 'node_id' => $this->id, + 'type' => NodeTaskGroupType::SelfUpgrade, + 'invoker_id' => auth()->user()->id, + ]); + + $taskGroup->tasks()->createMany([ + [ + 'type' => NodeTaskType::DownloadAgentUpgrade, + 'meta' => [ + 'targetVersion' => $targetVersion, + 'downloadUrl' => $release->download_url, + ], + 'payload' => [ + 'TargetVersion' => $targetVersion, + 'DownloadUrl' => $release->download_url, + ] + ], + [ + 'type' => NodeTaskType::UpdateAgentSymlink, + 'meta' => [ + 'targetVersion' => $targetVersion, + ], + 'payload' => [ + 'TargetVersion' => $targetVersion, + ] + ], + [ + 'type' => NodeTaskType::ConfirmAgentUpgrade, + 'meta' => [ + 'targetVersion' => $targetVersion, + ], + 'payload' => [ + 'TargetVersion' => $targetVersion, + ] + ] + ]); + } } diff --git a/app/Models/NodeTaskGroupType.php b/app/Models/NodeTaskGroupType.php index fc73f3e..45e39b0 100644 --- a/app/Models/NodeTaskGroupType.php +++ b/app/Models/NodeTaskGroupType.php @@ -8,4 +8,5 @@ enum NodeTaskGroupType: int case CreateService = 1; case UpdateService = 2; case DeleteService = 3; + case SelfUpgrade = 4; } diff --git a/app/Models/NodeTaskType.php b/app/Models/NodeTaskType.php index 4f1a291..b0f09ef 100644 --- a/app/Models/NodeTaskType.php +++ b/app/Models/NodeTaskType.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Events\NodeTasks\ConfirmAgentUpgrade\ConfirmAgentUpgradeCompleted; +use App\Events\NodeTasks\ConfirmAgentUpgrade\ConfirmAgentUpgradeFailed; use App\Events\NodeTasks\CreateConfig\CreateConfigCompleted; use App\Events\NodeTasks\CreateConfig\CreateConfigFailed; use App\Events\NodeTasks\CreateNetwork\CreateNetworkCompleted; @@ -12,14 +14,20 @@ use App\Events\NodeTasks\CreateService\CreateServiceFailed; use App\Events\NodeTasks\DeleteService\DeleteServiceCompleted; use App\Events\NodeTasks\DeleteService\DeleteServiceFailed; +use App\Events\NodeTasks\DownloadAgentUpgrade\DownloadAgentUpgradeCompleted; +use App\Events\NodeTasks\DownloadAgentUpgrade\DownloadAgentUpgradeFailed; use App\Events\NodeTasks\InitSwarm\InitSwarmCompleted; use App\Events\NodeTasks\InitSwarm\InitSwarmFailed; use App\Events\NodeTasks\RebuildCaddyConfig\ApplyCaddyConfigCompleted; use App\Events\NodeTasks\RebuildCaddyConfig\ApplyCaddyConfigFailed; +use App\Events\NodeTasks\UpdateAgentSymlink\UpdateAgentSymlinkCompleted; +use App\Events\NodeTasks\UpdateAgentSymlink\UpdateAgentSymlinkFailed; use App\Events\NodeTasks\UpdateNode\UpdateCurrentNodeCompleted; use App\Events\NodeTasks\UpdateNode\UpdateCurrentNodeFailed; use App\Events\NodeTasks\UpdateService\UpdateServiceCompleted; use App\Events\NodeTasks\UpdateService\UpdateServiceFailed; +use App\Models\NodeTasks\ConfirmAgentUpgrade\ConfirmAgentUpgradeMeta; +use App\Models\NodeTasks\ConfirmAgentUpgrade\ConfirmAgentUpgradeResult; use App\Models\NodeTasks\CreateConfig\CreateConfigMeta; use App\Models\NodeTasks\CreateConfig\CreateConfigResult; use App\Models\NodeTasks\CreateNetwork\CreateNetworkMeta; @@ -30,10 +38,14 @@ use App\Models\NodeTasks\CreateService\CreateServiceResult; use App\Models\NodeTasks\DeleteService\DeleteServiceMeta; use App\Models\NodeTasks\DeleteService\DeleteServiceResult; +use App\Models\NodeTasks\DownloadAgentUpgrade\DownloadAgentUpgradeMeta; +use App\Models\NodeTasks\DownloadAgentUpgrade\DownloadAgentUpgradeResult; use App\Models\NodeTasks\InitSwarm\InitSwarmMeta; use App\Models\NodeTasks\InitSwarm\InitSwarmResult; use App\Models\NodeTasks\ApplyCaddyConfig\ApplyCaddyConfigMeta; use App\Models\NodeTasks\ApplyCaddyConfig\ApplyCaddyConfigResult; +use App\Models\NodeTasks\UpdateAgentSymlink\UpdateAgentSymlinkMeta; +use App\Models\NodeTasks\UpdateAgentSymlink\UpdateAgentSymlinkResult; use App\Models\NodeTasks\UpdateCurrentNode\UpdateCurrentNodeMeta; use App\Models\NodeTasks\UpdateCurrentNode\UpdateCurrentNodeResult; use App\Models\NodeTasks\UpdateService\UpdateServiceMeta; @@ -51,6 +63,9 @@ enum NodeTaskType: int case UpdateService = 6; case UpdateCurrentNode = 7; case DeleteService = 8; + case DownloadAgentUpgrade = 9; + case UpdateAgentSymlink = 10; + case ConfirmAgentUpgrade = 11; public function meta(): string { @@ -64,6 +79,9 @@ public function meta(): string self::UpdateService => UpdateServiceMeta::class, self::UpdateCurrentNode => UpdateCurrentNodeMeta::class, self::DeleteService => DeleteServiceMeta::class, + self::DownloadAgentUpgrade => DownloadAgentUpgradeMeta::class, + self::UpdateAgentSymlink => UpdateAgentSymlinkMeta::class, + self::ConfirmAgentUpgrade => ConfirmAgentUpgradeMeta::class, }; } @@ -79,6 +97,9 @@ public function result(): string self::UpdateService => UpdateServiceResult::class, self::UpdateCurrentNode => UpdateCurrentNodeResult::class, self::DeleteService => DeleteServiceResult::class, + self::DownloadAgentUpgrade => DownloadAgentUpgradeResult::class, + self::UpdateAgentSymlink => UpdateAgentSymlinkResult::class, + self::ConfirmAgentUpgrade => ConfirmAgentUpgradeResult::class, }; } @@ -94,6 +115,9 @@ public function completed(): string self::UpdateService => UpdateServiceCompleted::class, self::UpdateCurrentNode => UpdateCurrentNodeCompleted::class, self::DeleteService => DeleteServiceCompleted::class, + self::DownloadAgentUpgrade => DownloadAgentUpgradeCompleted::class, + self::UpdateAgentSymlink => UpdateAgentSymlinkCompleted::class, + self::ConfirmAgentUpgrade => ConfirmAgentUpgradeCompleted::class, }; } @@ -109,6 +133,9 @@ public function failed(): string self::UpdateService => UpdateServiceFailed::class, self::UpdateCurrentNode => UpdateCurrentNodeFailed::class, self::DeleteService => DeleteServiceFailed::class, + self::DownloadAgentUpgrade => DownloadAgentUpgradeFailed::class, + self::UpdateAgentSymlink => UpdateAgentSymlinkFailed::class, + self::ConfirmAgentUpgrade => ConfirmAgentUpgradeFailed::class, }; } } diff --git a/app/Models/NodeTasks/ConfirmAgentUpgrade/ConfirmAgentUpgradeMeta.php b/app/Models/NodeTasks/ConfirmAgentUpgrade/ConfirmAgentUpgradeMeta.php new file mode 100644 index 0000000..54d81fc --- /dev/null +++ b/app/Models/NodeTasks/ConfirmAgentUpgrade/ConfirmAgentUpgradeMeta.php @@ -0,0 +1,20 @@ +{$this->targetVersion}"; + } +} diff --git a/app/Models/NodeTasks/ConfirmAgentUpgrade/ConfirmAgentUpgradeResult.php b/app/Models/NodeTasks/ConfirmAgentUpgrade/ConfirmAgentUpgradeResult.php new file mode 100644 index 0000000..9f26eb4 --- /dev/null +++ b/app/Models/NodeTasks/ConfirmAgentUpgrade/ConfirmAgentUpgradeResult.php @@ -0,0 +1,19 @@ +{$this->targetVersion}"; + } +} diff --git a/app/Models/NodeTasks/DownloadAgentUpgrade/DownloadAgentUpgradeResult.php b/app/Models/NodeTasks/DownloadAgentUpgrade/DownloadAgentUpgradeResult.php new file mode 100644 index 0000000..9a0c958 --- /dev/null +++ b/app/Models/NodeTasks/DownloadAgentUpgrade/DownloadAgentUpgradeResult.php @@ -0,0 +1,21 @@ +{$this->fileSize} bytes in {$this->downloadTime} seconds."; + } +} diff --git a/app/Models/NodeTasks/UpdateAgentSymlink/UpdateAgentSymlinkMeta.php b/app/Models/NodeTasks/UpdateAgentSymlink/UpdateAgentSymlinkMeta.php new file mode 100644 index 0000000..552718c --- /dev/null +++ b/app/Models/NodeTasks/UpdateAgentSymlink/UpdateAgentSymlinkMeta.php @@ -0,0 +1,20 @@ +{$this->targetVersion}"; + } +} diff --git a/app/Models/NodeTasks/UpdateAgentSymlink/UpdateAgentSymlinkResult.php b/app/Models/NodeTasks/UpdateAgentSymlink/UpdateAgentSymlinkResult.php new file mode 100644 index 0000000..b202077 --- /dev/null +++ b/app/Models/NodeTasks/UpdateAgentSymlink/UpdateAgentSymlinkResult.php @@ -0,0 +1,19 @@ +withExceptions(function (Exceptions $exceptions) { // - })->create(); + }) + ->withSchedule(function (Schedule $schedule) { + $schedule->job(CheckAgentUpdates::class) + ->everyMinute() + ->withoutOverlapping(); + }) + ->create(); diff --git a/database/factories/AgentReleaseFactory.php b/database/factories/AgentReleaseFactory.php new file mode 100644 index 0000000..f0182a0 --- /dev/null +++ b/database/factories/AgentReleaseFactory.php @@ -0,0 +1,23 @@ + + */ +class AgentReleaseFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + // + ]; + } +} diff --git a/database/migrations/2024_06_29_100400_create_agent_releases_table.php b/database/migrations/2024_06_29_100400_create_agent_releases_table.php new file mode 100644 index 0000000..0745213 --- /dev/null +++ b/database/migrations/2024_06_29_100400_create_agent_releases_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('tag_name'); + $table->string('os'); + $table->string('arch'); + $table->string('download_url'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('agent_releases'); + } +}; diff --git a/resources/js/Pages/Nodes/Partials/AgentStatus.vue b/resources/js/Pages/Nodes/Partials/AgentStatus.vue index f58d31f..2b95b94 100644 --- a/resources/js/Pages/Nodes/Partials/AgentStatus.vue +++ b/resources/js/Pages/Nodes/Partials/AgentStatus.vue @@ -4,10 +4,24 @@ import PrimaryButton from "@/Components/PrimaryButton.vue"; import AgentInstall from "@/Pages/Nodes/Partials/AgentInstall.vue"; import FormSection from "@/Components/FormSection.vue"; import ValueCard from "@/Components/ValueCard.vue"; +import {useForm} from "@inertiajs/vue3"; -defineProps([ - 'node' +const props = defineProps([ + 'node', + 'lastAgentVersion', ]) + +const upgradeAgentForm = useForm({ + targetVersion: props.lastAgentVersion, +}) + +function upgradeAgent() { + upgradeAgentForm.post(route('nodes.upgrade-agent', { + node: props.node.id, + }), { + preserveScroll: true, + }); +} - \ No newline at end of file diff --git a/resources/js/Pages/Nodes/Partials/AgentUpgradeStatus.vue b/resources/js/Pages/Nodes/Partials/AgentUpgradeStatus.vue new file mode 100644 index 0000000..5768bae --- /dev/null +++ b/resources/js/Pages/Nodes/Partials/AgentUpgradeStatus.vue @@ -0,0 +1,26 @@ + + + \ No newline at end of file diff --git a/resources/js/Pages/Nodes/Partials/SwarmDetails.vue b/resources/js/Pages/Nodes/Partials/SwarmDetails.vue index 1b841e3..59d8f0a 100644 --- a/resources/js/Pages/Nodes/Partials/SwarmDetails.vue +++ b/resources/js/Pages/Nodes/Partials/SwarmDetails.vue @@ -15,7 +15,7 @@ import ActionSection from "@/Components/ActionSection.vue"; diff --git a/resources/js/Pages/Nodes/Show.vue b/resources/js/Pages/Nodes/Show.vue index 8d4c325..ff72bc9 100644 --- a/resources/js/Pages/Nodes/Show.vue +++ b/resources/js/Pages/Nodes/Show.vue @@ -6,10 +6,13 @@ import AgentStatus from "@/Pages/Nodes/Partials/AgentStatus.vue"; import SectionBorder from "@/Components/SectionBorder.vue"; import InitSwarmProgress from "@/Pages/Nodes/Partials/InitSwarmProgress.vue"; import SwarmDetails from "@/Pages/Nodes/Partials/SwarmDetails.vue"; +import AgentUpgradeStatus from "@/Pages/Nodes/Partials/AgentUpgradeStatus.vue"; defineProps([ 'node', 'initTaskGroup', + 'lastAgentVersion', + 'agentUpgradeTaskGroup', ]); @@ -19,7 +22,8 @@ defineProps([ - + + diff --git a/routes/web.php b/routes/web.php index 3d3bc74..df64e44 100644 --- a/routes/web.php +++ b/routes/web.php @@ -31,6 +31,8 @@ Route::post('/node-task-groups/{taskGroup}/retry', [NodeTaskGroupController::class, 'retry'])->name('node-task-groups.retry'); Route::resource("nodes", NodeController::class); + Route::post('/nodes/{node}/upgrade-agent', [NodeController::class, 'upgradeAgent'])->name('nodes.upgrade-agent'); + Route::resource("services", ServiceController::class); Route::get('/services/{service}/deployments', [ServiceController::class, 'deployments'])->name('services.deployments'); Route::post('/services/{service}/deployments', [ServiceController::class, 'deploy'])->name('services.deploy');