diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php index c36f3cd7ed..35592b6cb9 100644 --- a/app/Jobs/ScheduledTaskJob.php +++ b/app/Jobs/ScheduledTaskJob.php @@ -155,12 +155,12 @@ class ScheduledTaskJob implements ShouldBeEncrypted, ShouldQueue ]); if ($this->resource->type() === 'application') { - $containers = getCurrentApplicationContainerStatus($this->server, $this->resource, 0); - if ($containers->count() > 0) { - $containers->each(function ($container) { + // `docker ps -a` also lists stopped containers, for example the old container of a rolling update. + getCurrentApplicationContainerStatus($this->server, $this->resource, 0) + ->filter(fn ($container) => data_get($container, 'State') === 'running') + ->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')) { @@ -182,7 +182,7 @@ class ScheduledTaskJob implements ShouldBeEncrypted, ShouldQueue } foreach ($this->containers as $containerName) { - if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container.'-'.$this->resource->uuid)) { + if (count($this->containers) == 1 || $containerName === $this->task->container || str_starts_with($containerName, $this->task->container.'-'.$this->resource->uuid)) { $cmd = "sh -c '".str_replace("'", "'\''", $this->task->command)."'"; $dockerCommand = $this->server->isNonRoot() ? 'sudo docker' : 'docker'; $execCommand = "{$dockerCommand} exec ".escapeshellarg($containerName)." {$cmd}"; diff --git a/tests/Feature/ScheduledTaskApplicationContainerTest.php b/tests/Feature/ScheduledTaskApplicationContainerTest.php new file mode 100644 index 0000000000..fd1162f644 --- /dev/null +++ b/tests/Feature/ScheduledTaskApplicationContainerTest.php @@ -0,0 +1,84 @@ + 0]); + config(['app.maintenance.store' => 'array', 'constants.ssh.mux_enabled' => false]); + + $team = Team::factory()->create(); + $server = Server::factory()->create([ + 'team_id' => $team->id, + 'private_key_id' => PrivateKey::factory()->create(['team_id' => $team->id])->id, + ]); + $server->settings->update(['is_reachable' => true, 'is_usable' => true, 'force_disabled' => false, 'docker_version' => '28.0.0']); + $destination = StandaloneDocker::query()->where('server_id', $server->id)->firstOrFail(); + $project = Project::factory()->create(['team_id' => $team->id]); + + $this->application = Application::factory()->create([ + 'environment_id' => Environment::factory()->create(['project_id' => $project->id])->id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + ]); + $this->task = ScheduledTask::factory()->create([ + 'team_id' => $team->id, + 'application_id' => $this->application->id, + 'container' => null, + 'command' => 'echo hello', + ]); +}); + +/** + * @param array $containers container name => docker state + */ +function fakeApplicationContainers(array $containers): void +{ + $output = collect($containers) + ->map(fn (string $state, string $name) => json_encode(['ID' => $name, 'Names' => $name, 'Labels' => 'coolify.applicationId=1', 'State' => $state])) + ->implode("\n"); + + Process::fake(fn ($process) => str_contains($process->command, 'docker ps -a --filter') + ? Process::result(output: $output) + : Process::result(output: 'done')); +} + +test('a scheduled task ignores stopped application containers', function () { + $uuid = $this->application->uuid; + fakeApplicationContainers(["{$uuid}-111111111111" => 'exited', "{$uuid}-222222222222" => 'running']); + + (new ScheduledTaskJob($this->task))->handle(); + + Process::assertRan(fn ($process) => str_contains($process->command, "docker exec '{$uuid}-222222222222' sh -c")); + Process::assertDidntRun(fn ($process) => str_contains($process->command, "docker exec '{$uuid}-111111111111'")); + expect($this->task->executions()->sole()->status)->toBe('success'); +}); + +test('a scheduled task runs in the application container with the exact name', function () { + $uuid = $this->application->uuid; + fakeApplicationContainers(["{$uuid}-111111111111" => 'running', "{$uuid}-222222222222" => 'running']); + $this->task->update(['container' => "{$uuid}-222222222222"]); + + (new ScheduledTaskJob($this->task->refresh()))->handle(); + + Process::assertRan(fn ($process) => str_contains($process->command, "docker exec '{$uuid}-222222222222' sh -c")); + expect($this->task->executions()->sole()->status)->toBe('success'); +});