commit
d8639f58d7
@ -31,7 +31,7 @@ ## Github Sponsors ($15+)
|
||||
<a href="https://github.com/whitesidest"><img src="https://avatars.githubusercontent.com/u/12365916?s=52&v=4" width="60px" alt="Tyler Whitesides" /></a>
|
||||
<a href="https://github.com/aniftyco"><img src="https://github.com/aniftyco.png" width="60px" alt="NiftyCo" /></a>
|
||||
<a href="https://github.com/iujlaki"><img src="https://github.com/iujlaki.png" width="60px" alt="Imre Ujlaki" /></a>
|
||||
<a href="https://github.com/Illyism"><img src="https://github.com/Illyism.png" width="60px" alt="Ilias Ism" /></a>
|
||||
<a href="https://il.ly"><img src="https://github.com/Illyism.png" width="60px" alt="Ilias Ism" /></a>
|
||||
<a href="https://github.com/urtho"><img src="https://github.com/urtho.png" width="60px" alt="Paweł Pierścionek" /></a>
|
||||
<a href="https://github.com/monocursive"><img src="https://github.com/monocursive.png" width="60px" alt="Michael Mazurczak" /></a>
|
||||
|
||||
|
@ -99,7 +99,7 @@ private function alive()
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Http::get("https://undead.coollabs.io/coolify/v4/alive?appId=$id&version=$version");
|
||||
Http::get("https://undead.coolify.io/v4/alive?appId=$id&version=$version");
|
||||
echo "I am alive!\n";
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in alive: {$e->getMessage()}\n";
|
||||
@ -142,83 +142,83 @@ private function cleanup_stucked_resources()
|
||||
try {
|
||||
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($applications as $application) {
|
||||
echo "Deleting stucked application: {$application->name}\n";
|
||||
echo "Deleting stuck application: {$application->name}\n";
|
||||
$application->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stucked application: {$e->getMessage()}\n";
|
||||
echo "Error in cleaning stuck application: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$postgresqls = StandalonePostgresql::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($postgresqls as $postgresql) {
|
||||
echo "Deleting stucked postgresql: {$postgresql->name}\n";
|
||||
echo "Deleting stuck postgresql: {$postgresql->name}\n";
|
||||
$postgresql->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stucked postgresql: {$e->getMessage()}\n";
|
||||
echo "Error in cleaning stuck postgresql: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$redis = StandaloneRedis::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($redis as $redis) {
|
||||
echo "Deleting stucked redis: {$redis->name}\n";
|
||||
echo "Deleting stuck redis: {$redis->name}\n";
|
||||
$redis->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stucked redis: {$e->getMessage()}\n";
|
||||
echo "Error in cleaning stuck redis: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$mongodbs = StandaloneMongodb::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($mongodbs as $mongodb) {
|
||||
echo "Deleting stucked mongodb: {$mongodb->name}\n";
|
||||
echo "Deleting stuck mongodb: {$mongodb->name}\n";
|
||||
$mongodb->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stucked mongodb: {$e->getMessage()}\n";
|
||||
echo "Error in cleaning stuck mongodb: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$mysqls = StandaloneMysql::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($mysqls as $mysql) {
|
||||
echo "Deleting stucked mysql: {$mysql->name}\n";
|
||||
echo "Deleting stuck mysql: {$mysql->name}\n";
|
||||
$mysql->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stucked mysql: {$e->getMessage()}\n";
|
||||
echo "Error in cleaning stuck mysql: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$mariadbs = StandaloneMariadb::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($mariadbs as $mariadb) {
|
||||
echo "Deleting stucked mariadb: {$mariadb->name}\n";
|
||||
echo "Deleting stuck mariadb: {$mariadb->name}\n";
|
||||
$mariadb->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stucked mariadb: {$e->getMessage()}\n";
|
||||
echo "Error in cleaning stuck mariadb: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$services = Service::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($services as $service) {
|
||||
echo "Deleting stucked service: {$service->name}\n";
|
||||
echo "Deleting stuck service: {$service->name}\n";
|
||||
$service->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stucked service: {$e->getMessage()}\n";
|
||||
echo "Error in cleaning stuck service: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$serviceApps = ServiceApplication::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($serviceApps as $serviceApp) {
|
||||
echo "Deleting stucked serviceapp: {$serviceApp->name}\n";
|
||||
echo "Deleting stuck serviceapp: {$serviceApp->name}\n";
|
||||
$serviceApp->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stucked serviceapp: {$e->getMessage()}\n";
|
||||
echo "Error in cleaning stuck serviceapp: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$serviceDbs = ServiceDatabase::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($serviceDbs as $serviceDb) {
|
||||
echo "Deleting stucked serviceapp: {$serviceDb->name}\n";
|
||||
echo "Deleting stuck serviceapp: {$serviceDb->name}\n";
|
||||
$serviceDb->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stucked serviceapp: {$e->getMessage()}\n";
|
||||
echo "Error in cleaning stuck serviceapp: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
// Cleanup any resources that are not attached to any environment or destination or server
|
||||
|
@ -5,12 +5,14 @@
|
||||
use App\Jobs\CheckLogDrainContainerJob;
|
||||
use App\Jobs\CleanupInstanceStuffsJob;
|
||||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Jobs\ScheduledTaskJob;
|
||||
use App\Jobs\InstanceAutoUpdateJob;
|
||||
use App\Jobs\ContainerStatusJob;
|
||||
use App\Jobs\PullHelperImageJob;
|
||||
use App\Jobs\ServerStatusJob;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ScheduledTask;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
@ -30,6 +32,7 @@ protected function schedule(Schedule $schedule): void
|
||||
$this->check_resources($schedule);
|
||||
$this->check_scheduled_backups($schedule);
|
||||
$this->pull_helper_image($schedule);
|
||||
$this->check_scheduled_tasks($schedule);
|
||||
} else {
|
||||
// Instance Jobs
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||
@ -41,6 +44,7 @@ protected function schedule(Schedule $schedule): void
|
||||
$this->check_scheduled_backups($schedule);
|
||||
$this->check_resources($schedule);
|
||||
$this->pull_helper_image($schedule);
|
||||
$this->check_scheduled_tasks($schedule);
|
||||
}
|
||||
}
|
||||
private function pull_helper_image($schedule)
|
||||
@ -107,6 +111,32 @@ private function check_scheduled_backups($schedule)
|
||||
}
|
||||
}
|
||||
|
||||
private function check_scheduled_tasks($schedule) {
|
||||
$scheduled_tasks = ScheduledTask::all();
|
||||
if ($scheduled_tasks->isEmpty()) {
|
||||
ray('no scheduled tasks');
|
||||
return;
|
||||
}
|
||||
foreach ($scheduled_tasks as $scheduled_task) {
|
||||
$service = $scheduled_task->service()->get();
|
||||
$application = $scheduled_task->application()->get();
|
||||
|
||||
if (!$application && !$service) {
|
||||
ray('application/service attached to scheduled task does not exist');
|
||||
$scheduled_task->delete();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
|
||||
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
|
||||
}
|
||||
$schedule->job(new ScheduledTaskJob(
|
||||
task: $scheduled_task
|
||||
))->cron($scheduled_task->frequency)->onOneServer();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected function commands(): void
|
||||
{
|
||||
$this->load(__DIR__ . '/Commands');
|
||||
|
@ -3,14 +3,13 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Events\TestEvent;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\S3Storage;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\TeamInvitation;
|
||||
use App\Models\User;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
@ -35,25 +34,25 @@ public function realtime_test() {
|
||||
public function verify() {
|
||||
return view('auth.verify-email');
|
||||
}
|
||||
public function email_verify() {
|
||||
request()->fulfill();
|
||||
public function email_verify(EmailVerificationRequest $request) {
|
||||
$request->fulfill();
|
||||
$name = request()->user()?->name;
|
||||
send_internal_notification("User {$name} verified their email address.");
|
||||
return redirect(RouteServiceProvider::HOME);
|
||||
}
|
||||
public function forgot_password() {
|
||||
public function forgot_password(Request $request) {
|
||||
if (is_transactional_emails_active()) {
|
||||
$arrayOfRequest = request()->only(Fortify::email());
|
||||
request()->merge([
|
||||
$arrayOfRequest = $request->only(Fortify::email());
|
||||
$request->merge([
|
||||
'email' => Str::lower($arrayOfRequest['email']),
|
||||
]);
|
||||
$type = set_transanctional_email_settings();
|
||||
if (!$type) {
|
||||
return response()->json(['message' => 'Transactional emails are not active'], 400);
|
||||
}
|
||||
request()->validate([Fortify::email() => 'required|email']);
|
||||
$request->validate([Fortify::email() => 'required|email']);
|
||||
$status = Password::broker(config('fortify.passwords'))->sendResetLink(
|
||||
request()->only(Fortify::email())
|
||||
$request->only(Fortify::email())
|
||||
);
|
||||
if ($status == Password::RESET_LINK_SENT) {
|
||||
return app(SuccessfulPasswordResetLinkRequestResponse::class, ['status' => $status]);
|
||||
|
@ -911,16 +911,18 @@ private function generate_nixpacks_confs()
|
||||
$parsed = Toml::Parse($this->nixpacks_plan);
|
||||
// Do any modifications here
|
||||
$cmds = collect(data_get($parsed, 'phases.setup.cmds', []));
|
||||
$this->generate_env_variables();
|
||||
data_set($parsed, 'phases.setup.cmds', $cmds);
|
||||
$this->nixpacks_plan = json_encode($parsed);
|
||||
$merged_envs = $this->env_args->merge(collect(data_get($parsed, 'variables', [])));
|
||||
data_set($parsed, 'variables', $merged_envs->toArray());
|
||||
$this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function nixpacks_build_cmd()
|
||||
{
|
||||
$this->generate_env_variables();
|
||||
$nixpacks_command = "nixpacks plan -f toml {$this->env_args}";
|
||||
$nixpacks_command = "nixpacks plan -f toml";
|
||||
if ($this->application->build_command) {
|
||||
$nixpacks_command .= " --build-cmd \"{$this->application->build_command}\"";
|
||||
}
|
||||
@ -939,21 +941,20 @@ private function generate_env_variables()
|
||||
$this->env_args = collect([]);
|
||||
if ($this->pull_request_id === 0) {
|
||||
foreach ($this->application->nixpacks_environment_variables as $env) {
|
||||
$this->env_args->push("--env {$env->key}={$env->value}");
|
||||
$this->env_args->put($env->key, $env->value);
|
||||
}
|
||||
foreach ($this->application->build_environment_variables as $env) {
|
||||
$this->env_args->push("--env {$env->key}={$env->value}");
|
||||
$this->env_args->put($env->key, $env->value);
|
||||
}
|
||||
} else {
|
||||
foreach ($this->application->nixpacks_environment_variables_preview as $env) {
|
||||
$this->env_args->push("--env {$env->key}={$env->value}");
|
||||
$this->env_args->put($env->key, $env->value);
|
||||
}
|
||||
foreach ($this->application->build_environment_variables_preview as $env) {
|
||||
$this->env_args->push("--env {$env->key}={$env->value}");
|
||||
$this->env_args->put($env->key, $env->value);
|
||||
}
|
||||
}
|
||||
|
||||
$this->env_args = $this->env_args->implode(' ');
|
||||
// $this->env_args = $this->env_args->implode(' ');
|
||||
}
|
||||
|
||||
private function generate_compose_file()
|
||||
@ -1275,26 +1276,33 @@ private function build_image()
|
||||
} else {
|
||||
if ($this->application->build_pack === 'nixpacks') {
|
||||
$this->nixpacks_plan = base64_encode($this->nixpacks_plan);
|
||||
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d > {$this->workdir}/thegameplan.json"), "hidden" => true]);
|
||||
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d > /artifacts/thegameplan.json"), "hidden" => true]);
|
||||
if ($this->force_rebuild) {
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "nixpacks build -c thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir}"), "hidden" => true
|
||||
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir}"), "hidden" => true
|
||||
]);
|
||||
} else {
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "nixpacks build -c thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir}"), "hidden" => true
|
||||
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir}"), "hidden" => true
|
||||
]);
|
||||
}
|
||||
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "rm /artifacts/thegameplan.json"), "hidden" => true]);
|
||||
} else {
|
||||
if ($this->force_rebuild) {
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
|
||||
]);
|
||||
$build_command = "docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}";
|
||||
$base64_build_command = base64_encode($build_command);
|
||||
} else {
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
|
||||
]);
|
||||
$build_command = "docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}";
|
||||
$base64_build_command = base64_encode($build_command);
|
||||
}
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d > /artifacts/build.sh"), "hidden" => true
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "bash /artifacts/build.sh"), "hidden" => true
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$dockerfile = base64_encode("FROM {$this->application->static_image}
|
||||
@ -1320,6 +1328,8 @@ private function build_image()
|
||||
}
|
||||
}");
|
||||
}
|
||||
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
|
||||
$base64_build_command = base64_encode($build_command);
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '{$dockerfile}' | base64 -d > {$this->workdir}/Dockerfile")
|
||||
@ -1328,38 +1338,55 @@ private function build_image()
|
||||
executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf")
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"), "hidden" => true
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d > /artifacts/build.sh"), "hidden" => true
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "bash /artifacts/build.sh"), "hidden" => true
|
||||
]
|
||||
);
|
||||
} else {
|
||||
// Pure Dockerfile based deployment
|
||||
if ($this->application->dockerfile) {
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "docker build --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"), "hidden" => true
|
||||
]);
|
||||
$build_command = "docker build --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
|
||||
$base64_build_command = base64_encode($build_command);
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d > /artifacts/build.sh"), "hidden" => true
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "bash /artifacts/build.sh"), "hidden" => true
|
||||
]
|
||||
);
|
||||
} else {
|
||||
if ($this->application->build_pack === 'nixpacks') {
|
||||
$this->nixpacks_plan = base64_encode($this->nixpacks_plan);
|
||||
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d > {$this->workdir}/thegameplan.json"), "hidden" => true]);
|
||||
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d > /artifacts/thegameplan.json"), "hidden" => true]);
|
||||
if ($this->force_rebuild) {
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "nixpacks build -c thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir}"), "hidden" => true
|
||||
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir}"), "hidden" => true
|
||||
]);
|
||||
} else {
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "nixpacks build -c thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir}"), "hidden" => true
|
||||
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir}"), "hidden" => true
|
||||
]);
|
||||
}
|
||||
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "rm /artifacts/thegameplan.json"), "hidden" => true]);
|
||||
} else {
|
||||
if ($this->force_rebuild) {
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "docker build --no-cache {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"), "hidden" => true
|
||||
]);
|
||||
$build_command = "docker build --no-cache {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
|
||||
$base64_build_command = base64_encode($build_command);
|
||||
} else {
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "docker build {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"), "hidden" => true
|
||||
]);
|
||||
$build_command = "docker build {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
|
||||
$base64_build_command = base64_encode($build_command);
|
||||
}
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d > /artifacts/build.sh"), "hidden" => true
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "bash /artifacts/build.sh"), "hidden" => true
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1439,11 +1466,13 @@ private function generate_build_env_variables()
|
||||
$this->build_args = collect(["--build-arg SOURCE_COMMIT=\"{$this->commit}\""]);
|
||||
if ($this->pull_request_id === 0) {
|
||||
foreach ($this->application->build_environment_variables as $env) {
|
||||
$this->build_args->push("--build-arg {$env->key}=\"{$env->value}\"");
|
||||
$value = escapeshellarg($env->value);
|
||||
$this->build_args->push("--build-arg {$env->key}={$value}");
|
||||
}
|
||||
} else {
|
||||
foreach ($this->application->build_environment_variables_preview as $env) {
|
||||
$this->build_args->push("--build-arg {$env->key}=\"{$env->value}\"");
|
||||
$value = escapeshellarg($env->value);
|
||||
$this->build_args->push("--build-arg {$env->key}={$value}");
|
||||
}
|
||||
}
|
||||
|
||||
|
115
app/Jobs/ScheduledTaskJob.php
Normal file
115
app/Jobs/ScheduledTaskJob.php
Normal file
@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\ScheduledTask;
|
||||
use App\Models\ScheduledTaskExecution;
|
||||
use App\Models\Server;
|
||||
use App\Models\Application;
|
||||
use App\Models\Service;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Collection;
|
||||
use Throwable;
|
||||
|
||||
class ScheduledTaskJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public ?Team $team = null;
|
||||
public Server $server;
|
||||
public ScheduledTask $task;
|
||||
public Application|Service $resource;
|
||||
|
||||
public ?ScheduledTaskExecution $task_log = null;
|
||||
public string $task_status = 'failed';
|
||||
public ?string $task_output = null;
|
||||
public array $containers = [];
|
||||
|
||||
public function __construct($task)
|
||||
{
|
||||
$this->task = $task;
|
||||
if ($service = $task->service()->first()) {
|
||||
$this->resource = $service;
|
||||
} else if ($application = $task->application()->first()) {
|
||||
$this->resource = $application;
|
||||
}
|
||||
$this->team = Team::find($task->team_id);
|
||||
}
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [new WithoutOverlapping($this->task->id)];
|
||||
}
|
||||
|
||||
public function uniqueId(): int
|
||||
{
|
||||
return $this->task->id;
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$this->task_log = ScheduledTaskExecution::create([
|
||||
'scheduled_task_id' => $this->task->id,
|
||||
]);
|
||||
|
||||
$this->server = $this->resource->destination->server;
|
||||
|
||||
if ($this->resource->type() == 'application') {
|
||||
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0);
|
||||
if ($containers->count() > 0) {
|
||||
$containers->each(function ($container) {
|
||||
$this->containers[] = str_replace('/', '', $container['Names']);
|
||||
});
|
||||
}
|
||||
}
|
||||
elseif ($this->resource->type() == 'service') {
|
||||
$this->resource->applications()->get()->each(function ($application) {
|
||||
if (str(data_get($application, 'status'))->contains('running')) {
|
||||
$this->containers[] = data_get($application, 'name') . '-' . data_get($this->resource, 'uuid');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (count($this->containers) == 0) {
|
||||
throw new \Exception('ScheduledTaskJob failed: No containers running.');
|
||||
}
|
||||
|
||||
if (count($this->containers) > 1 && empty($this->task->container)) {
|
||||
throw new \Exception('ScheduledTaskJob failed: More than one container exists but no container name was provided.');
|
||||
}
|
||||
|
||||
foreach ($this->containers as $containerName) {
|
||||
if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container . '-' . $this->resource->uuid)) {
|
||||
$cmd = 'sh -c "' . str_replace('"', '\"', $this->task->command) . '"';
|
||||
$exec = "docker exec {$containerName} {$cmd}";
|
||||
$this->task_output = instant_remote_process([$exec], $this->server, true);
|
||||
$this->task_log->update([
|
||||
'status' => 'success',
|
||||
'message' => $this->task_output,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No valid container was found.
|
||||
throw new \Exception('ScheduledTaskJob failed: No valid container was found. Is the container name correct?');
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
if ($this->task_log) {
|
||||
$this->task_log->update([
|
||||
'status' => 'failed',
|
||||
'message' => $this->task_output ?? $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
send_internal_notification('ScheduledTaskJob failed with: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
@ -209,7 +209,7 @@ public function resetDefaultLabels($showToaster = true)
|
||||
public function updatedApplicationFqdn()
|
||||
{
|
||||
$this->resetDefaultLabels(false);
|
||||
$this->dispatch('success', 'Labels reseted to default!');
|
||||
$this->dispatch('success', 'Labels reset to default!');
|
||||
}
|
||||
public function submit($showToaster = true)
|
||||
{
|
||||
|
139
app/Livewire/Project/Database/Import.php
Normal file
139
app/Livewire/Project/Database/Import.php
Normal file
@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
use Exception;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class Import extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public $file;
|
||||
public $resource;
|
||||
public $parameters;
|
||||
public $containers;
|
||||
public bool $validated = true;
|
||||
public bool $scpInProgress = false;
|
||||
public bool $importRunning = false;
|
||||
public string $validationMsg = '';
|
||||
public Server $server;
|
||||
public string $container;
|
||||
public array $importCommands = [];
|
||||
public string $postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d $POSTGRES_DB';
|
||||
public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p $MYSQL_PASSWORD $MYSQL_DATABASE';
|
||||
public string $mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p $MARIADB_PASSWORD $MARIADB_DATABASE';
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$userId = auth()->user()->id;
|
||||
return [
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
];
|
||||
}
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->getContainers();
|
||||
}
|
||||
|
||||
public function getContainers()
|
||||
{
|
||||
$this->containers = collect();
|
||||
if (!data_get($this->parameters, 'database_uuid')) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->first();
|
||||
if (is_null($resource)) {
|
||||
$resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first();
|
||||
if (is_null($resource)) {
|
||||
$resource = StandaloneMongodb::where('uuid', $this->parameters['database_uuid'])->first();
|
||||
if (is_null($resource)) {
|
||||
$resource = StandaloneMysql::where('uuid', $this->parameters['database_uuid'])->first();
|
||||
if (is_null($resource)) {
|
||||
$resource = StandaloneMariadb::where('uuid', $this->parameters['database_uuid'])->first();
|
||||
if (is_null($resource)) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->resource = $resource;
|
||||
$this->server = $this->resource->destination->server;
|
||||
$this->container = $this->resource->uuid;
|
||||
if (str(data_get($this, 'resource.status'))->startsWith('running')) {
|
||||
$this->containers->push($this->container);
|
||||
}
|
||||
|
||||
if ($this->containers->count() > 1) {
|
||||
$this->validated = false;
|
||||
$this->validationMsg = 'The database service has more than one container running. Cannot import.';
|
||||
}
|
||||
|
||||
if (
|
||||
$this->resource->getMorphClass() == 'App\Models\StandaloneRedis'
|
||||
|| $this->resource->getMorphClass() == 'App\Models\StandaloneMongodb'
|
||||
) {
|
||||
$this->validated = false;
|
||||
$this->validationMsg = 'This database type is not currently supported.';
|
||||
}
|
||||
}
|
||||
|
||||
public function runImport()
|
||||
{
|
||||
$this->validate([
|
||||
'file' => 'required|file|max:102400'
|
||||
]);
|
||||
|
||||
$this->importRunning = true;
|
||||
$this->scpInProgress = true;
|
||||
|
||||
try {
|
||||
$uploadedFilename = $this->file->store('backup-import');
|
||||
$path = Storage::path($uploadedFilename);
|
||||
$tmpPath = '/tmp/' . basename($uploadedFilename);
|
||||
|
||||
// SCP the backup file to the server.
|
||||
instant_scp($path, $tmpPath, $this->server);
|
||||
$this->scpInProgress = false;
|
||||
|
||||
$this->importCommands[] = "docker cp {$tmpPath} {$this->container}:{$tmpPath}";
|
||||
|
||||
switch ($this->resource->getMorphClass()) {
|
||||
case 'App\Models\StandaloneMariadb':
|
||||
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mariadbRestoreCommand} < {$tmpPath}'";
|
||||
$this->importCommands[] = "rm {$tmpPath}";
|
||||
break;
|
||||
case 'App\Models\StandaloneMysql':
|
||||
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mysqlRestoreCommand} < {$tmpPath}'";
|
||||
$this->importCommands[] = "rm {$tmpPath}";
|
||||
break;
|
||||
case 'App\Models\StandalonePostgresql':
|
||||
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->postgresqlRestoreCommand} {$tmpPath}'";
|
||||
$this->importCommands[] = "rm {$tmpPath}";
|
||||
break;
|
||||
}
|
||||
|
||||
$this->importCommands[] = "docker exec {$this->container} sh -c 'rm {$tmpPath}'";
|
||||
$this->importCommands[] = "docker exec {$this->container} sh -c 'echo \"Import finished with exit code $?\"'";
|
||||
|
||||
if (!empty($this->importCommands)) {
|
||||
$activity = remote_process($this->importCommands, $this->server, ignore_errors: true);
|
||||
$this->dispatch('newMonitorActivity', $activity->id);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->validated = false;
|
||||
$this->validationMsg = $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
58
app/Livewire/Project/Shared/ScheduledTask/Add.php
Normal file
58
app/Livewire/Project/Shared/ScheduledTask/Add.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Project\Shared\ScheduledTask;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class Add extends Component
|
||||
{
|
||||
public $parameters;
|
||||
public string $name;
|
||||
public string $command;
|
||||
public string $frequency;
|
||||
public ?string $container = '';
|
||||
|
||||
protected $listeners = ['clearScheduledTask' => 'clear'];
|
||||
protected $rules = [
|
||||
'name' => 'required|string',
|
||||
'command' => 'required|string',
|
||||
'frequency' => 'required|string',
|
||||
'container' => 'nullable|string',
|
||||
];
|
||||
protected $validationAttributes = [
|
||||
'name' => 'name',
|
||||
'command' => 'command',
|
||||
'frequency' => 'frequency',
|
||||
'container' => 'container',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$this->validate();
|
||||
$isValid = validate_cron_expression($this->frequency);
|
||||
if (!$isValid) {
|
||||
$this->dispatch('error', 'Invalid Cron / Human expression.');
|
||||
return;
|
||||
}
|
||||
$this->dispatch('saveScheduledTask', [
|
||||
'name' => $this->name,
|
||||
'command' => $this->command,
|
||||
'frequency' => $this->frequency,
|
||||
'container' => $this->container,
|
||||
]);
|
||||
$this->clear();
|
||||
}
|
||||
|
||||
public function clear()
|
||||
{
|
||||
$this->name = '';
|
||||
$this->command = '';
|
||||
$this->frequency = '';
|
||||
$this->container = '';
|
||||
}
|
||||
}
|
56
app/Livewire/Project/Shared/ScheduledTask/All.php
Normal file
56
app/Livewire/Project/Shared/ScheduledTask/All.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Project\Shared\ScheduledTask;
|
||||
|
||||
use App\Models\ScheduledTask;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class All extends Component
|
||||
{
|
||||
public $resource;
|
||||
public string|null $modalId = null;
|
||||
public ?string $variables = null;
|
||||
public array $parameters;
|
||||
protected $listeners = ['refreshTasks', 'saveScheduledTask' => 'submit'];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->modalId = new Cuid2(7);
|
||||
}
|
||||
public function refreshTasks()
|
||||
{
|
||||
$this->resource->refresh();
|
||||
}
|
||||
|
||||
public function submit($data)
|
||||
{
|
||||
try {
|
||||
$task = new ScheduledTask();
|
||||
$task->name = $data['name'];
|
||||
$task->command = $data['command'];
|
||||
$task->frequency = $data['frequency'];
|
||||
$task->container = $data['container'];
|
||||
$task->team_id = currentTeam()->id;
|
||||
|
||||
switch ($this->resource->type()) {
|
||||
case 'application':
|
||||
$task->application_id = $this->resource->id;
|
||||
break;
|
||||
case 'standalone-postgresql':
|
||||
$task->standalone_postgresql_id = $this->resource->id;
|
||||
break;
|
||||
case 'service':
|
||||
$task->service_id = $this->resource->id;
|
||||
break;
|
||||
}
|
||||
$task->save();
|
||||
$this->refreshTasks();
|
||||
$this->dispatch('success', 'Scheduled task added successfully.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
27
app/Livewire/Project/Shared/ScheduledTask/Executions.php
Normal file
27
app/Livewire/Project/Shared/ScheduledTask/Executions.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Project\Shared\ScheduledTask;
|
||||
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Component;
|
||||
|
||||
class Executions extends Component
|
||||
{
|
||||
public $executions = [];
|
||||
public $selectedKey;
|
||||
public function getListeners()
|
||||
{
|
||||
return [
|
||||
"selectTask",
|
||||
];
|
||||
}
|
||||
|
||||
public function selectTask($key): void
|
||||
{
|
||||
if ($key == $this->selectedKey) {
|
||||
$this->selectedKey = null;
|
||||
return;
|
||||
}
|
||||
$this->selectedKey = $key;
|
||||
}
|
||||
}
|
72
app/Livewire/Project/Shared/ScheduledTask/Show.php
Normal file
72
app/Livewire/Project/Shared/ScheduledTask/Show.php
Normal file
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Project\Shared\ScheduledTask;
|
||||
|
||||
use App\Models\ScheduledTask as ModelsScheduledTask;
|
||||
use Livewire\Component;
|
||||
use App\Models\Application;
|
||||
use App\Models\Service;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Show extends Component
|
||||
{
|
||||
public $parameters;
|
||||
public Application|Service $resource;
|
||||
public ModelsScheduledTask $task;
|
||||
public ?string $modalId = null;
|
||||
public string $type;
|
||||
|
||||
protected $rules = [
|
||||
'task.name' => 'required|string',
|
||||
'task.command' => 'required|string',
|
||||
'task.frequency' => 'required|string',
|
||||
'task.container' => 'nullable|string',
|
||||
];
|
||||
protected $validationAttributes = [
|
||||
'name' => 'name',
|
||||
'command' => 'command',
|
||||
'frequency' => 'frequency',
|
||||
'container' => 'container',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
|
||||
if (data_get($this->parameters, 'application_uuid')) {
|
||||
$this->type = 'application';
|
||||
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
|
||||
} else if (data_get($this->parameters, 'service_uuid')) {
|
||||
$this->type = 'service';
|
||||
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
||||
}
|
||||
|
||||
$this->modalId = new Cuid2(7);
|
||||
$this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first();
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$this->validate();
|
||||
$this->task->save();
|
||||
$this->dispatch('success', 'Scheduled task updated successfully.');
|
||||
$this->dispatch('refreshTasks');
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
try {
|
||||
$this->task->delete();
|
||||
|
||||
if ($this->type == 'application') {
|
||||
return redirect()->route('project.application.configuration', $this->parameters);
|
||||
}
|
||||
else {
|
||||
return redirect()->route('project.service.configuration', $this->parameters);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
}
|
@ -315,6 +315,11 @@ public function nixpacks_environment_variables_preview(): HasMany
|
||||
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->where('key', 'like', 'NIXPACKS_%');
|
||||
}
|
||||
|
||||
public function scheduled_tasks(): HasMany
|
||||
{
|
||||
return $this->hasMany(ScheduledTask::class)->orderBy('name', 'asc');
|
||||
}
|
||||
|
||||
public function private_key()
|
||||
{
|
||||
return $this->belongsTo(PrivateKey::class);
|
||||
|
28
app/Models/ScheduledTask.php
Normal file
28
app/Models/ScheduledTask.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
class ScheduledTask extends BaseModel
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
public function service()
|
||||
{
|
||||
return $this->belongsTo(Service::class);
|
||||
}
|
||||
public function application()
|
||||
{
|
||||
return $this->belongsTo(Application::class);
|
||||
}
|
||||
public function latest_log(): HasOne
|
||||
{
|
||||
return $this->hasOne(ScheduledTaskExecution::class)->latest();
|
||||
}
|
||||
public function executions(): HasMany
|
||||
{
|
||||
return $this->hasMany(ScheduledTaskExecution::class);
|
||||
}
|
||||
}
|
15
app/Models/ScheduledTaskExecution.php
Normal file
15
app/Models/ScheduledTaskExecution.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ScheduledTaskExecution extends BaseModel
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
public function scheduledTask(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ScheduledTask::class);
|
||||
}
|
||||
}
|
@ -358,10 +358,10 @@ public function isLogDrainEnabled()
|
||||
public function validateOS(): bool | Stringable
|
||||
{
|
||||
$os_release = instant_remote_process(['cat /etc/os-release'], $this);
|
||||
$datas = collect(explode("\n", $os_release));
|
||||
$releaseLines = collect(explode("\n", $os_release));
|
||||
$collectedData = collect([]);
|
||||
foreach ($datas as $data) {
|
||||
$item = Str::of($data)->trim();
|
||||
foreach ($releaseLines as $line) {
|
||||
$item = Str::of($line)->trim();
|
||||
$collectedData->put($item->before('=')->value(), $item->after('=')->lower()->replace('"', '')->value());
|
||||
}
|
||||
$ID = data_get($collectedData, 'ID');
|
||||
|
@ -396,6 +396,10 @@ public function byName(string $name)
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public function scheduled_tasks(): HasMany
|
||||
{
|
||||
return $this->hasMany(ScheduledTask::class)->orderBy('name', 'asc');
|
||||
}
|
||||
public function environment_variables(): HasMany
|
||||
{
|
||||
return $this->hasMany(EnvironmentVariable::class)->orderBy('key', 'asc');
|
||||
|
@ -14,8 +14,8 @@ public function send(SendsEmail $notifiable, Notification $notification): void
|
||||
{
|
||||
try {
|
||||
$this->bootConfigs($notifiable);
|
||||
$recepients = $notifiable->getRecepients($notification);
|
||||
if (count($recepients) === 0) {
|
||||
$recipients = $notifiable->getRecepients($notification);
|
||||
if (count($recipients) === 0) {
|
||||
throw new Exception('No email recipients found');
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ public function send(SendsEmail $notifiable, Notification $notification): void
|
||||
[],
|
||||
[],
|
||||
fn (Message $message) => $message
|
||||
->to($recepients)
|
||||
->to($recipients)
|
||||
->subject($mailMessage->subject)
|
||||
->html((string)$mailMessage->render())
|
||||
);
|
||||
@ -35,8 +35,8 @@ public function send(SendsEmail $notifiable, Notification $notification): void
|
||||
}
|
||||
ray($e->getMessage());
|
||||
$message = "EmailChannel error: {$e->getMessage()}. Failed to send email to:";
|
||||
if (isset($recepients)) {
|
||||
$message .= implode(', ', $recepients);
|
||||
if (isset($recipients)) {
|
||||
$message .= implode(', ', $recipients);
|
||||
}
|
||||
if (isset($mailMessage)) {
|
||||
$message .= " with subject: {$mailMessage->subject}";
|
||||
|
@ -67,6 +67,47 @@ function savePrivateKeyToFs(Server $server)
|
||||
return $location;
|
||||
}
|
||||
|
||||
function generateScpCommand(Server $server, string $source, string $dest)
|
||||
{
|
||||
$user = $server->user;
|
||||
$port = $server->port;
|
||||
$privateKeyLocation = savePrivateKeyToFs($server);
|
||||
$timeout = config('constants.ssh.command_timeout');
|
||||
$connectionTimeout = config('constants.ssh.connection_timeout');
|
||||
$serverInterval = config('constants.ssh.server_interval');
|
||||
|
||||
$scp_command = "timeout $timeout scp ";
|
||||
$scp_command .= "-i {$privateKeyLocation} "
|
||||
. '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
|
||||
. '-o PasswordAuthentication=no '
|
||||
. "-o ConnectTimeout=$connectionTimeout "
|
||||
. "-o ServerAliveInterval=$serverInterval "
|
||||
. '-o RequestTTY=no '
|
||||
. '-o LogLevel=ERROR '
|
||||
. "-P {$port} "
|
||||
. "{$source} "
|
||||
. "{$user}@{$server->ip}:{$dest}";
|
||||
|
||||
return $scp_command;
|
||||
}
|
||||
function instant_scp(string $source, string $dest, Server $server, $throwError = true)
|
||||
{
|
||||
$timeout = config('constants.ssh.command_timeout');
|
||||
$scp_command = generateScpCommand($server, $source, $dest);
|
||||
$process = Process::timeout($timeout)->run($scp_command);
|
||||
$output = trim($process->output());
|
||||
$exitCode = $process->exitCode();
|
||||
if ($exitCode !== 0) {
|
||||
if (!$throwError) {
|
||||
return null;
|
||||
}
|
||||
return excludeCertainErrors($process->errorOutput(), $exitCode);
|
||||
}
|
||||
if ($output === 'null') {
|
||||
$output = null;
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
function generateSshCommand(Server $server, string $command, bool $isMux = true)
|
||||
{
|
||||
$user = $server->user;
|
||||
|
@ -53,7 +53,9 @@
|
||||
|
||||
'temporary_file_upload' => [
|
||||
'disk' => null, // Example: 'local', 's3' | Default: 'default'
|
||||
'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
|
||||
'rules' => [ // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
|
||||
'file', 'max:256000'
|
||||
],
|
||||
'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp'
|
||||
'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
|
||||
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
// The release version of your application
|
||||
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
|
||||
'release' => '4.0.0-beta.185',
|
||||
'release' => '4.0.0-beta.186',
|
||||
// When left empty or `null` the Laravel environment will be used
|
||||
'environment' => config('app.env'),
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
<?php
|
||||
|
||||
return '4.0.0-beta.185';
|
||||
return '4.0.0-beta.186';
|
||||
|
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('scheduled_tasks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->boolean('enabled')->default(true);
|
||||
$table->string('name');
|
||||
$table->string('command');
|
||||
$table->string('frequency');
|
||||
$table->string('container')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreignId('application_id')->nullable();
|
||||
$table->foreignId('service_id')->nullable();
|
||||
$table->foreignId('team_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('scheduled_tasks');
|
||||
}
|
||||
};
|
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('scheduled_task_executions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->enum('status', ['success', 'failed', 'running'])->default('running');
|
||||
$table->longText('message')->nullable();
|
||||
$table->foreignId('scheduled_task_id');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('scheduled_task_executions');
|
||||
}
|
||||
};
|
@ -37,3 +37,7 @@ RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
|
||||
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
|
||||
;fi"
|
||||
|
||||
RUN { \
|
||||
echo 'upload_max_filesize=256M'; \
|
||||
echo 'post_max_size=256M'; \
|
||||
} > /etc/php/current_version/cli/conf.d/upload-limits.ini
|
@ -62,3 +62,8 @@ RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
|
||||
echo 'arm64' && \
|
||||
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
|
||||
;fi"
|
||||
|
||||
RUN { \
|
||||
echo 'upload_max_filesize=256M'; \
|
||||
echo 'post_max_size=256M'; \
|
||||
} > /etc/php/current_version/cli/conf.d/upload-limits.ini
|
@ -302,7 +302,7 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap
|
||||
</div>
|
||||
<div class="mt-1 text-base leading-7 text-gray-300">
|
||||
Once you connected your server, Coolify will start managing it and do a
|
||||
lot of adminstrative tasks for you. You can also write your own scripts to
|
||||
lot of administrative tasks for you. You can also write your own scripts to
|
||||
automate your server<span class="text-warning">*</span>.
|
||||
</div>
|
||||
</div>
|
||||
@ -384,7 +384,7 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap
|
||||
<div class="text-2xl font-semibold text-white">Powerful API</div>
|
||||
</div>
|
||||
<div class="mt-1 text-base leading-7 text-gray-300">
|
||||
Programatically deploy, query, and manage your servers & resources.
|
||||
Programmatically deploy, query, and manage your servers & resources.
|
||||
Integrate to your CI/CD pipelines, or build your own custom integrations. <span
|
||||
class="text-warning">*</span>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<nav class="flex pt-2 pb-10">
|
||||
<ol class="flex items-center">
|
||||
<li class="inline-flex items-center">
|
||||
<a wire:nagivate class="text-xs truncate lg:text-sm"
|
||||
<a wire:navigate class="text-xs truncate lg:text-sm"
|
||||
href="{{ route('project.show', ['project_uuid' => $this->parameters['project_uuid']]) }}">
|
||||
{{ data_get($resource, 'environment.project.name', 'Undefined Name') }}</a>
|
||||
</li>
|
||||
|
@ -1,5 +1,6 @@
|
||||
<x-emails.layout>
|
||||
Connection could not be establised with one of your S3 Storage ({{ $name }}). Please fix it [here]({{ $url }}).
|
||||
|
||||
Connection could not be established with one of your S3 Storage ({{ $name }}). Please fix it [here]({{ $url }}).
|
||||
|
||||
{{ $reason }}
|
||||
</x-emails.layout>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div>
|
||||
<dialog id="sendTestEmail" class="modal">
|
||||
<form method="dialog" class="flex flex-col gap-2 rounded modal-box" wire:submit='submit'>
|
||||
<x-forms.input placeholder="test@example.com" id="emails" label="Recepients" required />
|
||||
<x-forms.input placeholder="test@example.com" id="emails" label="Recipients" required />
|
||||
<x-forms.button onclick="sendTestEmail.close()" wire:click="sendTestNotification">
|
||||
Send Email
|
||||
</x-forms.button>
|
||||
|
@ -18,7 +18,9 @@
|
||||
href="#">Environment
|
||||
Variables</a>
|
||||
@endif
|
||||
|
||||
<a :class="activeTab === 'scheduled-tasks' && 'text-white'"
|
||||
@click.prevent="activeTab = 'scheduled-tasks'; window.location.hash = 'scheduled-tasks'" href="#">Scheduled Tasks
|
||||
</a>
|
||||
@if ($application->git_based())
|
||||
<a :class="activeTab === 'source' && 'text-white'"
|
||||
@click.prevent="activeTab = 'source'; window.location.hash = 'source'" href="#">Source</a>
|
||||
@ -54,6 +56,7 @@
|
||||
href="#">Resource Limits
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<a :class="activeTab === 'danger' && 'text-white'"
|
||||
@click.prevent="activeTab = 'danger'; window.location.hash = 'danger'" href="#">Danger Zone
|
||||
</a>
|
||||
@ -97,6 +100,9 @@
|
||||
<div x-cloak x-show="activeTab === 'resource-limits'">
|
||||
<livewire:project.shared.resource-limits :resource="$application" />
|
||||
</div>
|
||||
<div x-cloak x-show="activeTab === 'scheduled-tasks'">
|
||||
<livewire:project.shared.scheduled-task.all :resource="$application" />
|
||||
</div>
|
||||
<div x-cloak x-show="activeTab === 'danger'">
|
||||
<livewire:project.shared.danger :resource="$application" />
|
||||
</div>
|
||||
|
@ -7,7 +7,7 @@
|
||||
</x-forms.button>
|
||||
</div>
|
||||
@isset($rate_limit_remaining)
|
||||
<div class="pt-1 ">Requests remaning till rate limited by Git: {{ $rate_limit_remaining }}</div>
|
||||
<div class="pt-1 ">Requests remaining till rate limited by Git: {{ $rate_limit_remaining }}</div>
|
||||
@endisset
|
||||
@if (count($pull_requests) > 0)
|
||||
<div wire:loading.remove wire:target='load_prs'>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div>
|
||||
<h1>Configuration</h1>
|
||||
<livewire:project.database.heading :database="$database" />
|
||||
<x-modal modalId="startDatabase">
|
||||
<x-modal modalId="startDatabase" noSubmit>
|
||||
<x-slot:modalBody>
|
||||
<livewire:activity-monitor header="Database Startup Logs" />
|
||||
</x-slot:modalBody>
|
||||
@ -31,6 +31,11 @@
|
||||
window.location.hash = 'storages'"
|
||||
href="#">Storages
|
||||
</a>
|
||||
<a :class="activeTab === 'import' && 'text-white'"
|
||||
@click.prevent="activeTab = 'import';
|
||||
window.location.hash = 'import'" href="#">Import
|
||||
Backup
|
||||
</a>
|
||||
<a :class="activeTab === 'webhooks' && 'text-white'"
|
||||
@click.prevent="activeTab = 'webhooks'; window.location.hash = 'webhooks'" href="#">Webhooks
|
||||
</a>
|
||||
@ -39,6 +44,7 @@
|
||||
window.location.hash = 'resource-limits'"
|
||||
href="#">Resource Limits
|
||||
</a>
|
||||
|
||||
<a :class="activeTab === 'danger' && 'text-white'"
|
||||
@click.prevent="activeTab = 'danger';
|
||||
window.location.hash = 'danger'"
|
||||
@ -74,6 +80,9 @@
|
||||
<div x-cloak x-show="activeTab === 'resource-limits'">
|
||||
<livewire:project.shared.resource-limits :resource="$database" />
|
||||
</div>
|
||||
<div x-cloak x-show="activeTab === 'import'">
|
||||
<livewire:project.database.import :resource="$database" />
|
||||
</div>
|
||||
<div x-cloak x-show="activeTab === 'danger'">
|
||||
<livewire:project.shared.danger :resource="$database" />
|
||||
</div>
|
||||
|
60
resources/views/livewire/project/database/import.blade.php
Normal file
60
resources/views/livewire/project/database/import.blade.php
Normal file
@ -0,0 +1,60 @@
|
||||
<div>
|
||||
<h2>Import Backup</h2>
|
||||
<div class="mt-2 mb-4 rounded alert alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span>This is a destructive action, existing data will be replaced!</span>
|
||||
</div>
|
||||
|
||||
@if (str(data_get($resource, 'status'))->startsWith('running'))
|
||||
@if (!$validated)
|
||||
<div>{{ $validationMsg }}</div>
|
||||
@else
|
||||
<form disabled wire:submit.prevent="runImport" x-data="{ isFinished: false, isUploading: false, progress: 0 }">
|
||||
@if ($resource->type() === 'standalone-postgresql')
|
||||
<x-forms.input class="mb-2" label="Custom Import Command"
|
||||
wire:model='postgresqlRestoreCommand'></x-forms.input>
|
||||
@elseif ($resource->type() === 'standalone-mysql')
|
||||
<x-forms.input class="mb-2" label="Custom Import Command"
|
||||
wire:model='mysqlRestoreCommand'></x-forms.input>
|
||||
@elseif ($resource->type() === 'standalone-mariadb')
|
||||
<x-forms.input class="mb-2" label="Custom Import Command"
|
||||
wire:model='mariadbRestoreCommand'></x-forms.input>
|
||||
@endif
|
||||
<div x-on:livewire-upload-start="isUploading = true; isFinished = false"
|
||||
x-on:livewire-upload-finish="isUploading = false; isFinished = true"
|
||||
x-on:livewire-upload-error="isUploading = false"
|
||||
x-on:livewire-upload-progress="progress = $event.detail.progress">
|
||||
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" for="file_input">Upload
|
||||
file</label>
|
||||
<input wire:model="file"
|
||||
class="block w-full text-sm rounded cursor-pointer text-whiteborder bg-coolgray-100 border-coolgray-400 focus:outline-none"
|
||||
aria-describedby="file_input_help" id="file_input" type="file">
|
||||
<p class="mt-1 text-sm text-neutral-500" id="file_input_help">Max file size: 256MB
|
||||
</p>
|
||||
|
||||
@error('file')
|
||||
<span class="error">{{ $message }}</span>
|
||||
@enderror
|
||||
<div x-show="isUploading">
|
||||
<progress max="100" x-bind:value="progress"
|
||||
class="progress progress-warning"></progress>
|
||||
</div>
|
||||
</div>
|
||||
<x-forms.button type="submit" class="w-full mt-4" x-show="isFinished">Import Backup</x-forms.button>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
@if ($scpInProgress)
|
||||
<div>Database backup is being copied to server...</div>
|
||||
@endif
|
||||
|
||||
<div class="container w-full pt-4 mx-auto">
|
||||
<livewire:activity-monitor header="Database import output" />
|
||||
</div>
|
||||
@else
|
||||
<div class="text-neutral-500">Database must be running to import a backup.</div>
|
||||
@endif
|
||||
</div>
|
@ -10,8 +10,8 @@
|
||||
helper="
|
||||
You can use these variables in your Docker Compose file and Coolify will generate default values or replace them with the values you set on the UI forms.<br>
|
||||
<br>
|
||||
- SERVICE_FQDN_*: FQDN - could be changable from the UI. (example: SERVICE_FQDN_GHOST)<br>
|
||||
- SERVICE_URL_*: URL parsed from FQDN - could be changable from the UI. (example: SERVICE_URL_GHOST)<br>
|
||||
- SERVICE_FQDN_*: FQDN - could be changeable from the UI. (example: SERVICE_FQDN_GHOST)<br>
|
||||
- SERVICE_URL_*: URL parsed from FQDN - could be changeable from the UI. (example: SERVICE_URL_GHOST)<br>
|
||||
- SERVICE_BASE64_64_*: Generated 'base64' string with length of '64' (example: SERVICE_BASE64_64_GHOST, to generate 32 bit: SERVICE_BASE64_32_GHOST)<br>
|
||||
- SERVICE_USER_*: Generated user (example: SERVICE_USER_MYSQL)<br>
|
||||
- SERVICE_PASSWORD_*: Generated password (example: SERVICE_PASSWORD_MYSQL)<br>"
|
||||
|
@ -13,6 +13,19 @@
|
||||
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'; if(window.location.search) window.location.search = ''"
|
||||
href="#">Storages
|
||||
</a>
|
||||
<a :class="activeTab === 'environment-variables' && 'text-white'"
|
||||
@click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'"
|
||||
href="#">Environment
|
||||
Variables</a>
|
||||
<a :class="activeTab === 'scheduled-tasks' && 'text-white'"
|
||||
@click.prevent="activeTab = 'scheduled-tasks'; window.location.hash = 'scheduled-tasks'"
|
||||
href="#">Scheduled Tasks
|
||||
</a>
|
||||
<a :class="activeTab === 'danger' && 'text-white'"
|
||||
@click.prevent="activeTab = 'danger';
|
||||
window.location.hash = 'danger'"
|
||||
href="#">Danger Zone
|
||||
|
||||
@if (
|
||||
$serviceDatabase?->databaseType() === 'standalone-mysql' ||
|
||||
$serviceDatabase?->databaseType() === 'standalone-postgresql' ||
|
||||
@ -56,6 +69,13 @@
|
||||
<livewire:project.database.create-scheduled-backup :database="$serviceDatabase" :s3s="$s3s" />
|
||||
<livewire:project.database.scheduled-backups :database="$serviceDatabase" />
|
||||
</div>
|
||||
</div>
|
||||
<div x-cloak x-show="activeTab === 'scheduled-tasks'">
|
||||
<livewire:project.shared.scheduled-task.all :resource="$service" />
|
||||
</div>
|
||||
<div x-cloak x-show="activeTab === 'danger'">
|
||||
<livewire:project.shared.danger :resource="$service" />
|
||||
</div>
|
||||
@endisset
|
||||
</div>
|
||||
</div>
|
||||
|
@ -7,7 +7,7 @@
|
||||
server <span class="px-1 text-warning">{{ data_get($resource, 'destination.server.name') }}</span>
|
||||
in <span class="px-1 text-warning"> {{ data_get($resource, 'destination.network') }} </span> network.</a>
|
||||
</div>
|
||||
{{-- Additonal Destinations:
|
||||
{{-- Additional Destinations:
|
||||
{{$resource->additional_destinations}} --}}
|
||||
{{-- @if (count($servers) > 0)
|
||||
<div>
|
||||
|
@ -0,0 +1,15 @@
|
||||
<dialog id="newTask" class="modal">
|
||||
<form method="dialog" class="flex flex-col gap-2 rounded modal-box" wire:submit='submit'>
|
||||
<h3 class="text-lg font-bold">Add Scheduled Task</h3>
|
||||
<x-forms.input placeholder="Run cron" id="name" label="Name" required />
|
||||
<x-forms.input placeholder="php artisan schedule:run" id="command" label="Command" required />
|
||||
<x-forms.input placeholder="0 0 * * * or daily" id="frequency" label="Frequency" required />
|
||||
<x-forms.input placeholder="php" id="container" helper="You can leave it empty if your resource only have one container." label="Container name" />
|
||||
<x-forms.button onclick="newTask.close()" type="submit">
|
||||
Save
|
||||
</x-forms.button>
|
||||
</form>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
@ -0,0 +1,24 @@
|
||||
<div>
|
||||
<div class="flex gap-2">
|
||||
<h2 class="pb-4">Scheduled Tasks</h2>
|
||||
<x-forms.button class="btn" onclick="newTask.showModal()">+ Add</x-forms.button>
|
||||
<livewire:project.shared.scheduled-task.add />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@forelse($resource->scheduled_tasks as $task)
|
||||
<a class="flex flex-col box"
|
||||
@if ($resource->type() == 'application')
|
||||
href="{{ route('project.application.scheduled-tasks', [...$parameters, 'task_uuid' => $task->uuid]) }}">
|
||||
@elseif ($resource->type() == 'service')
|
||||
href="{{ route('project.service.scheduled-tasks', [...$parameters, 'task_uuid' => $task->uuid]) }}">
|
||||
@endif
|
||||
<div><span class="font-bold text-warning">{{ $task->name }}<span></div>
|
||||
<div>Frequency: {{ $task->frequency }}</div>
|
||||
<div>Last run: {{ data_get($task->latest_log, 'status', 'No runs yet') }}</div>
|
||||
</a>
|
||||
@empty
|
||||
<div class="text-neutral-500">No scheduled tasks configured.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,32 @@
|
||||
<div class="flex flex-col-reverse gap-2">
|
||||
@forelse($executions as $execution)
|
||||
@if (data_get($execution, 'id') == $selectedKey)
|
||||
<div class="p-2">
|
||||
@if (data_get($execution, 'message'))
|
||||
<div>
|
||||
<pre>{{ data_get($execution, 'message') }}</pre>
|
||||
</div>
|
||||
@else
|
||||
<div>No output was recorded for this execution.</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
<a wire:click="selectTask({{ data_get($execution, 'id') }})" @class([
|
||||
'flex flex-col border-l border-dashed transition-colors box-without-bg bg-coolgray-100 hover:bg-coolgray-100',
|
||||
'bg-coolgray-200 text-white hover:bg-coolgray-200' =>
|
||||
data_get($execution, 'id') == $selectedKey,
|
||||
'border-green-500' => data_get($execution, 'status') === 'success',
|
||||
'border-red-500' => data_get($execution, 'status') === 'failed',
|
||||
])>
|
||||
@if (data_get($execution, 'status') === 'running')
|
||||
<div class="absolute top-2 right-2">
|
||||
<x-loading />
|
||||
</div>
|
||||
@endif
|
||||
<div>Status: {{ data_get($execution, 'status') }}</div>
|
||||
<div>Started At: {{ data_get($execution, 'created_at') }}</div>
|
||||
</a>
|
||||
@empty
|
||||
<div>No executions found.</div>
|
||||
@endforelse
|
||||
</div>
|
@ -0,0 +1,43 @@
|
||||
<div>
|
||||
<x-modal yesOrNo modalId="{{ $modalId }}" modalTitle="Delete Scheduled Task">
|
||||
<x-slot:modalBody>
|
||||
<p>Are you sure you want to delete this scheduled task <span
|
||||
class="font-bold text-warning">({{ $task->name }})</span>?</p>
|
||||
</x-slot:modalBody>
|
||||
</x-modal>
|
||||
|
||||
<h1>Scheduled Task</h1>
|
||||
@if ($type === 'application')
|
||||
<livewire:project.application.heading :application="$resource" />
|
||||
@elseif ($type === 'service')
|
||||
<livewire:project.service.navbar :service="$resource" :parameters="$parameters" />
|
||||
@endif
|
||||
|
||||
<form wire:submit="submit" class="w-full">
|
||||
<div class="flex flex-col gap-2 pb-4">
|
||||
<div class="flex items-end gap-2 pt-4">
|
||||
<h2>Scheduled Task</h2>
|
||||
<x-forms.button type="submit">
|
||||
Save
|
||||
</x-forms.button>
|
||||
|
||||
<x-forms.button isError isModal modalId="{{ $modalId }}">
|
||||
Delete
|
||||
</x-forms.button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full gap-2">
|
||||
<x-forms.input placeholder="Name" id="task.name" label="Name" required />
|
||||
<x-forms.input placeholder="php artisan schedule:run" id="task.command" label="Command" required />
|
||||
<x-forms.input placeholder="0 0 * * * or daily" id="task.frequency" label="Frequency" required />
|
||||
<x-forms.input placeholder="php" helper="You can leave it empty if your resource only have one container."
|
||||
id="task.container" label="Container name" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="pt-4">
|
||||
<h3 class="py-4">Recent executions</h3>
|
||||
<livewire:project.shared.scheduled-task.executions key="{{ $task->id }}" selectedKey="" :executions="$task->executions->take(-20)" />
|
||||
</div>
|
||||
</div>
|
@ -1,7 +1,7 @@
|
||||
<div>
|
||||
<dialog id="sendTestEmail" class="modal">
|
||||
<form method="dialog" class="flex flex-col gap-2 rounded modal-box" wire:submit='submit'>
|
||||
<x-forms.input placeholder="test@example.com" id="emails" label="Recepients" required />
|
||||
<x-forms.input placeholder="test@example.com" id="emails" label="Recipients" required />
|
||||
<x-forms.button onclick="sendTestEmail.close()" wire:click="sendTestNotification">
|
||||
Send Email
|
||||
</x-forms.button>
|
||||
|
@ -50,6 +50,7 @@
|
||||
use App\Livewire\Project\EnvironmentEdit;
|
||||
use App\Livewire\Project\Shared\ExecuteContainerCommand;
|
||||
use App\Livewire\Project\Shared\Logs;
|
||||
use App\Livewire\Project\Shared\ScheduledTask\Show as ScheduledTaskShow;
|
||||
|
||||
use App\Livewire\Security\ApiTokens;
|
||||
use App\Livewire\Security\PrivateKey\Create as SecurityPrivateKeyCreate;
|
||||
@ -67,6 +68,12 @@
|
||||
use App\Livewire\Source\Github\Change as GitHubChange;
|
||||
use App\Livewire\Subscription\Index as SubscriptionIndex;
|
||||
use App\Livewire\Waitlist\Index as WaitlistIndex;
|
||||
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Laravel\Fortify\Contracts\FailedPasswordResetLinkRequestResponse;
|
||||
use Laravel\Fortify\Contracts\SuccessfulPasswordResetLinkRequestResponse;
|
||||
use Laravel\Fortify\Fortify;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
if (isDev()) {
|
||||
Route::get('/dev/compose', Compose::class)->name('dev.compose');
|
||||
@ -139,6 +146,7 @@
|
||||
Route::get('/deployment/{deployment_uuid}', DeploymentShow::class)->name('project.application.deployment.show');
|
||||
Route::get('/logs', Logs::class)->name('project.application.logs');
|
||||
Route::get('/command', ExecuteContainerCommand::class)->name('project.application.command');
|
||||
Route::get('/tasks/{task_uuid}', ScheduledTaskShow::class)->name('project.application.scheduled-tasks');
|
||||
});
|
||||
Route::prefix('project/{project_uuid}/{environment_name}/database/{database_uuid}')->group(function () {
|
||||
Route::get('/', DatabaseConfiguration::class)->name('project.database.configuration');
|
||||
@ -151,6 +159,7 @@
|
||||
Route::get('/', ServiceConfiguration::class)->name('project.service.configuration');
|
||||
Route::get('/{service_name}', ServiceIndex::class)->name('project.service.index');
|
||||
Route::get('/command', ExecuteContainerCommand::class)->name('project.service.command');
|
||||
Route::get('/tasks/{task_uuid}', ScheduledTaskShow::class)->name('project.service.scheduled-tasks');
|
||||
});
|
||||
|
||||
Route::get('/servers', ServerIndex::class)->name('server.index');
|
||||
|
@ -28,7 +28,7 @@ ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensu
|
||||
;;
|
||||
esac
|
||||
|
||||
# Ovewrite LATEST_VERSION if user pass a version number
|
||||
# Overwrite LATEST_VERSION if user pass a version number
|
||||
if [ "$1" != "" ]; then
|
||||
LATEST_VERSION=$1
|
||||
LATEST_VERSION="${LATEST_VERSION,,}"
|
||||
|
@ -565,7 +565,7 @@ services:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- appwrite-builds:/storage/builds:rw
|
||||
- appwrite-functions:/storage/functions:rw
|
||||
# Host mount nessessary to share files between executor and runtimes.
|
||||
# Host mount necessary to share files between executor and runtimes.
|
||||
# It's not possible to share mount file between 2 containers without host mount (copying is too slow)
|
||||
- /tmp:/tmp:rw
|
||||
environment:
|
||||
|
@ -1,5 +1,5 @@
|
||||
# documentation: https://github.com/mregni/EmbyStat/wiki/docker
|
||||
# slogan: EmyStat is an open-source, self-hosted web analytics tool, designed to provide insight into website traffic and user behavior, of your local Emby deployement, all within your control.
|
||||
# slogan: EmyStat is an open-source, self-hosted web analytics tool, designed to provide insight into website traffic and user behavior, of your local Emby deployment, all within your control.
|
||||
# tags: media, server, movies, tv, music
|
||||
|
||||
services:
|
||||
|
@ -121,7 +121,7 @@
|
||||
},
|
||||
"embystat": {
|
||||
"documentation": "https:\/\/github.com\/mregni\/EmbyStat\/wiki\/docker",
|
||||
"slogan": "EmyStat is an open-source, self-hosted web analytics tool, designed to provide insight into website traffic and user behavior, of your local Emby deployement, all within your control.",
|
||||
"slogan": "EmyStat is an open-source, self-hosted web analytics tool, designed to provide insight into website traffic and user behavior, of your local Emby deployment, all within your control.",
|
||||
"compose": "c2VydmljZXM6CiAgZW1ieXN0YXQ6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvZW1ieXN0YXQ6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0VNQllTVEFUCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnZW1ieXN0YXQtY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6NjU1NScKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=",
|
||||
"tags": [
|
||||
"media",
|
||||
|
@ -4,7 +4,7 @@
|
||||
"version": "3.12.36"
|
||||
},
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.185"
|
||||
"version": "4.0.0-beta.186"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user