From 93347a812c86ef9100583f4449ded5b441a382ec Mon Sep 17 00:00:00 2001 From: Austin Best Date: Wed, 21 Aug 2024 23:36:00 -0400 Subject: [PATCH] WIP... Closes #43 Implement sqlite3 to replace settings and servers json files Add nightly branch Refactor notifications so multiples can be sent per platform Add a trait/interface function loader Add some defines for common platforms Fix the dialogClose function Fix backups not being removed Some rewording --- .github/workflows/docker-publish.yml | 4 +- Dockerfile | 5 + README.md | 10 +- root/app/www/public/ajax/commands.php | 15 +- root/app/www/public/ajax/compose.php | 6 +- root/app/www/public/ajax/containers.php | 301 ++++++++------ root/app/www/public/ajax/login.php | 5 + root/app/www/public/ajax/notification.php | 385 +++++++++++------- root/app/www/public/ajax/overview.php | 10 +- root/app/www/public/ajax/settings.php | 147 ++++--- root/app/www/public/ajax/tasks.php | 28 +- root/app/www/public/api/index.php | 36 +- root/app/www/public/classes/Database.php | 96 +++++ root/app/www/public/classes/Docker.php | 61 +-- root/app/www/public/classes/Maintenance.php | 26 +- root/app/www/public/classes/Notifications.php | 128 ++++-- .../www/public/classes/interfaces/Docker.php | 50 +++ .../classes/interfaces/Notifications.php | 15 + .../traits/Database/ContainerGroupLink.php | 70 ++++ .../traits/Database/ContainerGroups.php | 80 ++++ .../traits/Database/ContainerSettings.php | 85 ++++ .../traits/Database/NotificationLink.php | 81 ++++ .../traits/Database/NotificationPlatform.php | 30 ++ .../traits/Database/NotificationTrigger.php | 61 +++ .../classes/traits/Database/Servers.php | 56 +++ .../classes/traits/Database/Settings.php | 69 ++++ .../www/public/classes/traits/Docker/API.php | 4 +- .../classes/traits/Docker/Container.php | 13 +- .../traits/Notifications/Templates.php | 64 +++ .../traits/Notifications/notifiarr.php | 4 +- root/app/www/public/crons/health.php | 24 +- root/app/www/public/crons/housekeeper.php | 46 ++- root/app/www/public/crons/prune.php | 17 +- root/app/www/public/crons/pulls.php | 40 +- root/app/www/public/crons/sse.php | 2 +- root/app/www/public/crons/state.php | 103 +++-- root/app/www/public/crons/stats.php | 4 +- root/app/www/public/functions/api.php | 141 ++++++- root/app/www/public/functions/common.php | 35 +- root/app/www/public/functions/containers.php | 42 +- root/app/www/public/functions/curl.php | 4 +- root/app/www/public/functions/docker.php | 21 +- root/app/www/public/functions/files.php | 4 +- .../www/public/functions/helpers/array.php | 5 + .../www/public/functions/helpers/strings.php | 14 +- .../www/public/functions/notifications.php | 47 --- root/app/www/public/includes/constants.php | 21 +- root/app/www/public/includes/footer.php | 2 +- root/app/www/public/includes/header.php | 52 +-- root/app/www/public/index.php | 10 +- root/app/www/public/js/common.js | 37 +- root/app/www/public/js/containers.js | 13 +- root/app/www/public/js/notification.js | 122 +++++- root/app/www/public/js/settings.js | 2 +- root/app/www/public/loader.php | 90 ++-- .../public/migrations/001_initial_setup.php | 274 +++++++++++++ root/app/www/public/sse.php | 2 +- root/app/www/public/startup.php | 18 +- root/etc/php82/conf.d/dockwatch.ini | 2 +- 59 files changed, 2294 insertions(+), 845 deletions(-) create mode 100644 root/app/www/public/classes/Database.php create mode 100644 root/app/www/public/classes/interfaces/Docker.php create mode 100644 root/app/www/public/classes/interfaces/Notifications.php create mode 100644 root/app/www/public/classes/traits/Database/ContainerGroupLink.php create mode 100644 root/app/www/public/classes/traits/Database/ContainerGroups.php create mode 100644 root/app/www/public/classes/traits/Database/ContainerSettings.php create mode 100644 root/app/www/public/classes/traits/Database/NotificationLink.php create mode 100644 root/app/www/public/classes/traits/Database/NotificationPlatform.php create mode 100644 root/app/www/public/classes/traits/Database/NotificationTrigger.php create mode 100644 root/app/www/public/classes/traits/Database/Servers.php create mode 100644 root/app/www/public/classes/traits/Database/Settings.php create mode 100644 root/app/www/public/classes/traits/Notifications/Templates.php delete mode 100644 root/app/www/public/functions/notifications.php create mode 100644 root/app/www/public/migrations/001_initial_setup.php diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 4b42365..ce38013 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -4,11 +4,11 @@ name: Docker on: push: - branches: [ "main", "develop" ] + branches: [ "main", "develop", "nightly" ] # Publish semver tags as releases. tags: [ 'v*.*.*' ] pull_request: - branches: [ "main", "develop" ] + branches: [ "main", "develop", "nightly" ] env: # Use docker.io for Docker Hub if empty diff --git a/Dockerfile b/Dockerfile index 5ea2348..3f1befc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,11 @@ RUN \ # install sockets RUN apk add --no-cache \ php82-sockets + +# install sqlite3 +RUN apk add --no-cache \ + sqlite \ + php82-sqlite3 # add regctl for container digest checks ARG TARGETARCH diff --git a/README.md b/README.md index 9cfd62d..b31be72 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,12 @@ ## Purpose Simple UI driven way to manage updates & notifications for Docker containers. -No database required. All settings are stored locally in a volume mount. +No database container required, all settings are stored locally with sqlite3 + +## Branches +- `:main` Stable releases after testing on develop +- `:develop` Usually pretty stable +- `:nightly` Use at your own risk, suggest joining Discord to keep up to date with what is going on. Might not be able to hop back to develop or main!! ## Notification triggers @@ -21,12 +26,13 @@ No database required. All settings are stored locally in a volume mount. ## Notification platforms - Notifiarr +- Telegram (*coming soon*) ## Update options - Ignore -- Auto update - Check for updates +- Auto update ## Additional features diff --git a/root/app/www/public/ajax/commands.php b/root/app/www/public/ajax/commands.php index b5c9d4a..317c1be 100644 --- a/root/app/www/public/ajax/commands.php +++ b/root/app/www/public/ajax/commands.php @@ -42,10 +42,10 @@ $serverData) { + foreach ($serversTable as $serverId => $serverData) { ?> - + $serverData) { - if (in_array($serverIndex, $servers)) { + foreach ($serversTable as $serverId => $serverData) { + if (in_array($serverId, $servers)) { $serverOverride = $serverData; $apiResponse = apiRequest($_POST['command'], ['name' => $_POST['container'], 'params' => $_POST['parameters']]); - - if ($apiResponse['code'] == 200) { - $apiResponse = $apiResponse['response']['docker']; - } else { - $apiResponse = $apiResponse['code'] .': '. $apiResponse['error']; - } + $apiResponse = $apiResponse['code'] == 200 ? $apiResponse['response']['docker'] : $apiResponse['code'] .': '. $apiResponse['error']; ?>

diff --git a/root/app/www/public/ajax/compose.php b/root/app/www/public/ajax/compose.php index c03e65a..ba09996 100644 --- a/root/app/www/public/ajax/compose.php +++ b/root/app/www/public/ajax/compose.php @@ -15,7 +15,7 @@ container_name: dockwatch image: ghcr.io/notifiarr/dockwatch:main ports: - - 9999:80/tcp + - ' . APP_PORT . ':80/tcp environment: - PGID=999 - TZ=America/New_York @@ -35,7 +35,7 @@ closedir($dir); if ($_POST['m'] == 'init') { - if ($_SESSION['serverIndex'] != 0) { + if ($_SESSION['activeServerId'] != APP_SERVER_ID) { echo 'Remote compose management is not supported. Please do that on the Dockwatch instance directly.'; } else { ?> @@ -176,4 +176,4 @@ } else { echo $up; } -} \ No newline at end of file +} diff --git a/root/app/www/public/ajax/containers.php b/root/app/www/public/ajax/containers.php index ad97f51..821f111 100644 --- a/root/app/www/public/ajax/containers.php +++ b/root/app/www/public/ajax/containers.php @@ -10,10 +10,13 @@ require 'shared.php'; if ($_POST['m'] == 'init') { - $dependencyFile = $docker->setContainerDependencies($processList); - $pulls = is_array($pullsFile) ? $pullsFile : json_decode($pullsFile, true); + $containersTable = $database->getContainers(); + $containerGroupsTable = $database->getContainerGroups(); + $containerLinksTable = $database->getContainerGroupLinks(); + $dependencyFile = $docker->setContainerDependencies($processList); + $pulls = is_array($pullsFile) ? $pullsFile : json_decode($pullsFile, true); + $pullsNotice = empty($pullsFile) ? true : false; array_sort_by_key($processList, 'Names'); - $pullsNotice = empty($pullsFile) ? true : false; ?>
@@ -26,7 +29,7 @@
- Real time updates: 60)' : 'disabled') ?> + Real time updates: 60)' : 'disabled' ?>
@@ -47,31 +50,36 @@ $containerGroup) { - $groupCPU = $groupMemory = 0; + $groupContainerHashes = []; + if ($containerLinksTable) { + foreach ($containerGroupsTable as $containerGroup) { + $groupHash = $containerGroup['hash']; + $groupContainers = $database->getGroupLinkContainersFromGroupId($containerLinksTable, $containersTable, $containerGroup['id']); + $groupCPU = $groupMemory = 0; foreach ($processList as $process) { $nameHash = md5($process['Names']); - if (in_array($nameHash, $containerGroup['containers'])) { - $memUsage = floatval(str_replace('%', '', $process['stats']['MemPerc'])); - $groupMemory += $memUsage; - - $cpuUsage = floatval(str_replace('%', '', $process['stats']['CPUPerc'])); - if (intval($settingsFile['global']['cpuAmount']) > 0) { - $cpuUsage = number_format(($cpuUsage / intval($settingsFile['global']['cpuAmount'])), 2); + foreach ($groupContainers as $groupContainer) { + if ($nameHash == $groupContainer['hash']) { + $memUsage = floatval(str_replace('%', '', $process['stats']['MemPerc'])); + $groupMemory += $memUsage; + + $cpuUsage = floatval(str_replace('%', '', $process['stats']['CPUPerc'])); + if (intval($settingsTable['cpuAmount']) > 0) { + $cpuUsage = number_format(($cpuUsage / intval($settingsTable['cpuAmount'])), 2); + } + $groupCPU += $cpuUsage; } - $groupCPU += $cpuUsage; } } ?> - + @@ -83,11 +91,12 @@ findContainer(['hash' => $_POST['hash'], 'data' => $stateFile]); - if ($_POST['action'] == 'stop' || $_POST['action'] == 'restart') { + if (str_equals_any($_POST['action'], ['stop', 'restart'])) { apiRequest('dockerStopContainer', [], ['name' => $container['Names']]); } - if ($_POST['action'] == 'start' || $_POST['action'] == 'restart') { + if (str_equals_any($_POST['action'], ['start', 'restart'])) { apiRequest('dockerStartContainer', [], ['name' => $container['Names']]); } $return = renderContainerRow($_POST['hash'], 'json'); - if ($_POST['action'] == 'start' || $_POST['action'] == 'restart') { + if (str_equals_any($_POST['action'], ['start', 'restart'])) { $return['length'] = 'Up 1 second'; } @@ -499,6 +501,10 @@ $processList = json_decode($processList['response']['docker'], true); array_sort_by_key($processList, 'Names'); + $containersTable = $database->getContainers(); + $containerGroupTable = $database->getContainerGroups(); + $containerGroupLinksTable = $database->getContainerGroupLinks(); + ?>
@@ -507,11 +513,11 @@
getContainerFromHash($nameHash, $containersTable); + $inGroup = ''; + + if ($containerGroupTable) { + foreach ($containerGroupTable as $containerGroup) { + $containersInGroup = $database->getGroupLinkContainersFromGroupId($containerGroupLinksTable, $containersTable, $containerGroup['id']); + + foreach ($containersInGroup as $containerInGroup) { + if ($containerInGroup['hash'] == $nameHash) { + $inGroup = $containerGroup['name']; + break; + } } } } ?> - + - + getContainers(); + $containerGroupTable = $database->getContainerGroups(); + $containerGroupLinksTable = $database->getContainerGroupLinks(); + foreach ($processList as $process) { $nameHash = md5($process['Names']); + $container = $database->getContainerFromHash($nameHash, $containersTable); $inGroup = ''; $inThisGroup = false; - if ($settingsFile['containerGroups']) { - foreach ($settingsFile['containerGroups'] as $groupHash => $groupContainers) { - if (in_array($nameHash, $groupContainers['containers'])) { - $inGroup = $groupContainers['name']; - if ($groupHash == $_POST['groupHash']) { - $inThisGroup = true; + if ($containerGroupTable) { + foreach ($containerGroupTable as $containerGroup) { + $containersInGroup = $database->getGroupLinkContainersFromGroupId($containerGroupLinksTable, $containersTable, $containerGroup['id']); + + foreach ($containersInGroup as $containerInGroup) { + if ($containerInGroup['hash'] == $nameHash) { + $inGroup = $containerGroup['name']; + + if ($containerGroup['id'] == $_POST['groupId']) { + $inGroup = '' . $containerGroup['name'] . ''; + $inThisGroup = true; + } + + break; } } } } + ?> - + - + getContainers(); + $containerGroupTable = $database->getContainerGroups(); + $containerGroupLinksTable = $database->getContainerGroupLinks(); + if ($_POST['delete']) { - unset($settingsFile['containerGroups'][$groupHash]); + $database->deleteContainerGroup($groupId); } else { - if ($_POST['selection'] == '1' && is_array($settingsFile['containerGroups'])) { - foreach ($settingsFile['containerGroups'] as $groupDetails) { - if (strtolower($groupDetails['name']) == strtolower($groupName)) { + if (!$groupId) { + foreach ($containerGroupTable as $containerGroup) { + if (str_compare('nocase', $containerGroup['name'], $groupName)) { $error = 'A group with that name already exists'; break; } } + + if (!$error) { + $groupId = $database->addContainerGroup($groupName); + + if (!$groupId) { + $error = 'Error creating the new \'' . $groupName . '\' group: ' . $database->error(); + } + } + } else { + foreach ($containerGroupTable as $containerGroup) { + if ($containerGroup['id'] == $groupId) { + if ($containerGroup['name'] != $groupName) { + $database->updateContainerGroup($groupId, ['name' => $database->prepare($groupName)]); + } + break; + } + } } if (!$error) { - $containers = []; - foreach ($_POST as $key => $val) { if (!str_contains($key, 'groupContainer')) { continue; } - list($junk, $containerHash) = explode('-', $key); - $containers[] = $containerHash; - } + list($junk, $containerId) = explode('-', $key); - $settingsFile['containerGroups'][$groupHash] = ['name' => $groupName, 'containers' => $containers]; - } - } + $linkExists = false; + foreach ($containerGroupLinksTable as $groupLink) { + if ($groupLink['group_id'] != $groupId) { + continue; + } + + if ($groupLink['container_id'] == $containerId) { + $linkExists = true; + break; + } + } - if (!$error) { - setServerFile('settings', $settingsFile); + if ($linkExists) { + if (!$val) { + $database->removeContainerGroupLink($groupId, $containerId); + } + } else { + if ($val) { + $database->addContainerGroupLink($groupId, $containerId); + } + } + } + } } echo $error; } if ($_POST['m'] == 'updateOptions') { - $processList = apiRequest('dockerProcessList', ['format' => true]); - $processList = json_decode($processList['response']['docker'], true); + $containersTable = $database->getContainers(); + $processList = apiRequest('dockerProcessList', ['format' => true]); + $processList = json_decode($processList['response']['docker'], true); array_sort_by_key($processList, 'Names'); ?> @@ -648,25 +709,24 @@ getContainerFromHash($nameHash, $containersTable); ?> - +

- Containers: + Containers:
   
Group
') ?>' ?> Not assigned' ?>
' : '') : '') ?>' : '') : '' ?> Not assigned' ?>
- - - +
- - -
-
- - - -
-
- - - -
+
+
+
+ + + +
+
+ + +
-
getContainers(); foreach ($_POST as $key => $val) { - preg_match('/container-update-([^-\n]+)|container-frequency-([^-\n]+)/', $key, $matches, PREG_OFFSET_CAPTURE); - if (!$matches) { + if (!str_contains($key, 'container-frequency-')) { continue; } - - $hash = $matches[1][0]; - if (!$hash || $hash == "all") { + + $hash = str_replace('container-frequency-', '', $key); + if (!$hash || $hash == 'all') { continue; } - list($minute, $hour, $dom, $month, $dow) = explode(' ', $_POST['container-frequency-' . $hash]); - $frequency = $minute . ' ' . $hour . ' ' . $dom . ' ' . $month . ' ' . $dow; + list($minute, $hour, $dom, $month, $dow) = explode(' ', $key); + $frequency = $minute . ' ' . $hour . ' ' . $dom . ' ' . $month . ' ' . $dow; + $updates = intval($_POST['container-update-' . $hash]); try { $cron = Cron\CronExpression::factory($frequency); @@ -722,16 +779,12 @@ $frequency = DEFAULT_CRON; } - $newSettings[$hash]['updates'] = $val; - $newSettings[$hash]['frequency'] = $frequency; - $newSettings[$hash]['restartUnhealthy'] = $settingsFile['containers'][$hash]['restartUnhealthy']; - $newSettings[$hash]['disableNotifications'] = $settingsFile['containers'][$hash]['disableNotifications']; - $newSettings[$hash]['shutdownDelay'] = $settingsFile['containers'][$hash]['shutdownDelay']; - $newSettings[$hash]['shutdownDelaySeconds'] = $settingsFile['containers'][$hash]['shutdownDelaySeconds']; + //-- ONLY UPDATE WHAT HAS CHANGED + $container = $database->getContainerFromHash($hash, $containersTable); + if ($container['updates'] != $updates || $container['frequency'] != $frequency) { + $database->updateContainer($hash, ['updates' => $updates, 'frequency' => $database->prepare($frequency)]); + } } - - $settingsFile['containers'] = $newSettings; - setServerFile('settings', $settingsFile); } if ($_POST['m'] == 'openEditContainer') { @@ -773,7 +826,7 @@ Web UI - This will create a dockwatch label, should be a valid URL (Ex: http://dockwatch or http://10.1.0.1:9999) + This will create a dockwatch label, should be a valid URL (Ex: http://dockwatch or http://10.1.0.1:) @@ -878,6 +931,8 @@ } if ($_POST['m'] == 'updateContainerOption') { - $settingsFile['containers'][$_POST['hash']][$_POST['option']] = $_POST['setting']; - $saveSettings = setServerFile('settings', $settingsFile); + $containersTable = $database->getContainers(); + $container = $database->getContainerGroupFromHash($_POST['hash'], $containersTable); + + $database->updateContainer($_POST['hash'], [$_POST['option'] => $database->prepare($_POST['setting'])]); } diff --git a/root/app/www/public/ajax/login.php b/root/app/www/public/ajax/login.php index 700a380..a4aaaa6 100644 --- a/root/app/www/public/ajax/login.php +++ b/root/app/www/public/ajax/login.php @@ -9,6 +9,11 @@ require 'shared.php'; +if ($_POST['m'] == 'resetSession') { + session_unset(); + session_destroy(); +} + if ($_POST['m'] == 'login') { logger(SYSTEM_LOG, 'login ->'); diff --git a/root/app/www/public/ajax/notification.php b/root/app/www/public/ajax/notification.php index 642e1e9..0e8511c 100644 --- a/root/app/www/public/ajax/notification.php +++ b/root/app/www/public/ajax/notification.php @@ -9,174 +9,281 @@ require 'shared.php'; -$triggers = [ - [ - 'name' => 'updated', - 'label' => 'Updated', - 'desc' => 'Send a notification when a container has had an update applied', - 'event' => 'updates' - ],[ - 'name' => 'updates', - 'label' => 'Updates', - 'desc' => 'Send a notification when a container has an update available', - 'event' => 'updates' - ],[ - 'name' => 'stateChange', - 'label' => 'State change', - 'desc' => 'Send a notification when a container has a state change (running -> down)', - 'event' => 'state' - ],[ - 'name' => 'added', - 'label' => 'Added', - 'desc' => 'Send a notification when a container is added', - 'event' => 'state' - ],[ - 'name' => 'removed', - 'label' => 'Removed', - 'desc' => 'Send a notification when a container is removed', - 'event' => 'state' - ],[ - 'name' => 'prune', - 'label' => 'Prune', - 'desc' => 'Send a notification when an image or volume is pruned', - 'event' => 'prune' - ],[ - 'name' => 'cpuHigh', - 'label' => 'CPU usage', - 'desc' => 'Send a notification when container CPU usage exceeds threshold (set in Settings)', - 'event' => 'usage' - ],[ - 'name' => 'memHigh', - 'label' => 'Memory usage', - 'desc' => 'Send a notification when container memory usage exceeds threshold (set in Settings)', - 'event' => 'usage' - ],[ - 'name' => 'health', - 'label' => 'Health change', - 'desc' => 'Send a notification when container becomes unhealthy', - 'event' => 'health' - ] - ]; - if ($_POST['m'] == 'init') { - $notificationPlatforms = $notifications->getPlatforms(); + $notificationPlatformTable = $database->getNotificationPlatforms(); + $notificationTriggersTable = $database->getNotificationTriggers(); + $notificationLinkTable = $database->getNotificationLinks(); + ?> -
-
-

Triggers

-
- - - - - - - - - - - - - - - - - - - - +
+ Platforms +
+ ' : 'Coming soon!'; + + ?> +
+
+
+

+
+
+
+ +
+
+ +
+
+ Configured senders +
+ +
+
+ Notifications have not been setup yet, click the plus icon above to set them up. +
+
+ + -
-
NotificationDescriptionWebhook EventPlatform
class="form-check-input notification-check" id="notifications-name-"> - -
+
+
+
+

+ + + +

+
+ You have not configured any triggers for this notificationgetNotificationTriggerNameFromId($triggerId, $notificationTriggersTable); + $enabledTriggers[] = $trigger; + } + + echo '
Enabled: ' . implode(', ', $enabledTriggers) . '
'; + } + ?> +
+
+
+
+ +
-
-

Platforms

- $platform) { ?> -
-
- - + + getNotificationPlatforms(); + $notificationTriggersTable = $database->getNotificationTriggers(); + $notificationLinkTable = $database->getNotificationLinks(); + $platformParameters = json_decode($notificationPlatformTable[$_POST['platformId']]['parameters'], true); + $platformName = $notifications->getNotificationPlatformNameFromId($_POST['platformId'], $notificationPlatformTable); + $linkRow = $notificationLinkTable[$_POST['linkId']]; + $existingTriggers = $existingParameters = []; + + if ($linkRow) { + $existingTriggers = $linkRow['trigger_ids'] ? json_decode($linkRow['trigger_ids'], true) : []; + $existingParameters = $linkRow['platform_parameters'] ? json_decode($linkRow['platform_parameters'], true) : []; + $existingName = $linkRow['name']; + } + + ?> +
+

+
+
+ + + + + + + + + + - - - + + + + - - - + + +
TriggerDescriptionEvent
NameSettingDescription type="checkbox" class="form-check-input notification-trigger" id="notificationTrigger-">
+ + + + + + + + + + + + + $platformParameterData) { + ?> - - - + + - - -
Setting
+ Name Required
+ The name of this notification sender +
*' : '') ?>Required' : '') ?>
+ type="text" id="notificationPlatformParameter-" class="form-control" value=""> +
+ + + +
+
+ + + + + +
- -
-
-
$val) { - if (!str_contains($key, '-name-')) { - continue; +if ($_POST['m'] == 'addNotification') { + if (!$_POST['platformId']) { + $error = 'Missing required platform id'; + } + + if (!$error) { + $notificationPlatformTable = $database->getNotificationPlatforms(); + $notificationTriggersTable = $database->getNotificationTriggers(); + $notificationLinkTable = $database->getNotificationLinks(); + $platformParameters = json_decode($notificationPlatformTable[$_POST['platformId']]['parameters'], true); + $platformName = $notifications->getNotificationPlatformNameFromId($_POST['platformId'], $notificationPlatformTable); + + //-- CHECK FOR REQUIRED FIELDS + foreach ($platformParameters as $platformParameterField => $platformParameterData) { + if ($platformParameterData['required'] && !$_POST['notificationPlatformParameter-' . $platformParameterField]) { + $error = 'Missing required platform field: ' . $platformParameterData['label']; + break; + } } - $type = str_replace('notifications-name-', '', $key); - $newSettings[$type] = [ - 'active' => trim($val), - 'platform' => $_POST['notifications-platform-' . $type] - ]; - } - $settingsFile['notifications']['triggers'] = $newSettings; + if (!$error) { + $triggerIds = $platformParameters = []; + $senderName = $platformName; - //-- PLATFORM SETTINGS - $newSettings = []; - foreach ($_POST as $key => $val) { - $strip = str_replace('notifications-platform-', '', $key); - list($platformId, $platformField) = explode('-', $strip); + foreach ($_POST as $key => $val) { + if (str_contains($key, 'notificationTrigger-') && $val) { + $triggerIds[] = str_replace('notificationTrigger-', '', $key); + } - if (!is_numeric($platformId)) { - continue; + if (str_contains($key, 'notificationPlatformParameter-')) { + $field = str_replace('notificationPlatformParameter-', '', $key); + + if ($field != 'name') { + $platformParameters[$field] = $val; + } else { + $senderName = $val; + } + } + } + + $database->addNotificationLink($_POST['platformId'], $triggerIds, $platformParameters, $senderName); } + } - $newSettings[$platformId][$platformField] = trim($val); + echo json_encode(['error' => $error]); +} + +if ($_POST['m'] == 'saveNotification') { + if (!$_POST['platformId']) { + $error = 'Missing required platform id'; + } + if (!$_POST['linkId']) { + $error = 'Missing required link id'; } - $settingsFile['notifications']['platforms'] = $newSettings; - $saveSettings = setServerFile('settings', $settingsFile); + if (!$error) { + $notificationPlatformTable = $database->getNotificationPlatforms(); + $notificationTriggersTable = $database->getNotificationTriggers(); + $notificationLinkTable = $database->getNotificationLinks(); + $platformParameters = json_decode($notificationPlatformTable[$_POST['platformId']]['parameters'], true); + $platformName = $notifications->getNotificationPlatformNameFromId($_POST['platformId'], $notificationPlatformTable); + + //-- CHECK FOR REQUIRED FIELDS + foreach ($platformParameters as $platformParameterField => $platformParameterData) { + if ($platformParameterData['required'] && !$_POST['notificationPlatformParameter-' . $platformParameterField]) { + $error = 'Missing required platform field: ' . $platformParameterData['label']; + break; + } + } + + if (!$error) { + $triggerIds = $platformParameters = []; + $senderName = $platformName; - if ($saveSettings['code'] != 200) { - $error = 'Error saving notification settings on server ' . ACTIVE_SERVER_NAME; + foreach ($_POST as $key => $val) { + if (str_contains($key, 'notificationTrigger-') && $val) { + $triggerIds[] = str_replace('notificationTrigger-', '', $key); + } + + if (str_contains($key, 'notificationPlatformParameter-')) { + $field = str_replace('notificationPlatformParameter-', '', $key); + + if ($field != 'name') { + $platformParameters[$field] = $val; + } else { + $senderName = $val; + } + } + } + + $database->updateNotificationLink($_POST['linkId'], $triggerIds, $platformParameters, $senderName); + } } - echo json_encode(['error' => $error, 'server' => ACTIVE_SERVER_NAME]); + echo json_encode(['error' => $error]); +} + +if ($_POST['m'] == 'deleteNotification') { + $database->deleteNotificationLink($_POST['linkId']); } if ($_POST['m'] == 'testNotify') { - $apiResponse = apiRequest('testNotify', [], ['platform' => $_POST['platform']]); + $apiResponse = apiRequest('testNotify', [], ['linkId' => $_POST['linkId']]); if ($apiResponse['code'] == 200) { $result = 'Test notification sent on server ' . ACTIVE_SERVER_NAME; diff --git a/root/app/www/public/ajax/overview.php b/root/app/www/public/ajax/overview.php index 70c3208..23e910f 100644 --- a/root/app/www/public/ajax/overview.php +++ b/root/app/www/public/ajax/overview.php @@ -83,8 +83,8 @@ $network += bytesFromString($netUsed); } - if (intval($settingsFile['global']['cpuAmount']) > 0) { - $cpuActual = number_format(($cpu / intval($settingsFile['global']['cpuAmount'])), 2); + if (intval($settingsTable['cpuAmount']) > 0) { + $cpuActual = number_format(($cpu / intval($settingsTable['cpuAmount'])), 2); } ?> @@ -98,7 +98,7 @@
Running:
Stopped:
- Total: + Total:
@@ -122,7 +122,7 @@
Up to date:
Outdated:
- Unchecked: + Unchecked:
@@ -197,4 +197,4 @@ Server name - + The name of this server, also passed in the notification payload Maintenance IP - + This IP is used to do updates/restarts for Dockwatch. It will create another container dockwatch-maintenance with this IP and after it has updated/restarted Dockwatch it will be removed. This is only required if you do static IP assignment for your containers. Maintenance port - + This port is used to do updates/restarts for Dockwatch. It will create another container dockwatch-maintenance with this port and after it has updated/restarted Dockwatch it will be removed. -

Login Failures

+

Login failures

@@ -81,7 +80,7 @@
-

Server list

+

Instances list

@@ -93,7 +92,7 @@ @@ -102,21 +101,21 @@ } else { ?> - - - + + + 1) { - foreach ($serversFile as $serverIndex => $serverSettings) { - if ($serverIndex == 0) { + if (count($serversTable) > 1) { + foreach ($serversTable as $serverSettings) { + if ($serverSettings['id'] == APP_SERVER_ID) { continue; } ?> - - - + + +
Sorry, remote management of server access is not allowed. Go to the server to make those changes.
-

New Containers

+

New containers

@@ -148,18 +147,18 @@
Updates1 - + What settings to use for new containers that are added
-

Auto Prune

+

Auto prune

@@ -173,21 +172,21 @@ @@ -198,7 +197,7 @@ '. $x .''; + $option .= ''; } echo $option; ?> @@ -223,21 +222,21 @@ @@ -258,7 +257,7 @@ @@ -279,28 +278,28 @@ @@ -322,25 +321,25 @@ - + - + @@ -358,7 +357,6 @@ } if ($_POST['m'] == 'saveGlobalSettings') { - $currentSettings = getServerFile('settings'); $newSettings = []; foreach ($_POST as $key => $val) { @@ -378,7 +376,7 @@ } //-- ENVIRONMENT SWITCHING - if ($currentSettings['global']['environment'] != $_POST['environment']) { + if ($settingsTable['environment'] != $_POST['environment']) { if ($_POST['environment'] == 0) { //-- USE INTERNAL linkWebroot('internal'); } else { //-- USE EXTERNAL @@ -386,18 +384,15 @@ } } - $settingsFile['global'] = $newSettings; - $saveSettings = setServerFile('settings', $settingsFile); - - if ($saveSettings['code'] != 200) { - $error = 'Error saving global settings on server ' . ACTIVE_SERVER_NAME; - } + $settingsTable = $database->setSettings($newSettings, $settingsTable); //-- ONLY MAKE SERVER CHANGES ON LOCAL - if ($_SESSION['serverIndex'] == 0) { + if ($_SESSION['activeServerId'] == APP_SERVER_ID) { + $serverList = []; + //-- ADD SERVER TO LIST if ($_POST['serverList-name-new'] && $_POST['serverList-url-new'] && $_POST['serverList-apikey-new']) { - $serversFile[] = ['name' => $_POST['serverList-name-new'], 'url' => rtrim($_POST['serverList-url-new'], '/'), 'apikey' => $_POST['serverList-apikey-new']]; + $serverList[] = ['name' => $_POST['serverList-name-new'], 'url' => rtrim($_POST['serverList-url-new'], '/'), 'apikey' => $_POST['serverList-apikey-new']]; } //-- UPDATE SERVER LIST @@ -406,14 +401,14 @@ continue; } - list($name, $field, $index) = explode('-', $key); + list($name, $field, $instanceId) = explode('-', $key); - if (!is_numeric($index)) { + if (!is_numeric($instanceId)) { continue; } - if ($_POST['serverList-name-' . $index] && $_POST['serverList-url-' . $index] && $_POST['serverList-apikey-' . $index]) { - $serversFile[$index] = ['name' => $_POST['serverList-name-' . $index], 'url' => rtrim($_POST['serverList-url-' . $index], '/'), 'apikey' => $_POST['serverList-apikey-' . $index]]; + if ($_POST['serverList-name-' . $instanceId] && $_POST['serverList-url-' . $instanceId] && $_POST['serverList-apikey-' . $instanceId]) { + $serverList[$instanceId] = ['name' => $_POST['serverList-name-' . $instanceId], 'url' => rtrim($_POST['serverList-url-' . $instanceId], '/'), 'apikey' => $_POST['serverList-apikey-' . $instanceId]]; } } @@ -423,41 +418,39 @@ continue; } - list($name, $field, $index) = explode('-', $key); + list($name, $field, $instanceId) = explode('-', $key); - if (!is_numeric($index)) { + if (!is_numeric($instanceId)) { continue; } - if (!$_POST['serverList-name-' . $index] && !$_POST['serverList-url-' . $index] && !$_POST['serverList-apikey-' . $index]) { - unset($serversFile[$index]); + if (!$_POST['serverList-name-' . $instanceId] && !$_POST['serverList-url-' . $instanceId] && !$_POST['serverList-apikey-' . $instanceId]) { + $serverList[$instanceId]['remove'] = true; } } - $saveServers = setServerFile('servers', $serversFile); - - if ($saveServers['code'] != 200) { - $error = 'Error saving server list on server ' . ACTIVE_SERVER_NAME; - } - } else { - $serversFile = getFile(SERVERS_FILE); + $serversTable = $database->setServers($serverList); } - $serverList = ''; - foreach ($serversFile as $serverIndex => $serverDetails) { - $ping = curl($serverDetails['url'] . '/api/?request=ping', ['x-api-key: ' . $serverDetails['apikey']]); - $disabled = ''; - if ($ping['code'] != 200) { - $disabled = ' [HTTP: ' . $ping['code'] . ']'; - } - $serverList .= ''; + $serverPings = apiRequestServerPings(); + $serverList = ''; + $serverList .= ' '; + + $_SESSION['serverList'] = $serverList; + $_SESSION['serverListUpdated'] = time(); echo json_encode(['error' => $error, 'server' => ACTIVE_SERVER_NAME, 'serverList' => $serverList]); } //-- CALLED FROM THE NAV MENU SELECT if ($_POST['m'] == 'updateServerIndex') { - $_SESSION['serverIndex'] = intval($_POST['index']); - $_SESSION['serverList'] = ''; -} \ No newline at end of file + $_SESSION['activeServerId'] = intval($_POST['index']); + $_SESSION['serverList'] = ''; +} diff --git a/root/app/www/public/ajax/tasks.php b/root/app/www/public/ajax/tasks.php index 631febb..b192f7a 100644 --- a/root/app/www/public/ajax/tasks.php +++ b/root/app/www/public/ajax/tasks.php @@ -26,43 +26,43 @@ - + - + - + - + - + - + - + @@ -141,17 +141,11 @@ } if ($_POST['m'] == 'updateTaskDisabled') { - if ($_POST['task'] == 'sse') { - $settingsFile['global']['sseEnabled'] = !intval($_POST['disabled']); + if ($_POST['task'] == 'sseEnabled') { + $database->setSetting('sseEnabled', !intval($_POST['disabled'])); } else { - $settingsFile['tasks'][$_POST['task']] = ['disabled' => intval($_POST['disabled'])]; - } - - $saveSettings = setServerFile('settings', $settingsFile); - - if ($saveSettings['code'] != 200) { - $error = 'Error saving task settings on server ' . ACTIVE_SERVER_NAME; + $database->setSetting($_POST['task'], intval($_POST['disabled'])); } echo json_encode(['error' => $error, 'server' => ACTIVE_SERVER_NAME]); -} \ No newline at end of file +} diff --git a/root/app/www/public/api/index.php b/root/app/www/public/api/index.php index e000dc0..2cea066 100644 --- a/root/app/www/public/api/index.php +++ b/root/app/www/public/api/index.php @@ -19,18 +19,22 @@ $code = 200; $apikey = $_SERVER['HTTP_X_API_KEY'] ? $_SERVER['HTTP_X_API_KEY'] : $_GET['apikey']; -if ($apikey != $serversFile[0]['apikey']) { +if ($apikey != $serversTable[APP_SERVER_ID]['apikey']) { apiResponse(401, ['error' => 'Invalid apikey']); } $_POST = json_decode(file_get_contents('php://input'), true); +if (str_contains_any($_GET['request'], ['ping', 'database-', 'file-', 'docker-'])) { +$response = apiRequestLocal($_GET['request'], ($_POST ?: $_GET), $_POST); +}else{ + switch (true) { case $_GET['request']: //-- GETTERS switch ($_GET['request']) { - case 'dwStats': - $response = getStats(); + case 'dependency': + $response = ['dependency' => getFile(DEPENDENCY_FILE)]; break; case 'dockerAutoCompose': if (!$_GET['name']) { @@ -115,17 +119,27 @@ case 'dockerStats': $response = ['docker' => $docker->stats($_GET['useCache'])]; break; + case 'dwStats': + $response = getStats(); + break; + case 'getServersTable': + $response = ['servers' => $serversTable]; + break; + case 'getSettingsTable': + $response = ['settings' => $settingsTable]; + break; case 'health': $response = ['health' => getFile(HEALTH_FILE)]; break; case 'ping': - $response = ['result' => 'pong from ' . ACTIVE_SERVER_NAME]; + $response = apiRequestLocal($_GET['request'], [], []); break; case 'pull': $response = ['pull' => getFile(PULL_FILE)]; break; - case 'settings': - $response = ['settings' => getFile(SETTINGS_FILE)]; + case 'runMigrations': + $database->migrations(); + $response = ['migrations' => []]; break; case 'state': $response = ['state' => getFile(STATE_FILE)]; @@ -133,12 +147,6 @@ case 'stats': $response = ['state' => getFile(STATS_FILE)]; break; - case 'servers': - $response = ['servers' => getFile(SERVERS_FILE)]; - break; - case 'dependency': - $response = ['dependency' => getFile(DEPENDENCY_FILE)]; - break; case 'sse': $response = ['sse' => getFile(SSE_FILE)]; break; @@ -296,7 +304,8 @@ $response = ['result' => executeTask($_POST['task'])]; break; case 'testNotify': - $testNotification = sendTestNotification($_POST['platform']); + $notifications = $notifications ?: new Notifications(); + $testNotification = $notifications->sendTestNotification($_POST['linkId']); if ($testNotification) { $code = '400'; @@ -311,5 +320,6 @@ break; } +} //-- RETURN apiResponse($code, $response); diff --git a/root/app/www/public/classes/Database.php b/root/app/www/public/classes/Database.php new file mode 100644 index 0000000..374dd05 --- /dev/null +++ b/root/app/www/public/classes/Database.php @@ -0,0 +1,96 @@ +connect(DATABASE_PATH . 'dockwatch.db'); + } + + public function connect($dbFile) + { + $db = new SQLite3($dbFile, SQLITE3_OPEN_CREATE | SQLITE3_OPEN_READWRITE); + $this->db = $db; + } + + public function query($query) + { + return $this->db->query($query); + } + + public function fetchAssoc($res) + { + return !$res ? [] : $res->fetchArray(SQLITE3_ASSOC); + } + + public function affectedRows($res) + { + return !$res ? 0 : $res->changes(SQLITE3_ASSOC); + } + + public function insertId() + { + return $this->db->lastInsertRowID(); + } + + public function error() + { + return $this->db->lastErrorMsg(); + } + + public function prepare($in) + { + $out = addslashes(stripslashes($in)); + return $out; + } + + public function migrations() + { + $database = $this; + $db = $this->db; + + if (filesize(DATABASE_PATH . 'dockwatch.db') == 0) { //-- INITIAL SETUP + logger(SYSTEM_LOG, 'Creating database and applying migration 001_initial_setup'); + require MIGRATIONS_PATH . '001_initial_setup.php'; + } else { //-- GET CURRENT MIGRATION & CHECK FOR NEEDED MIGRATIONS + $dir = opendir(MIGRATIONS_PATH); + while ($migration = readdir($dir)) { + if (substr($migration, 0, 3) > $this->getSetting('migration') && str_contains($migration, '.php')) { + logger(SYSTEM_LOG, 'Applying migration ' . $migration); + require MIGRATIONS_PATH . $migration; + } + } + closedir($dir); + } + } +} diff --git a/root/app/www/public/classes/Docker.php b/root/app/www/public/classes/Docker.php index 433bd9b..b033641 100644 --- a/root/app/www/public/classes/Docker.php +++ b/root/app/www/public/classes/Docker.php @@ -7,30 +7,27 @@ ---------------------------------- */ -//-- BRING IN THE TRAITS -$traits = ABSOLUTE_PATH . 'classes/traits/Docker/'; -$traitsDir = opendir($traits); -while ($traitFile = readdir($traitsDir)) { - if (str_contains($traitFile, '.php')) { - require $traits . $traitFile; - } -} -closedir($traitsDir); +//-- BRING IN THE EXTRAS +loadClassExtras('Docker'); class Docker { - use API; use Container; + use DockersApi; use Image; use Network; use Process; use Volume; protected $shell; + protected $database; public function __construct() { - $this->shell = new Shell(); + global $shell, $database; + + $this->shell = $shell ?? new Shell(); + $this->database = $database ?? new Database(); } public function stats($useCache) @@ -104,45 +101,3 @@ public function isIO($name) return str_contains($name, '/') ? $name : 'library/' . $name; } } - -interface DockerApi -{ - //-- CONTAINER SPECIFIC - public const STOP_CONTAINER = '/containers/%s/stop'; - public const CREATE_CONTAINER = '/containers/create?name=%s'; -} - -//-- https://docs.docker.com/reference/cli/docker -interface DockerSock -{ - //-- GENERAL - public const VERSION = '/usr/bin/docker version %s'; - public const RUN = '/usr/bin/docker run %s'; - public const LOGS = '/usr/bin/docker logs %s'; - public const PROCESSLIST_FORMAT = '/usr/bin/docker ps --all --no-trunc --size=false --format="{{json . }}" | jq -s --tab .'; - public const PROCESSLIST_CUSTOM = '/usr/bin/docker ps %s'; - public const STATS_FORMAT = '/usr/bin/docker stats --all --no-trunc --no-stream --format="{{json . }}" | jq -s --tab .'; - public const INSPECT_FORMAT = '/usr/bin/docker inspect %s --format="{{json . }}" | jq -s --tab .'; - public const INSPECT_CUSTOM = '/usr/bin/docker inspect %s %s'; - public const IMAGE_SIZES = '/usr/bin/docker images --format=\'{"ID":"{{ .ID }}", "Size": "{{ .Size }}"}\' | jq -s --tab .'; - //-- CONTAINER SPECIFIC - public const REMOVE_CONTAINER = '/usr/bin/docker container rm -f %s'; - public const START_CONTAINER = '/usr/bin/docker container start %s'; - public const STOP_CONTAINER = '/usr/bin/docker container stop %s%s'; - public const ORPHAN_CONTAINERS = '/usr/bin/docker images -f dangling=true --format="{{json . }}" | jq -s --tab .'; - public const CONTAINER_PORT = '/usr/bin/docker port %s %s'; - //-- IMAGE SPECIFIC - public const REMOVE_IMAGE = '/usr/bin/docker image rm %s'; - public const PULL_IMAGE = '/usr/bin/docker image pull %s'; - public const PRUNE_IMAGE = '/usr/bin/docker image prune -af'; - //-- VOLUME SPECIFIC - public const ORPHAN_VOLUMES = '/usr/bin/docker volume ls -qf dangling=true --format="{{json . }}" | jq -s --tab .'; - public const PRUNE_VOLUME = '/usr/bin/docker volume prune -af'; - public const REMOVE_VOLUME = '/usr/bin/docker volume rm %s'; - //-- NETWORK SPECIFIC - public const ORPHAN_NETWORKS = '/usr/bin/docker network ls -q --format="{{json . }}" | jq -s --tab .'; - public const INSPECT_NETWORK = '/usr/bin/docker network inspect %s --format="{{json . }}" | jq -s --tab .'; - public const PRUNE_NETWORK = '/usr/bin/docker network prune -af'; - public const REMOVE_NETWORK = '/usr/bin/docker network rm %s'; - public const GET_NETWORKS = '/usr/bin/docker network %s'; -} diff --git a/root/app/www/public/classes/Maintenance.php b/root/app/www/public/classes/Maintenance.php index a60d8b8..303d66b 100644 --- a/root/app/www/public/classes/Maintenance.php +++ b/root/app/www/public/classes/Maintenance.php @@ -12,15 +12,8 @@ * It is done this way so if things need to be done per host or per maintenance it is easy to split & log */ -//-- BRING IN THE TRAITS -$traits = ABSOLUTE_PATH . 'classes/traits/Maintenance/'; -$traitsDir = opendir($traits); -while ($traitFile = readdir($traitsDir)) { - if (str_contains($traitFile, '.php')) { - require $traits . $traitFile; - } -} -closedir($traitsDir); +//-- BRING IN THE EXTRAS +loadClassExtras('Maintenance'); class Maintenance { @@ -31,26 +24,21 @@ class Maintenance protected $maintenanceContainerName = 'dockwatch-maintenance'; protected $maintenancePort; protected $maintenanceIP; - protected $settingsFile; + protected $settingsTable; protected $hostContainer = []; protected $maintenanceContainer = []; protected $processList = []; public function __construct() { - global $docker, $settingsFile; + global $docker, $settingsTable; logger(MAINTENANCE_LOG, '$maintenance->__construct() ->'); - if (!$settingsFile) { - $settingsFile = getServerFile('settings'); - $settingsFile = $settingsFile['file']; - } - $this->docker = $docker; - $this->settingsFile = $settingsFile; - $this->maintenancePort = $settingsFile['global']['maintenancePort']; - $this->maintenanceIP = $settingsFile['global']['maintenanceIP']; + $this->settingsTable = $settingsTable; + $this->maintenancePort = $settingsTable['maintenancePort']; + $this->maintenanceIP = $settingsTable['maintenanceIP']; $getExpandedProcessList = getExpandedProcessList(true, true, true, true); $this->processList = is_array($getExpandedProcessList['processList']) ? $getExpandedProcessList['processList'] : []; $imageMatch = str_replace(':main', '', APP_IMAGE); diff --git a/root/app/www/public/classes/Notifications.php b/root/app/www/public/classes/Notifications.php index d9d47b3..8059128 100644 --- a/root/app/www/public/classes/Notifications.php +++ b/root/app/www/public/classes/Notifications.php @@ -7,38 +7,30 @@ ---------------------------------- */ -//-- BRING IN THE TRAITS -$traits = ABSOLUTE_PATH . 'classes/traits/Notifications/'; -$traitsDir = opendir($traits); -while ($traitFile = readdir($traitsDir)) { - if (str_contains($traitFile, '.php')) { - require $traits . $traitFile; - } -} -closedir($traitsDir); +//-- BRING IN THE EXTRAS +loadClassExtras('Notifications'); class Notifications { use Notifiarr; + use NotificationTemplates; protected $platforms; - protected $platformSettings; protected $headers; protected $logpath; protected $serverName; + protected $database; + public function __construct() { - global $platforms, $settingsFile; + global $platforms, $settingsTable, $database; - if (!$settingsFile) { - $settingsFile = getServerFile('settings'); - $settingsFile = $settingsFile['file']; - } + $this->database = $database ?? new Database(); + $this->platforms = $platforms; //-- includes/platforms.php + $this->logpath = LOGS_PATH . 'notifications/'; - $this->platforms = $platforms; //-- includes/platforms.php - $this->platformSettings = $settingsFile['notifications']['platforms']; - $this->logpath = LOGS_PATH . 'notifications/'; - $this->serverName = $settingsFile['global']['serverName']; + $settingsTable = $settingsTable ?? apiRequest2('database-getSettings'); + $this->serverName = is_array($settingsTable) ? $settingsTable['serverName'] : ''; } public function __toString() @@ -46,34 +38,102 @@ public function __toString() return 'Notifications initialized'; } - public function notify($platform, $payload) + public function sendTestNotification($linkId) + { + $payload = ['event' => 'test', 'title' => APP_NAME . ' test', 'message' => 'This is a test message sent from ' . APP_NAME]; + $return = ''; + $result = $this->notify($linkId, 'test', $payload); + + if ($result['code'] != 200) { + $return = 'Code ' . $result['code'] . ', ' . $result['error']; + } + + return $return; + } + + public function notify($linkId, $trigger, $payload) { + $linkIds = []; + $notificationPlatformTable = $this->database->getNotificationPlatforms(); + $notificationTriggersTable = $this->database->getNotificationTriggers(); + $notificationLinkTable = $this->database->getNotificationLinks(); + $triggerFields = $this->getTemplate($trigger); + + //-- MAKE IT MATCH THE TEMPLATE + foreach ($payload as $payloadField => $payloadVal) { + if (!array_key_exists($payloadField, $triggerFields) || !$payloadVal) { + unset($payload[$payloadField]); + } + } + if ($this->serverName) { $payload['server']['name'] = $this->serverName; } - $platformData = $this->getNotificationPlatformFromId($platform); - $logfile = $this->logpath . $platformData['name'] . '.log'; + if ($linkId) { + foreach ($notificationLinkTable as $notificationLink) { + if ($notificationLink['id'] == $linkId) { + $linkIds[] = $notificationLink; + } + } + + $notificationLink = $notificationLinkTable[$linkId]; + $notificationPlatform = $notificationPlatformTable[$notificationLink['platform_id']]; + } else { + foreach ($notificationTriggersTable as $notificationTrigger) { + if ($notificationTrigger['name'] == $trigger) { + foreach ($notificationLinkTable as $notificationLink) { + $triggers = makeArray(json_decode($notificationLink['trigger_ids'], true)); + + foreach ($triggers as $trigger) { + if ($trigger == $notificationTrigger['id']) { + $linkIds[] = $notificationLink; + } + } + } + break; + } + } + } + + foreach ($linkIds as $linkId) { + $platformId = $linkId['platform_id']; + $platformParameters = json_decode($linkId['platform_parameters'], true); + $platformName = ''; + + foreach ($notificationPlatformTable as $notificationPlatform) { + if ($notificationPlatform['id'] == $platformId) { + $platformName = $notificationPlatform['platform']; + break; + } + } - logger($logfile, 'notification request to ' . $platformData['name']); - logger($logfile, 'notification payload: ' . json_encode($payload)); + $logfile = $this->logpath . $platformName . '.log'; + logger($logfile, 'notification request to ' . $platformName); + logger($logfile, 'notification payload: ' . json_encode($payload)); - /* - Everything should return an array with code => ..., error => ... (if no error, just code is fine) - */ - switch ($platform) { - case 1: //-- Notifiarr - return $this->notifiarr($logfile, $payload); + switch ($platformId) { + case NotificationPlatforms::NOTIFIARR: + return $this->notifiarr($logfile, $platformParameters['apikey'], $payload); + } } } - public function getPlatforms() + public function getNotificationPlatformNameFromId($id, $platforms) { - return $this->platforms; + foreach ($platforms as $platform) { + if ($id == $platform['id']) { + return $platform['platform']; + } + } } - public function getNotificationPlatformFromId($platform) + public function getNotificationTriggerNameFromId($id, $triggers) { - return $this->platforms[$platform]; + foreach ($triggers as $trigger) { + if ($id == $trigger['id']) { + return $trigger['label']; + } + } } } diff --git a/root/app/www/public/classes/interfaces/Docker.php b/root/app/www/public/classes/interfaces/Docker.php new file mode 100644 index 0000000..25a1c31 --- /dev/null +++ b/root/app/www/public/classes/interfaces/Docker.php @@ -0,0 +1,50 @@ +containerGroupLinksTable) { + return $this->containerGroupLinksTable; + } + + $q = "SELECT * + FROM " . CONTAINER_GROUPS_LINK_TABLE; + $r = $this->query($q); + while ($row = $this->fetchAssoc($r)) { + $containerLinks[] = $row; + } + + $this->containerGroupLinksTable = $containerLinks; + return $containerLinks ?? []; + } + + public function getGroupLinkContainersFromGroupId($groupLinks, $containers, $groupId) + { + $groupContainers = []; + foreach ($groupLinks as $groupLink) { + if ($groupLink['group_id'] == $groupId) { + $groupContainers[] = $containers[$groupLink['container_id']]; + } + } + + return $groupContainers; + } + + public function addContainerGroupLink($groupId, $containerId) + { + $q = "INSERT INTO " . CONTAINER_GROUPS_LINK_TABLE . " + (`group_id`, `container_id`) + VALUES + ('" . intval($groupId) . "', '" . intval($containerId) . "')"; + $this->query($q); + + $this->containerGroupLinksTable = ''; + } + + public function removeContainerGroupLink($groupId, $containerId) + { + $q = "DELETE FROM " . CONTAINER_GROUPS_LINK_TABLE . " + WHERE group_id = '" . intval($groupId) . "' + AND container_id = '" . intval($containerId) . "'"; + $this->query($q); + + $this->containerGroupLinksTable = ''; + } + + public function deleteGroupLinks($groupId) + { + $q = "DELETE FROM " . CONTAINER_GROUPS_LINK_TABLE . " + WHERE group_id = " . $groupId; + $this->query($q); + + $this->containerGroupLinksTable = ''; + } +} diff --git a/root/app/www/public/classes/traits/Database/ContainerGroups.php b/root/app/www/public/classes/traits/Database/ContainerGroups.php new file mode 100644 index 0000000..f3f5104 --- /dev/null +++ b/root/app/www/public/classes/traits/Database/ContainerGroups.php @@ -0,0 +1,80 @@ +containerGroupsTable) { + return $this->containerGroupsTable; + } + + $q = "SELECT * + FROM " . CONTAINER_GROUPS_TABLE; + $r = $this->query($q); + while ($row = $this->fetchAssoc($r)) { + $containerGroups[] = $row; + } + + $this->containerGroupsTable = $containerGroups; + return $containerGroups ?? []; + } + + public function getContainerGroupFromHash($hash, $groups) + { + if (!$groups) { + $groups = $this->getContainerGroups(); + } + + foreach ($groups as $group) { + if ($group['hash'] == $hash) { + return $group; + } + } + + return []; + } + + public function updateContainerGroup($groupId, $fields = []) + { + $updateList = []; + foreach ($fields as $field => $val) { + $updateList[] = $field . ' = "' . $val . '"'; + } + + $q = "UPDATE " . CONTAINER_GROUPS_TABLE . " + SET " . implode(', ', $updateList) . " + WHERE id = '" . $groupId . "'"; + $this->query($q); + + $this->containerGroupsTable = ''; + } + + public function addContainerGroup($groupName) + { + $q = "INSERT INTO " . CONTAINER_GROUPS_TABLE . " + (`hash`, `name`) + VALUES + ('" . md5($groupName) . "', '" . $this->prepare($groupName) . "')"; + $this->query($q); + + $this->containerGroupsTable = ''; + return $this->insertId(); + } + + public function deleteContainerGroup($groupId) + { + $q = "DELETE FROM " . CONTAINER_GROUPS_TABLE . " + WHERE id = " . $groupId; + $this->query($q); + + $this->containerGroupsTable = ''; + $this->deleteGroupLinks($groupId); + } +} diff --git a/root/app/www/public/classes/traits/Database/ContainerSettings.php b/root/app/www/public/classes/traits/Database/ContainerSettings.php new file mode 100644 index 0000000..ecd83a4 --- /dev/null +++ b/root/app/www/public/classes/traits/Database/ContainerSettings.php @@ -0,0 +1,85 @@ +containersTable) { + return $this->containersTable; + } + + $q = "SELECT * + FROM " . CONTAINER_SETTINGS_TABLE; + $r = $this->query($q); + while ($row = $this->fetchAssoc($r)) { + $containers[$row['id']] = $row; + } + + $this->containersTable = $containers; + return $containers ?? []; + } + + public function getContainerFromHash($hash, $containers) + { + if (!$containers) { + $containers = $this->getContainers(); + } + + foreach ($containers as $container) { + if ($container['hash'] == $hash) { + return $container; + } + } + + return []; + } + + public function getContainer($hash) + { + $q = "SELECT * + FROM " . CONTAINER_SETTINGS_TABLE . " + WHERE hash = '" . $hash . "'"; + $r = $this->query($q); + + return $this->fetchAssoc($r); + } + + public function addContainer($fields = []) + { + $fieldList = $valList = []; + foreach ($fields as $field => $val) { + $fieldList[] = '`' . $field . '`'; + $valList[] = '"'. $val .'"'; + } + + $q = "INSERT INTO " . CONTAINER_SETTINGS_TABLE . " + (". implode(', ', $fieldList) .") + VALUES + (". implode(', ', $valList) .")"; + $this->query($q); + + $this->containersTable = ''; + } + + public function updateContainer($hash, $fields = []) + { + $updateList = []; + foreach ($fields as $field => $val) { + $updateList[] = $field . ' = "' . $val . '"'; + } + + $q = "UPDATE " . CONTAINER_SETTINGS_TABLE . " + SET " . implode(', ', $updateList) . " + WHERE hash = '" . $hash . "'"; + $this->query($q); + + $this->containersTable = ''; + } +} diff --git a/root/app/www/public/classes/traits/Database/NotificationLink.php b/root/app/www/public/classes/traits/Database/NotificationLink.php new file mode 100644 index 0000000..86b20aa --- /dev/null +++ b/root/app/www/public/classes/traits/Database/NotificationLink.php @@ -0,0 +1,81 @@ +notificationLinkTable) { + return $this->notificationLinkTable; + } + + $notificationLinkTable = []; + + $q = "SELECT * + FROM " . NOTIFICATION_LINK_TABLE; + $r = $this->query($q); + while ($row = $this->fetchAssoc($r)) { + $notificationLinkTable[$row['id']] = $row; + } + + $this->notificationLinkTable = $notificationLinkTable; + return $notificationLinkTable; + } + + public function updateNotificationLink($linkId, $triggerIds, $platformParameters, $senderName) + { + $q = "UPDATE " . NOTIFICATION_LINK_TABLE . " + SET name = '" . $this->prepare($senderName) . "', platform_parameters = '" . json_encode($platformParameters) . "', trigger_ids = '" . json_encode($triggerIds) . "' + WHERE id = '" . intval($linkId) . "'"; + $this->query($q); + + $this->notificationLinkTable = ''; + return $this->getNotificationLinks(); + } + + public function addNotificationLink($platformId, $triggerIds, $platformParameters, $senderName) + { + $q = "INSERT INTO " . NOTIFICATION_LINK_TABLE . " + (`name`, `platform_id`, `platform_parameters`, `trigger_ids`) + VALUES + ('" . $this->prepare($senderName) . "', '" . intval($platformId) . "', '" . json_encode($platformParameters) . "', '" . json_encode($triggerIds) . "')"; + $this->query($q); + + $this->notificationLinkTable = ''; + return $this->getNotificationLinks(); + } + + function deleteNotificationLink($linkId) + { + $q = "DELETE FROM " . NOTIFICATION_LINK_TABLE . " + WHERE id = " . intval($linkId); + $this->query($q); + + $this->notificationLinkTable = ''; + return $this->getNotificationLinks(); + } + + public function getNotificationLinkPlatformFromName($name) + { + $notificationLinks = $this->getNotificationLinks(); + $notificationTrigger = $this->getNotificationTriggerFromName($name); + + foreach ($notificationLinks as $notificationLink) { + if ($notificationLink['name'] == $name) { + $triggers = makeArray(json_decode($notificationLink['trigger_ids'], true)); + + foreach ($triggers as $trigger) { + if ($trigger == $notificationTrigger['id']) { + return $notificationLink['platform_id']; + } + } + } + } + } +} diff --git a/root/app/www/public/classes/traits/Database/NotificationPlatform.php b/root/app/www/public/classes/traits/Database/NotificationPlatform.php new file mode 100644 index 0000000..853452e --- /dev/null +++ b/root/app/www/public/classes/traits/Database/NotificationPlatform.php @@ -0,0 +1,30 @@ +notificationPlatformTable) { + return $this->notificationPlatformTable; + } + + $notificationPlatformTable = []; + + $q = "SELECT * + FROM " . NOTIFICATION_PLATFORM_TABLE; + $r = $this->query($q); + while ($row = $this->fetchAssoc($r)) { + $notificationPlatformTable[$row['id']] = $row; + } + + $this->notificationPlatformTable = $notificationPlatformTable; + return $notificationPlatformTable; + } +} diff --git a/root/app/www/public/classes/traits/Database/NotificationTrigger.php b/root/app/www/public/classes/traits/Database/NotificationTrigger.php new file mode 100644 index 0000000..9abe959 --- /dev/null +++ b/root/app/www/public/classes/traits/Database/NotificationTrigger.php @@ -0,0 +1,61 @@ +notificationTriggersTable) { + return $this->notificationTriggersTable; + } + + $notificationTriggersTable = []; + + $q = "SELECT * + FROM " . NOTIFICATION_TRIGGER_TABLE; + $r = $this->query($q); + while ($row = $this->fetchAssoc($r)) { + $notificationTriggersTable[$row['id']] = $row; + } + + $this->notificationTriggersTable = $notificationTriggersTable; + return $notificationTriggersTable; + } + + public function getNotificationTriggerFromName($name) + { + $triggers = $this->getNotificationTriggers(); + + foreach ($triggers as $trigger) { + if (str_compare($name, $trigger['name'])) { + return $trigger; + } + } + + return []; + } + + public function isNotificationTriggerEnabled($name) + { + $notificationLinks = $this->getNotificationLinks(); + $notificationTrigger = $this->getNotificationTriggerFromName($name); + + foreach ($notificationLinks as $notificationLink) { + $triggers = makeArray(json_decode($notificationLink['trigger_ids'], true)); + + foreach ($triggers as $trigger) { + if ($trigger == $notificationTrigger['id']) { + return true; + } + } + } + + return false; + } +} diff --git a/root/app/www/public/classes/traits/Database/Servers.php b/root/app/www/public/classes/traits/Database/Servers.php new file mode 100644 index 0000000..bddfc55 --- /dev/null +++ b/root/app/www/public/classes/traits/Database/Servers.php @@ -0,0 +1,56 @@ +serversTable; + } + + foreach ($serverList as $serverId => $serverSettings) { + switch (true) { + case $serverSettings['remove']: + $q = "DELETE FROM " . SERVERS_TABLE . " + WHERE id = " . $serverId; + break; + case !$serverId: + $q = "INSERT INTO " . SERVERS_TABLE . " + (`name`, `url`, `apikey`) + VALUES + ('" . $this->prepare($serverSettings['name']) . "', '" . $this->prepare($serverSettings['url']) . "', '" . $this->prepare($serverSettings['apikey']) . "')"; + break; + default: + $q = "UPDATE " . SERVERS_TABLE . " + SET name = '" . $this->prepare($serverSettings['name']) . "', url = '" . $this->prepare($serverSettings['url']) . "', apikey = '" . $this->prepare($serverSettings['apikey']) . "' + WHERE id = " . $serverId; + break; + } + $this->query($q); + } + + return $this->getServers(); + } + + public function getServers() + { + $serversTable = []; + + $q = "SELECT * + FROM " . SERVERS_TABLE; + $r = $this->query($q); + while ($row = $this->fetchAssoc($r)) { + $serversTable[$row['id']] = $row; + } + + $this->serversTable = $serversTable; + return $serversTable; + } +} diff --git a/root/app/www/public/classes/traits/Database/Settings.php b/root/app/www/public/classes/traits/Database/Settings.php new file mode 100644 index 0000000..7e013f4 --- /dev/null +++ b/root/app/www/public/classes/traits/Database/Settings.php @@ -0,0 +1,69 @@ +query($q); + $row = $this->fetchAssoc($r); + + return $row['value']; + } + + public function setSetting($field, $value) + { + $q = "UPDATE " . SETTINGS_TABLE . " + SET value = '" . $this->prepare($value) . "' + WHERE name = '" . $field . "'"; + $this->query($q); + + return $this->getSettings(); + } + + public function setSettings($newSettings = [], $currentSettings = []) + { + if (!$newSettings) { + return; + } + + if (!$currentSettings) { + $currentSettings = $this->getSettings(); + } + + foreach ($newSettings as $field => $value) { + if ($currentSettings[$field] != $value) { + $q = "UPDATE " . SETTINGS_TABLE . " + SET value = '" . $this->prepare($value) . "' + WHERE name = '" . $field . "'"; + $this->query($q); + } + } + + return $this->getSettings(); + } + + public function getSettings() + { + $settingsTable = []; + + $q = "SELECT * + FROM " . SETTINGS_TABLE ; + $r = $this->query($q); + while ($row = $this->fetchAssoc($r)) { + $settingsTable[$row['name']] = $row['value']; + } + + $this->settingsTable = $settingsTable; + return $settingsTable; + } +} diff --git a/root/app/www/public/classes/traits/Docker/API.php b/root/app/www/public/classes/traits/Docker/API.php index 1017695..ce304d5 100644 --- a/root/app/www/public/classes/traits/Docker/API.php +++ b/root/app/www/public/classes/traits/Docker/API.php @@ -13,7 +13,7 @@ $apiResponse = $docker->apiCurl($request, 'post'); */ -trait API +trait DockersApi { public function apiIsAvailable() { @@ -26,7 +26,7 @@ public function apiIsAvailable() return false; } - + if (file_exists('/var/run/docker.sock')) { return true; } diff --git a/root/app/www/public/classes/traits/Docker/Container.php b/root/app/www/public/classes/traits/Docker/Container.php index 5580e5a..450bbcd 100644 --- a/root/app/www/public/classes/traits/Docker/Container.php +++ b/root/app/www/public/classes/traits/Docker/Container.php @@ -29,18 +29,15 @@ public function startContainer($container) public function stopContainer($container) { - //-- GET CURRENT SETTINGS FILE - $settingsFile = getServerFile('settings'); - $settingsFile = $settingsFile['file']; - - $nameHash = md5($container); - $delay = (intval($settingsFile['containers'][$nameHash]['shutdownDelaySeconds']) >= 5 ? ' -t ' . $settingsFile['containers'][$nameHash]['shutdownDelaySeconds'] : ' -t 120'); + $nameHash = md5($container); + $container = $this->api->request('database-getContainerFromHash', ['hash' => $nameHash]); + $delay = intval($container['shutdownDelaySeconds']) >= 5 ? ' -t ' . $container['shutdownDelaySeconds'] : ' -t 120'; - if ($settingsFile['containers'][$nameHash]['shutdownDelay']) { + if ($container['shutdownDelay']) { logger(SYSTEM_LOG, 'stopContainer() delaying stop command for container ' . $container . ' with ' . $delay); } - $cmd = sprintf(DockerSock::STOP_CONTAINER, $this->shell->prepare($container), ($settingsFile['containers'][$nameHash]['shutdownDelay'] ? $this->shell->prepare($delay) : '')); + $cmd = sprintf(DockerSock::STOP_CONTAINER, $this->shell->prepare($container), ($container['shutdownDelay'] ? $this->shell->prepare($delay) : '')); return $this->shell->exec($cmd . ' 2>&1'); } diff --git a/root/app/www/public/classes/traits/Notifications/Templates.php b/root/app/www/public/classes/traits/Notifications/Templates.php new file mode 100644 index 0000000..e888800 --- /dev/null +++ b/root/app/www/public/classes/traits/Notifications/Templates.php @@ -0,0 +1,64 @@ + '', + 'container' => '', + 'restarted' => '' + ]; + case 'prune': + return [ + 'event' => '', + 'network' => '', + 'volume' => '', + 'image' => '', + 'imageList' => '' + ]; + case 'added': + case 'removed': + case 'stateChange': + return [ + 'event' => '', + 'changes' => '', + 'added' => '', + 'removed' => '' + ]; + case 'test': + return [ + 'event' => '', + 'title' => '', + 'message' => '' + ]; + case 'updated': + case 'updates': + return [ + 'event' => '', + 'available' => '', + 'updated' => '' + ]; + case 'cpuHigh': + case 'memHigh': + return [ + 'event' => '', + 'cpu' => '', + 'cpuThreshold' => '', + 'mem' => '', + 'memThreshold' => '' + ]; + default: + return []; + } + } +} diff --git a/root/app/www/public/classes/traits/Notifications/notifiarr.php b/root/app/www/public/classes/traits/Notifications/notifiarr.php index a06dc15..ddcf7c1 100644 --- a/root/app/www/public/classes/traits/Notifications/notifiarr.php +++ b/root/app/www/public/classes/traits/Notifications/notifiarr.php @@ -9,9 +9,9 @@ trait Notifiarr { - public function notifiarr($logfile, $payload) + public function notifiarr($logfile, $apikey, $payload) { - $headers = ['x-api-key:' . $this->platformSettings[1]['apikey']]; + $headers = ['x-api-key:' . $apikey]; $url = 'https://notifiarr.com/api/v1/notification/dockwatch'; $curl = curl($url, $headers, 'POST', json_encode($payload)); diff --git a/root/app/www/public/crons/health.php b/root/app/www/public/crons/health.php index baa0b5a..174867d 100644 --- a/root/app/www/public/crons/health.php +++ b/root/app/www/public/crons/health.php @@ -14,7 +14,7 @@ logger(CRON_HEALTH_LOG, 'run ->'); echo date('c') . ' Cron: health ->' . "\n"; -if ($settingsFile['tasks']['health']['disabled']) { +if ($settingsTable['taskHealthDisabled']) { logger(CRON_HEALTH_LOG, 'Cron cancelled: disabled in tasks menu'); logger(CRON_HEALTH_LOG, 'run <-'); echo date('c') . ' Cron: health cancelled, disabled in tasks menu' . "\n"; @@ -22,7 +22,7 @@ exit(); } -if (!$settingsFile['global']['restartUnhealthy'] && !$settingsFile['notifications']['triggers']['health']['active']) { +if (!$settingsTable['restartUnhealthy'] && !$database->isNotificationTriggerEnabled('health')) { logger(CRON_HEALTH_LOG, 'Cron cancelled: restart and notify disabled'); logger(CRON_HEALTH_LOG, 'run <-'); echo date('c') . ' Cron health cancelled: restart unhealthy and notify disabled' . "\n"; @@ -69,21 +69,23 @@ logger(CRON_HEALTH_LOG, '$unhealthy=' . json_encode($unhealthy, JSON_UNESCAPED_SLASHES)); if ($unhealthy) { + $containersTable = $database->getContainers(); + foreach ($unhealthy as $nameHash => $container) { $notify = false; if ($container['restart'] || $container['notify']) { continue; } - - $skipActions = skipContainerActions($container['name'], $skipContainerActions); + $thisContainer = $database->getContainerFromHash($nameHash, $containersTable); + $skipActions = skipContainerActions($container['name'], $skipContainerActions); if ($skipActions) { logger(CRON_HEALTH_LOG, 'skipping: ' . $container['name'] . ', blacklisted (no state changes) container'); continue; } - if (!$settingsFile['containers'][$nameHash]['restartUnhealthy']) { + if (!$thisContainer['restartUnhealthy']) { logger(CRON_HEALTH_LOG, 'skipping: ' . $container['name'] . ', restart unhealthy option not enabled'); continue; } @@ -91,9 +93,11 @@ $unhealthy[$nameHash]['notify'] = 0; $unhealthy[$nameHash]['restart'] = 0; - if ($settingsFile['notifications']['triggers']['health']['active'] && $settingsFile['notifications']['triggers']['health']['platform']) { + if ($database->isNotificationTriggerEnabled('health')) { $unhealthy[$nameHash]['notify'] = time(); $notify = true; + } else { + logger(CRON_HEALTH_LOG, 'skipping notification for \'' . $container['name'] . '\', no notification senders with the health event enabled'); } $dependencies = $dependencyFile[$container['name']]['containers']; @@ -119,16 +123,18 @@ } } - if ($settingsFile['containers'][$nameHash]['disableNotifications']) { + if ($notify && $thisContainer['disableNotifications']) { logger(CRON_HEALTH_LOG, 'skipping notification for \'' . $container['name'] . '\', container set to not notify'); $notify = false; } if ($notify) { logger(CRON_HEALTH_LOG, 'sending notification for \'' . $container['name'] . '\''); + $payload = ['event' => 'health', 'container' => $container['name'], 'restarted' => $unhealthy[$nameHash]['restart']]; + $notifications->notify(0, 'health', $payload); + logger(CRON_STATE_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['health']['platform'], $payload); } } @@ -140,4 +146,4 @@ } echo date('c') . ' Cron: health <-' . "\n"; -logger(CRON_HEALTH_LOG, 'run <-'); \ No newline at end of file +logger(CRON_HEALTH_LOG, 'run <-'); diff --git a/root/app/www/public/crons/housekeeper.php b/root/app/www/public/crons/housekeeper.php index 2720c56..155e331 100644 --- a/root/app/www/public/crons/housekeeper.php +++ b/root/app/www/public/crons/housekeeper.php @@ -14,7 +14,7 @@ logger(CRON_HOUSEKEEPER_LOG, 'run ->'); echo date('c') . ' Cron: housekeeper ->' . "\n"; -if ($settingsFile['tasks']['housekeeping']['disabled']) { +if ($settingsTable['tasksHousekeepingDisabled']) { logger(CRON_HOUSEKEEPER_LOG, 'Cron cancelled: disabled in tasks menu'); logger(CRON_HOUSEKEEPER_LOG, 'run <-'); echo date('c') . ' Cron: housekeeper cancelled, disabled in tasks menu' . "\n"; @@ -33,7 +33,7 @@ } logger(CRON_HOUSEKEEPER_LOG, 'removing \'' . TMP_PATH . $file . '\''); - unlink(TMP_PATH . $file); + $shell->exec('rm -rf ' . TMP_PATH . $file); } closedir($dir); } @@ -44,12 +44,12 @@ [ 'crons' => [[ 'message' => 'Cron log file cleanup (daily @ midnight)', - 'length' => ($settingsFile['global']['cronLogLength'] <= 1 ? 1 : $settingsFile['global']['cronLogLength']) + 'length' => ($settingsTable['cronLogLength'] <= 1 ? 1 : $settingsTable['cronLogLength']) ]] ],[ 'notifications' => [[ 'message' => 'Notification log file cleanup (daily @ midnight)', - 'length' => ($settingsFile['global']['notificationLogLength'] <= 1 ? 1 : $settingsFile['global']['notificationLogLength']) + 'length' => ($settingsTable['notificationLogLength'] <= 1 ? 1 : $settingsTable['notificationLogLength']) ]] ],[ 'system' => [[ @@ -59,11 +59,11 @@ ],[ 'type' => 'ui', 'message' => 'UI log file cleanup (daily @ midnight)', - 'length' => ($settingsFile['global']['uiLogLength'] <= 1 ? 1 : $settingsFile['global']['uiLogLength']) + 'length' => ($settingsTable['uiLogLength'] <= 1 ? 1 : $settingsTable['uiLogLength']) ],[ 'type' => 'api', 'message' => 'API log file cleanup (daily @ midnight)', - 'length' => ($settingsFile['global']['apiLogLength'] <= 1 ? 1 : $settingsFile['global']['apiLogLength']) + 'length' => ($settingsTable['apiLogLength'] <= 1 ? 1 : $settingsTable['apiLogLength']) ]] ] ]; @@ -87,7 +87,7 @@ if ($daysBetween > $settings['length']) { logger(CRON_HOUSEKEEPER_LOG, 'removing logfile'); - unlink($thisDir . $log); + $shell->exec('rm -rf ' . $thisDir . $log); } } } @@ -98,38 +98,40 @@ } //-- BACKUPS -createDirectoryTree(BACKUP_PATH . date('Ymd')); -$defines = get_defined_constants(); -foreach ($defines as $define => $defineValue) { - if (str_contains($define, '_FILE') && str_contains_all($defineValue, ['config/', '.json'])) { - $backupFiles[] = $defineValue; +if (!is_dir(BACKUP_PATH . date('Ymd'))) { + createDirectoryTree(BACKUP_PATH . date('Ymd')); + $defines = get_defined_constants(); + foreach ($defines as $define => $defineValue) { + if (str_contains($define, '_FILE') && str_contains_all($defineValue, ['config/', '.json'])) { + $backupFiles[] = $defineValue; + } } -} -foreach ($backupFiles as $backupFile) { - $file = explode('/', $backupFile); - $file = end($file); + foreach ($backupFiles as $backupFile) { + $file = explode('/', $backupFile); + $file = end($file); - logger(CRON_HOUSEKEEPER_LOG, 'backup file \'' . APP_DATA_PATH . $file . '\' to \'' . BACKUP_PATH . $file . '\''); - copy(APP_DATA_PATH . $file, BACKUP_PATH . date('Ymd') . '/' .$file); + logger(CRON_HOUSEKEEPER_LOG, 'backup file \'' . APP_DATA_PATH . $file . '\' to \'' . BACKUP_PATH . $file . '\''); + copy(APP_DATA_PATH . $file, BACKUP_PATH . date('Ymd') . '/' . $file); + } } $dir = opendir(BACKUP_PATH); while ($backup = readdir($dir)) { if (!is_dir(BACKUP_PATH . $backup)) { logger(CRON_HOUSEKEEPER_LOG, 'removing backup: ' . BACKUP_PATH . $backup); - unlink(BACKUP_PATH . $backup); + $shell->exec('rm -rf ' . BACKUP_PATH . $backup); } else { if ($backup[0] != '.') { $daysBetween = daysBetweenDates($backup, date('Ymd')); - if ($daysBetween > 3) { + if ($daysBetween >= APP_BACKUPS) { logger(CRON_HOUSEKEEPER_LOG, 'removing backup: ' . BACKUP_PATH . $backup); - unlink(BACKUP_PATH . $backup); + $shell->exec('rm -rf ' . BACKUP_PATH . $backup); } } } } echo date('c') . ' Cron: housekeeper <-' . "\n"; -logger(CRON_HOUSEKEEPER_LOG, 'run <-'); \ No newline at end of file +logger(CRON_HOUSEKEEPER_LOG, 'run <-'); diff --git a/root/app/www/public/crons/prune.php b/root/app/www/public/crons/prune.php index 227e4f3..1a3e6b4 100644 --- a/root/app/www/public/crons/prune.php +++ b/root/app/www/public/crons/prune.php @@ -16,7 +16,7 @@ logger(CRON_PRUNE_LOG, 'run ->'); echo date('c') . ' Cron: prune ->' . "\n"; -if ($settingsFile['tasks']['prune']['disabled']) { +if ($settingsTable['tasksPruneDisabled']) { logger(CRON_PRUNE_LOG, 'Cron cancelled: disabled in tasks menu'); logger(CRON_PRUNE_LOG, 'run <-'); echo date('c') . ' Cron: prune cancelled, disabled in tasks menu' . "\n"; @@ -24,7 +24,7 @@ exit(); } -$frequencyHour = $settingsFile['global']['autoPruneHour'] ? $settingsFile['global']['autoPruneHour'] : '12'; +$frequencyHour = $settingsTable['autoPruneHour'] ? $settingsTable['autoPruneHour'] : '12'; if ($frequencyHour !== date('G')) { logger(CRON_PRUNE_LOG, 'Cron: skipped, frequency setting will run at hour ' . $frequencyHour); logger(CRON_PRUNE_LOG, 'run <-'); @@ -43,7 +43,7 @@ logger(CRON_PRUNE_LOG, 'volumes=' . json_encode($volumes)); logger(CRON_PRUNE_LOG, 'networks=' . json_encode($networks)); -if ($settingsFile['global']['autoPruneImages']) { +if ($settingsTable['autoPruneImages']) { if ($images) { foreach ($images as $image) { $imagePrune[] = $image['ID']; @@ -54,7 +54,7 @@ logger(CRON_PRUNE_LOG, 'Auto prune images disabled'); } -if ($settingsFile['global']['autoPruneVolumes']) { +if ($settingsTable['autoPruneVolumes']) { if ($volumes) { foreach ($volumes as $volume) { $volumePrune[] = $volume['Name']; @@ -64,7 +64,7 @@ logger(CRON_PRUNE_LOG, 'Auto prune volumes disabled'); } -if ($settingsFile['global']['autoPruneNetworks']) { +if ($settingsTable['autoPruneNetworks']) { if ($networks) { foreach ($networks as $network) { $networkPrune[] = $network['ID']; @@ -98,11 +98,12 @@ logger(CRON_PRUNE_LOG, 'result: ' . $prune); } -if ($settingsFile['notifications']['triggers']['prune']['active'] && (count($volumePrune) > 0 || count($imagePrune) > 0 || count($networkPrune) > 0)) { +if ($database->isNotificationTriggerEnabled('prune') && (count($volumePrune) > 0 || count($imagePrune) > 0 || count($networkPrune) > 0)) { $payload = ['event' => 'prune', 'network' => count($networkPrune), 'volume' => count($volumePrune), 'image' => count($imagePrune), 'imageList' => $imageList]; + $notifications->notify(0, 'prune', $payload); + logger(CRON_PRUNE_LOG, 'Notification payload: ' . json_encode($payload)); - $notifications->notify($settingsFile['notifications']['triggers']['prune']['platform'], $payload); } echo date('c') . ' Cron: prune <-' . "\n"; -logger(CRON_PRUNE_LOG, 'run <-'); \ No newline at end of file +logger(CRON_PRUNE_LOG, 'run <-'); diff --git a/root/app/www/public/crons/pulls.php b/root/app/www/public/crons/pulls.php index 4cbd50d..f387813 100644 --- a/root/app/www/public/crons/pulls.php +++ b/root/app/www/public/crons/pulls.php @@ -14,9 +14,9 @@ logger(SYSTEM_LOG, 'Cron: running pulls'); logger(CRON_PULLS_LOG, 'run ->'); -echo date('c') . ' Cron: pulls' . "\n"; +echo date('c') . ' Cron: pulls ->' . "\n"; -if ($settingsFile['tasks']['pulls']['disabled']) { +if ($settingsTable['tasksPullsDisabled']) { logger(CRON_PULLS_LOG, 'Cron cancelled: disabled in tasks menu'); logger(CRON_PULLS_LOG, 'run <-'); echo date('c') . ' Cron: pulls cancelled, disabled in tasks menu' . "\n"; @@ -24,14 +24,16 @@ exit(); } -$updateSettings = $settingsFile['containers']; -$notify = []; -$startStamp = new DateTime(); +$containersTable = $database->getContainers(); +$notify = []; +$startStamp = new DateTime(); -if ($updateSettings) { +if ($containersTable) { $imagesUpdated = []; - foreach ($updateSettings as $containerHash => $containerSettings) { + foreach ($containersTable as $containerSettings) { + $containerHash = $containerSettings['hash']; + //-- SET TO IGNORE if (!$containerSettings['updates']) { continue; @@ -132,7 +134,7 @@ echo date('c') . ' ' . $msg . "\n"; } - if (!$dockerCommunicateAPI) { + if (!$isDockerApiAvailable) { $msg = 'Skipping container update: ' . $containerState['Names'] . ' (docker engine api access is not available)'; logger(CRON_PULLS_LOG, $msg); echo date('c') . ' ' . $msg . "\n"; @@ -296,7 +298,7 @@ } } - if ($settingsFile['notifications']['triggers']['updated']['active'] && !$settingsFile['containers'][$containerHash]['disableNotifications']) { + if ($database->isNotificationTriggerEnabled('updated') && !$containerSettings['disableNotifications']) { $notify['updated'][] = [ 'container' => $containerState['Names'], 'image' => $image, @@ -309,14 +311,14 @@ logger(CRON_PULLS_LOG, $msg); echo date('c') . ' ' . $msg . "\n"; - if ($settingsFile['notifications']['triggers']['updated']['active'] && !$settingsFile['containers'][$containerHash]['disableNotifications']) { + if ($database->isNotificationTriggerEnabled('updated') && !$containerSettings['disableNotifications']) { $notify['failed'][] = ['container' => $containerState['Names']]; } } } break; case 2: //-- Check for updates - if ($settingsFile['notifications']['triggers']['updates']['active'] && $inspectImage[0]['Id'] != $inspectContainer[0]['Image'] && !$settingsFile['containers'][$containerHash]['disableNotifications']) { + if ($database->isNotificationTriggerEnabled('updates') && !$containerSettings['disableNotifications'] && $inspectImage[0]['Id'] != $inspectContainer[0]['Image']) { $notify['available'][] = ['container' => $containerState['Names']]; } break; @@ -334,26 +336,28 @@ if ($notify) { //-- IF THEY USE THE SAME PLATFORM, COMBINE THEM - if ($settingsFile['notifications']['triggers']['updated']['platform'] == $settingsFile['notifications']['triggers']['updates']['platform']) { + if ($database->getNotificationLinkPlatformFromName('updated') == $database->getNotificationLinkPlatformFromName('updates')) { $payload = ['event' => 'updates', 'available' => $notify['available'], 'updated' => $notify['updated']]; + $notifications->notify(0, 'updates', $payload); + logger(CRON_PULLS_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['updated']['platform'], $payload); } else { if ($notify['available']) { $payload = ['event' => 'updates', 'available' => $notify['available']]; + $notifications->notify(0, 'updates', $payload); + logger(CRON_PULLS_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['updated']['platform'], $payload); } - if ($notify['usage']['mem']) { + if ($notify['updated']) { $payload = ['event' => 'updates', 'updated' => $notify['updated']]; + $notifications->notify(0, 'updated', $payload); + logger(CRON_PULLS_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['updates']['platform'], $payload); } } } } - echo date('c') . ' Cron: pulls <-' . "\n"; -logger(CRON_PULLS_LOG, 'run <-'); \ No newline at end of file +logger(CRON_PULLS_LOG, 'run <-'); diff --git a/root/app/www/public/crons/sse.php b/root/app/www/public/crons/sse.php index f4423e8..6157a22 100644 --- a/root/app/www/public/crons/sse.php +++ b/root/app/www/public/crons/sse.php @@ -14,7 +14,7 @@ logger(CRON_SSE_LOG, 'run ->'); echo date('c') . ' Cron: sse' . "\n"; -if (!$settingsFile['global']['sseEnabled']) { +if (!$settingsTable['sseEnabled']) { logger(CRON_SSE_LOG, 'Cron cancelled: disabled in tasks menu'); logger(CRON_SSE_LOG, 'run <-'); echo date('c') . ' Cron: sse cancelled, disabled in tasks menu' . "\n"; diff --git a/root/app/www/public/crons/state.php b/root/app/www/public/crons/state.php index b72a4b3..39f4bad 100644 --- a/root/app/www/public/crons/state.php +++ b/root/app/www/public/crons/state.php @@ -14,7 +14,7 @@ logger(CRON_STATE_LOG, 'run ->'); echo date('c') . ' Cron: state' . "\n"; -if ($settingsFile['tasks']['state']['disabled']) { +if ($settingsTable['taskStateDisabled']) { logger(CRON_STATE_LOG, 'Cron cancelled: disabled in tasks menu'); logger(CRON_STATE_LOG, 'run <-'); echo date('c') . ' Cron: state cancelled, disabled in tasks menu' . "\n"; @@ -36,10 +36,6 @@ logger(CRON_STATE_LOG, 'STATE_FILE update skipped, $currentStates empty'); } -//-- GET CURRENT SETTINGS FILE -$settingsFile = getServerFile('settings'); -$settingsFile = $settingsFile['file']; - logger(CRON_STATE_LOG, 'previousStates: ' . json_encode($previousStates, JSON_UNESCAPED_SLASHES)); logger(CRON_STATE_LOG, 'currentStates: ' . json_encode($currentStates, JSON_UNESCAPED_SLASHES)); @@ -55,52 +51,48 @@ logger(CRON_STATE_LOG, 'currentContainers: ' . json_encode($currentContainers, JSON_UNESCAPED_SLASHES)); //-- CHECK FOR ADDED CONTAINERS +$containersTable = $database->getContainers(); foreach ($currentContainers as $currentContainer) { if (!in_array($currentContainer, $previousContainers)) { - $containerHash = md5($currentContainer); - - $updates = is_array($settingsFile['global']) && array_key_exists('updates', $settingsFile['global']) ? $settingsFile['global']['updates'] : 3; //-- CHECK ONLY FALLBACK - $frequency = is_array($settingsFile['global']) && array_key_exists('updatesFrequency', $settingsFile['global']) ? $settingsFile['global']['updatesFrequency'] : DEFAULT_CRON; //-- DAILY FALLBACK - $settingsFile['containers'][$containerHash] = ['updates' => $updates, 'frequency' => $frequency]; + $containerHash = md5($currentContainer); + $container = $database->getContainerGroupFromHash($containerHash, $containersTable); + $updates = $settingsTable['updates'] ?: 3; //-- CHECK ONLY FALLBACK + $frequency = $settingsTable['updatesFrequency'] ?: DEFAULT_CRON; //-- DAILY FALLBACK + $added[] = ['container' => $currentContainer]; - if (!$settingsFile['containers'][$containerHash]['disableNotifications']) { - $added[] = ['container' => $currentContainer]; - } + $database->addContainer(['hash' => $containerHash, 'updates' => $updates, 'frequency' => $frequency]); } } -if ($added && $settingsFile['notifications']['triggers']['added']['active']) { +if ($added && $database->isNotificationTriggerEnabled('added')) { $notify['state']['added'] = $added; + logger(CRON_STATE_LOG, 'Added containers: ' . json_encode($added, JSON_UNESCAPED_SLASHES)); } logger(CRON_STATE_LOG, 'Added containers: ' . json_encode($added, JSON_UNESCAPED_SLASHES)); //-- CHECK FOR REMOVED CONTAINERS foreach ($previousContainers as $previousContainer) { if (!in_array($previousContainer, $currentContainers)) { - $containerHash = md5($previousContainer); + $containerHash = md5($previousContainer); + $container = $database->getContainerGroupFromHash($containerHash, $containersTable); - unset($settingsFile['containers'][$containerHash]); - - if (!$settingsFile['containers'][$containerHash]['disableNotifications']) { + if (!$container['disableNotifications']) { $removed[] = ['container' => $previousContainer]; } } } -if ($removed && $settingsFile['notifications']['triggers']['removed']['active']) { - $notify['state']['removed'] = $removed; -} -logger(CRON_STATE_LOG, 'Removed containers: ' . json_encode($removed, JSON_UNESCAPED_SLASHES)); -if ($added || $removed) { - logger(CRON_STATE_LOG, 'updating settings file: ' . json_encode($settingsFile, JSON_UNESCAPED_SLASHES)); - setServerFile('settings', $settingsFile); +if ($removed && $database->isNotificationTriggerEnabled('removed')) { + $notify['state']['removed'] = $removed; + logger(CRON_STATE_LOG, 'Removed containers: ' . json_encode($removed, JSON_UNESCAPED_SLASHES)); } //-- CHECK FOR STATE CHANGED CONTAINERS foreach ($currentStates as $currentState) { foreach ($previousStates as $previousState) { - $containerHash = md5($currentState['Names']); + $containerHash = md5($currentState['Names']); + $container = $database->getContainerGroupFromHash($containerHash, $containersTable); - if ($settingsFile['notifications']['triggers']['stateChange']['active'] && $currentState['Names'] == $previousState['Names'] && !$settingsFile['containers'][$containerHash]['disableNotifications']) { + if ($database->isNotificationTriggerEnabled('stateChange') && !$container['disableNotifications'] && $currentState['Names'] == $previousState['Names']) { if ($previousState['State'] != $currentState['State']) { $notify['state']['changed'][] = ['container' => $currentState['Names'], 'previous' => $previousState['State'], 'current' => $currentState['State']]; } @@ -110,30 +102,31 @@ logger(CRON_STATE_LOG, 'State changed containers: ' . json_encode($notify['state']['changed'], JSON_UNESCAPED_SLASHES)); foreach ($currentStates as $currentState) { - $containerHash = md5($currentState['Names']); + $containerHash = md5($currentState['Names']); + $container = $database->getContainerGroupFromHash($containerHash, $containersTable); //-- CHECK FOR HIGH CPU USAGE CONTAINERS - if ($settingsFile['notifications']['triggers']['cpuHigh']['active'] && floatval($settingsFile['global']['cpuThreshold']) > 0 && !$settingsFile['containers'][$containerHash]['disableNotifications']) { + if ($database->isNotificationTriggerEnabled('cpuHigh') && !$container['disableNotifications'] && floatval($settingsTable['cpuThreshold']) > 0) { if ($currentState['stats']['CPUPerc']) { $cpu = floatval(str_replace('%', '', $currentState['stats']['CPUPerc'])); - $cpuAmount = intval($settingsFile['global']['cpuAmount']); + $cpuAmount = intval($settingsTable['cpuAmount']); if ($cpuAmount > 0) { $cpu = number_format(($cpu / $cpuAmount), 2); } - if ($cpu > floatval($settingsFile['global']['cpuThreshold'])) { + if ($cpu > floatval($settingsTable['cpuThreshold'])) { $notify['usage']['cpu'][] = ['container' => $currentState['Names'], 'usage' => $cpu]; } } } //-- CHECK FOR HIGH MEMORY USAGE CONTAINERS - if ($settingsFile['notifications']['triggers']['memHigh']['active'] && floatval($settingsFile['global']['memThreshold']) > 0 && !$settingsFile['containers'][$containerHash]['disableNotifications']) { + if ($database->isNotificationTriggerEnabled('memHigh') && !$container['disableNotifications'] && floatval($settingsTable['memThreshold']) > 0) { if ($currentState['stats']['MemPerc']) { $mem = floatval(str_replace('%', '', $currentState['stats']['MemPerc'])); - if ($mem > floatval($settingsFile['global']['memThreshold'])) { + if ($mem > floatval($settingsTable['memThreshold'])) { $notify['usage']['mem'][] = ['container' => $currentState['Names'], 'usage' => $mem]; } } @@ -154,51 +147,53 @@ if ($notify['state']) { //-- IF THEY USE THE SAME PLATFORM, COMBINE THEM - if ($settingsFile['notifications']['triggers']['stateChange']['platform'] == $settingsFile['notifications']['triggers']['added']['platform'] && $settingsFile['notifications']['triggers']['stateChange']['platform'] == $settingsFile['notifications']['triggers']['removed']['platform']) { + if ( + $database->getNotificationLinkPlatformFromName('stateChange') == $database->getNotificationLinkPlatformFromName('added') && + $database->getNotificationLinkPlatformFromName('stateChange') == $database->getNotificationLinkPlatformFromName('removed') + ) { $payload = ['event' => 'state', 'changes' => $notify['state']['changed'], 'added' => $notify['state']['added'], 'removed' => $notify['state']['removed']]; + $notifications->notify(0, 'stateChange', $payload); + logger(CRON_STATE_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['stateChange']['platform'], $payload); } else { if ($notify['state']['changed']) { $payload = ['event' => 'state', 'changes' => $notify['state']['changed']]; + $notifications->notify(0, 'stateChange', $payload); + logger(CRON_STATE_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['stateChange']['platform'], $payload); } if ($notify['state']['added']) { $payload = ['event' => 'state', 'added' => $notify['state']['added']]; + $notifications->notify(0, 'added', $payload); + logger(CRON_STATE_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['added']['platform'], $payload); } if ($notify['state']['removed']) { $payload = ['event' => 'state', 'removed' => $notify['state']['removed']]; + $notifications->notify(0, 'removed', $payload); + logger(CRON_STATE_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['removed']['platform'], $payload); } } } if ($notify['usage']) { - //-- IF THEY USE THE SAME PLATFORM, COMBINE THEM - if ($settingsFile['notifications']['triggers']['cpuHigh']['platform'] == $settingsFile['notifications']['triggers']['memHigh']['platform']) { - $payload = ['event' => 'usage', 'cpu' => $notify['usage']['cpu'], 'cpuThreshold' => $settingsFile['global']['cpuThreshold'], 'mem' => $notify['usage']['mem'], 'memThreshold' => $settingsFile['global']['memThreshold']]; + if ($notify['usage']['cpu']) { + $payload = ['event' => 'usage', 'cpu' => $notify['usage']['cpu'], 'cpuThreshold' => $settingsTable['cpuThreshold']]; + $notifications->notify(0, 'cpuHigh', $payload); + logger(CRON_STATE_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['cpuHigh']['platform'], $payload); - } else { - if ($notify['usage']['cpu']) { - $payload = ['event' => 'usage', 'cpu' => $notify['usage']['cpu'], 'cpuThreshold' => $settingsFile['global']['cpuThreshold']]; - logger(CRON_STATE_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['cpuHigh']['platform'], $payload); - } + } - if ($notify['usage']['mem']) { - $payload = ['event' => 'usage', 'mem' => $notify['usage']['mem'], 'memThreshold' => $settingsFile['global']['memThreshold']]; - logger(CRON_STATE_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); - $notifications->notify($settingsFile['notifications']['triggers']['memHigh']['platform'], $payload); - } + if ($notify['usage']['mem']) { + $payload = ['event' => 'usage', 'mem' => $notify['usage']['mem'], 'memThreshold' => $settingsTable['memThreshold']]; + $notifications->notify(0, 'memHigh', $payload); + + logger(CRON_STATE_LOG, 'Notification payload: ' . json_encode($payload, JSON_UNESCAPED_SLASHES)); } } echo date('c') . ' Cron: state <-' . "\n"; -logger(CRON_STATE_LOG, 'run <-'); \ No newline at end of file +logger(CRON_STATE_LOG, 'run <-'); diff --git a/root/app/www/public/crons/stats.php b/root/app/www/public/crons/stats.php index 77e0efb..f48f248 100644 --- a/root/app/www/public/crons/stats.php +++ b/root/app/www/public/crons/stats.php @@ -14,7 +14,7 @@ logger(CRON_STATS_LOG, 'run ->'); echo date('c') . ' Cron: stats' . "\n"; -if ($settingsFile['tasks']['stats']['disabled']) { +if ($settingsTable['tasksStatsDisabled']) { logger(CRON_STATS_LOG, 'Cron cancelled: disabled in tasks menu'); logger(CRON_STATS_LOG, 'run <-'); echo date('c') . ' Cron: stats cancelled, disabled in tasks menu' . "\n"; @@ -26,4 +26,4 @@ setServerFile('stats', $dockerStats); echo date('c') . ' Cron: stats <-' . "\n"; -logger(CRON_STATS_LOG, 'run <-'); \ No newline at end of file +logger(CRON_STATS_LOG, 'run <-'); diff --git a/root/app/www/public/functions/api.php b/root/app/www/public/functions/api.php index 570fa7c..fb98a93 100644 --- a/root/app/www/public/functions/api.php +++ b/root/app/www/public/functions/api.php @@ -7,6 +7,29 @@ ---------------------------------- */ +function generateApikey($length = 32) +{ + return bin2hex(random_bytes($length)); +} + +function apiSetActiveServer($serverId, $serversTable) +{ + $_SESSION['activeServerId'] = $serversTable[$serverId]['id']; + $_SESSION['activeServerName'] = $serversTable[$serverId]['name']; + $_SESSION['activeServerUrl'] = rtrim($serversTable[$serverId]['url'], '/'); + $_SESSION['activeServerApikey'] = $serversTable[$serverId]['apikey']; +} + +function apiGetActiveServer() +{ + return [ + 'id' => $_SESSION['activeServerId'], + 'name' => $_SESSION['activeServerName'], + 'url' => $_SESSION['activeServerUrl'], + 'apikey' => $_SESSION['activeServerApikey'] + ]; +} + function apiResponse($code, $response) { session_unset(); @@ -57,4 +80,120 @@ function apiRequest($request, $params = [], $payload = []) } return $curl['response']; -} \ No newline at end of file +} + +function apiRequest2($endpoint, $parameters = [], $payload = []) +{ + global $database; + + $database = $database ?? new Database(); + $activeServer = apiGetActiveServer(); + + if ($activeServer['id'] == APP_SERVER_ID) { + return apiRequestLocal($endpoint, $parameters, $payload); + } + + return apiRequestRemote($endpoint, $parameters, $payload); +} + +function apiRequestRemote($endpoint, $parameters = [], $payload = []) +{ + $activeServer = apiGetActiveServer(); + logger(SYSTEM_LOG, 'apiRequestRemote() ' . $endpoint . ' for server ' . $activeServer['name']); + + if ($payload) { + $parameters = http_build_query($parameters); + + if (!$payload['request']) { + $payload['request'] = $endpoint; + } + + $curl = curl($activeServer['url'] . '/api/' . ($parameters ? '?' . $parameters : ''), ['x-api-key: ' . $activeServer['apikey']], 'POST', json_encode($payload)); + } else { + $parameters['request'] = $endpoint; + $builtParams = http_build_query($parameters); + + $curl = curl($activeServer['url'] . '/api/' . ($builtParams ? '?' . $builtParams : ''), ['x-api-key: ' . $activeServer['apikey']]); + } + + return $curl['response']; +} + +function apiRequestLocal($endpoint, $parameters = [], $payload = []) +{ + global $database, $docker; + + $database = $database ?? new Database(); + $docker = $docker ?? new Docker(); + + switch ($endpoint) { + case 'database-getContainer': + if (!$parameters['hash']) { + apiResponse(400, ['error' => 'Missing hash parameter']); + } + $containersTable = $database->getContainers(); + + return $database->getContainerFromHash($parameters['hash'], $containersTable); + case 'database-getContainers': + return $database->getContainers(); + case 'database-getNotificationLinks': + return $database->getNotificationLinks(); + case 'database-getNotificationPlatforms': + return $database->getNotificationPlatforms(); + case 'database-getNotificationTriggers': + return $database->getNotificationTriggers(); + case 'database-getServers': + return $database->getServers(); + case 'database-getSettings': + return $database->getSettings(); + case 'database-migrations': + return $database->migrations(); + case 'docker-imageSizes': + return $docker->getImageSizes(); + case 'docker-inspect': + if (!$parameters['name']) { + apiResponse(400, ['error' => 'Missing name parameter']); + } + + return $docker->inspect($parameters['name'], $parameters['useCache'], $parameters['format'], $parameters['params']); + case 'docker-processList': + return $docker->processList($parameters['useCache'], $parameters['format'], $parameters['params']); + case 'docker-stats': + return $docker->stats($parameters['useCache']); + case 'file-stats': + return getFile(STATS_FILE); + case 'file-pull': + return getFile(PULL_FILE); + case 'file-state': + return getFile(STATE_FILE); + case 'ping': + return 'pong from ' . ACTIVE_SERVER_NAME; + default: + apiResponse(405, ['error' => 'Invalid GET request']); + } +} + +function apiRequestServerPings() +{ + global $database; + + $database = $database ?? new Database(); + $serversTable = $database->getServers(); + + $servers = []; + foreach ($serversTable as $server) { + if ($server['id'] == APP_SERVER_ID) { + $servers[strtolower($server['name'])] = ['id' => $server['id'], 'name' => $server['name'], 'code' => 200]; + } else { + apiSetActiveServer($server['id'], $serversTable); + + $ping = apiRequest2('ping'); + $servers[strtolower($server['name'])] = ['id' => $server['id'], 'name' => $server['name'], 'code' => intval($ping['code'])]; + } + } + ksort($servers); + + apiSetActiveServer(ACTIVE_SERVER_ID, $serversTable); + + return $servers; +} diff --git a/root/app/www/public/functions/common.php b/root/app/www/public/functions/common.php index d897aca..40f042a 100644 --- a/root/app/www/public/functions/common.php +++ b/root/app/www/public/functions/common.php @@ -7,6 +7,29 @@ ---------------------------------- */ +function loadClassExtras($class) +{ + $extras = ['interfaces', 'traits']; + + foreach ($extras as $extraDir) { + if (file_exists(ABSOLUTE_PATH . 'classes/' . $extraDir . '/' . $class . '.php')) { + require ABSOLUTE_PATH . 'classes/' . $extraDir . '/' . $class . '.php'; + } else { + $extraFolder = ABSOLUTE_PATH . 'classes/' . $extraDir . '/' . $class . '/'; + + if (is_dir($extraFolder)) { + $openExtraDir = opendir($extraFolder); + while ($extraFile = readdir($openExtraDir)) { + if (str_contains($extraFile, '.php')) { + require $extraFolder . $extraFile; + } + } + closedir($openExtraDir); + } + } + } +} + function isDockwatchContainer($container) { $imageMatch = str_replace(':main', '', APP_IMAGE); @@ -19,11 +42,6 @@ function isDockwatchContainer($container) function automation() { - if (!file_exists(SERVERS_FILE)) { - $servers[] = ['name' => 'localhost', 'url' => 'http://localhost', 'apikey' => generateApikey()]; - setFile(SERVERS_FILE, $servers); - } - //-- CREATE DIRECTORIES createDirectoryTree(LOGS_PATH . 'crons'); createDirectoryTree(LOGS_PATH . 'notifications'); @@ -33,6 +51,8 @@ function automation() createDirectoryTree(BACKUP_PATH); createDirectoryTree(TMP_PATH); createDirectoryTree(COMPOSE_PATH); + createDirectoryTree(DATABASE_PATH); + createDirectoryTree(MIGRATIONS_PATH); } function createDirectoryTree($tree) @@ -82,11 +102,6 @@ function linkWebroot($location) } } -function generateApikey($length = 32) -{ - return bin2hex(random_bytes($length)); -} - function trackTime($label, $microtime = 0) { $backtrace = debug_backtrace(); diff --git a/root/app/www/public/functions/containers.php b/root/app/www/public/functions/containers.php index 4a8cd0c..ce9117a 100644 --- a/root/app/www/public/functions/containers.php +++ b/root/app/www/public/functions/containers.php @@ -9,7 +9,7 @@ function renderContainerRow($nameHash, $return) { - global $docker, $pullsFile, $settingsFile, $processList, $skipContainerActions, $groupHash; + global $docker, $pullsFile, $database, $settingsTable, $containersTable, $containerGroupsTable, $processList, $skipContainerActions, $groupHash; if (!$pullsFile) { $pullsFile = getServerFile('pull'); @@ -19,14 +19,6 @@ function renderContainerRow($nameHash, $return) $pullsFile = $pullsFile['file']; } - if (!$settingsFile) { - $settingsFile = getServerFile('settings'); - if ($settingsFile['code'] != 200) { - $apiError = $settingsFile['file']; - } - $settingsFile = $settingsFile['file']; - } - foreach ($processList as $process) { if (md5($process['Names']) == $nameHash) { break; @@ -41,7 +33,7 @@ function renderContainerRow($nameHash, $return) } $skipActions = skipContainerActions($process['inspect'][0]['Config']['Image'], $skipContainerActions); - $containerSettings = $settingsFile['containers'][$nameHash]; + $containerSettings = $database->getContainerFromHash($nameHash, $containersTable); $logo = getIcon($process['inspect']); $notificationIcon = ' '; @@ -53,8 +45,8 @@ function renderContainerRow($nameHash, $return) } $cpuUsage = floatval(str_replace('%', '', $process['stats']['CPUPerc'])); - if (intval($settingsFile['global']['cpuAmount']) > 0) { - $cpuUsage = number_format(($cpuUsage / intval($settingsFile['global']['cpuAmount'])), 2) . '%'; + if (intval($settingsTable['cpuAmount']) > 0) { + $cpuUsage = number_format(($cpuUsage / intval($settingsTable['cpuAmount'])), 2) . '%'; } $pullData = $pullsFile[$nameHash]; @@ -63,7 +55,7 @@ function renderContainerRow($nameHash, $return) $updateStatus = ($pullData['regctlDigest'] == $pullData['imageDigest']) ? 'Up to date' : 'Outdated'; } - $restartUnhealthy = $settingsFile['containers'][$nameHash]['restartUnhealthy']; + $restartUnhealthy = $containerSettings['restartUnhealthy']; $healthyRestartClass = 'text-success'; $healthyRestartText = 'Auto restart when unhealthy'; @@ -280,12 +272,12 @@ function renderContainerRow($nameHash, $return) @@ -333,13 +325,15 @@ function renderContainerRow($nameHash, $return) function skipContainerActions($container, $containers) { - global $docker, $settingsFile, $stateFile; + global $docker, $database, $settingsTable, $stateFile; - $settingsFileData = $settingsFile ? $settingsFile : getServerFile('settings'); - $stateFileData = $stateFile ? $stateFile : getServerFile('state'); + $stateFileData = $stateFile ?: getServerFile('state'); + $containersTable = $database->getContainers(); + + if ($containersTable) { + foreach ($containersTable as $containerSettings) { + $containerHash = $containerSettings['hash']; - if ($settingsFileData['containers']) { - foreach ($settingsFileData['containers'] as $containerHash => $containerSettings) { if ($containerSettings['blacklist']) { $containerState = $docker->findContainer(['hash' => $containerHash, 'data' => $stateFileData]); @@ -350,7 +344,7 @@ function skipContainerActions($container, $containers) } } - if ($settingsFileData['global']['overrideBlacklist'] == 0) { + if ($settingsTable['overrideBlacklist'] == 0) { foreach ($containers as $skip) { if (str_contains($container, $skip)) { return SKIP_FORCE; @@ -359,4 +353,4 @@ function skipContainerActions($container, $containers) } return SKIP_OFF; -} \ No newline at end of file +} diff --git a/root/app/www/public/functions/curl.php b/root/app/www/public/functions/curl.php index fbc325f..ec471de 100644 --- a/root/app/www/public/functions/curl.php +++ b/root/app/www/public/functions/curl.php @@ -9,9 +9,7 @@ function curl($url, $headers = [], $method = 'GET', $payload = '', $userPass = [], $timeout = 60) { - if (!is_string($payload)) { - $payload = ''; - } + $payload = !is_string($payload) ? '' : $payload; $curlHeaders = [ 'user-agent:' . APP_NAME, diff --git a/root/app/www/public/functions/docker.php b/root/app/www/public/functions/docker.php index 0572d11..9920a2b 100644 --- a/root/app/www/public/functions/docker.php +++ b/root/app/www/public/functions/docker.php @@ -9,7 +9,7 @@ function getExpandedProcessList($fetchProc, $fetchStats, $fetchInspect, $maintenance = false) { - global $docker; + global $docker, $api; if ($maintenance) { logger(MAINTENANCE_LOG, '$fetchProc=' . $fetchProc); @@ -28,8 +28,8 @@ function getExpandedProcessList($fetchProc, $fetchStats, $fetchInspect, $mainten $processList = json_decode($processList, true); logger(MAINTENANCE_LOG, 'dockerProcessList <-'); } else { - $processList = apiRequest('dockerProcessList', ['format' => true]); - $processList = json_decode($processList['response']['docker'], true); + $processList = apiRequest2('docker-processList', ['format' => true]); + $processList = json_decode($processList, true); } $loadTimes[] = trackTime('dockerProcessList <-'); @@ -41,8 +41,8 @@ function getExpandedProcessList($fetchProc, $fetchStats, $fetchInspect, $mainten $imageSizes = json_decode($imageSizes, true); logger(MAINTENANCE_LOG, 'dockerImageSizes <-'); } else { - $imageSizes = apiRequest('dockerImageSizes'); - $imageSizes = json_decode($imageSizes['response']['docker'], true); + $imageSizes = apiRequest2('docker-imageSizes'); + $imageSizes = json_decode($imageSizes, true); } $loadTimes[] = trackTime('dockerImageSizes <-'); } @@ -56,8 +56,7 @@ function getExpandedProcessList($fetchProc, $fetchStats, $fetchInspect, $mainten logger(MAINTENANCE_LOG, json_encode($dockerStats, JSON_UNESCAPED_SLASHES)); logger(MAINTENANCE_LOG, 'dockerStats <-'); } else { - $dockerStats = apiRequest('stats'); - $dockerStats = $dockerStats['response']['state']; + $dockerStats = apiRequest2('file-stats'); } if (!$dockerStats) { //-- NOT WRITTEN YET @@ -66,8 +65,8 @@ function getExpandedProcessList($fetchProc, $fetchStats, $fetchInspect, $mainten logger(MAINTENANCE_LOG, $dockerStats); $dockerStats = json_decode($dockerStats, true); } else { - $dockerStats = apiRequest('dockerStats'); - $dockerStats = json_decode($dockerStats['response']['docker'], true); + $dockerStats = apiRequest2('docker-stats', $_GET); + $dockerStats = json_decode($dockerStats, true); } } @@ -91,8 +90,8 @@ function getExpandedProcessList($fetchProc, $fetchStats, $fetchInspect, $mainten logger(MAINTENANCE_LOG, '$docker->inspect ' . json_encode(json_decode($inspect))); logger(MAINTENANCE_LOG, 'dockerInspect <-'); } else { - $inspect = apiRequest('dockerInspect', ['name' => implode(' ', $inspectContainers), 'format' => true]); - $inspectResults = json_decode($inspect['response']['docker'], true); + $inspect = apiRequest2('docker-inspect', ['name' => implode(' ', $inspectContainers), 'format' => true]); + $inspectResults = json_decode($inspect, true); } } diff --git a/root/app/www/public/functions/files.php b/root/app/www/public/functions/files.php index 2ffc9f4..3d21022 100644 --- a/root/app/www/public/functions/files.php +++ b/root/app/www/public/functions/files.php @@ -22,7 +22,7 @@ function loadJS() function getServerFile($file) { //-- NO NEED FOR AN API REQUEST LOCALLY - if (!$_SESSION['serverIndex']) { + if ($_SESSION['activeServerId'] == APP_SERVER_ID) { $localFile = constant(strtoupper($file) . '_FILE'); return ['code' => 200, 'file' => getFile($localFile)]; } @@ -37,7 +37,7 @@ function getServerFile($file) function setServerFile($file, $contents) { //-- NO NEED FOR AN API REQUEST LOCALLY - if (!$_SESSION['serverIndex']) { + if ($_SESSION['activeServerId'] == APP_SERVER_ID) { $localFile = constant(strtoupper($file) . '_FILE'); setFile($localFile, $contents); return ['code' => 200]; diff --git a/root/app/www/public/functions/helpers/array.php b/root/app/www/public/functions/helpers/array.php index 1d3f26a..4d084bb 100644 --- a/root/app/www/public/functions/helpers/array.php +++ b/root/app/www/public/functions/helpers/array.php @@ -7,6 +7,11 @@ ---------------------------------- */ +function makeArray($array) +{ + return is_array($array) ? $array : []; +} + function array_equals_any($haystack, $needles) { if (!is_array($haystack) || !is_array($needles)) { diff --git a/root/app/www/public/functions/helpers/strings.php b/root/app/www/public/functions/helpers/strings.php index 6ca31b8..2a187b8 100644 --- a/root/app/www/public/functions/helpers/strings.php +++ b/root/app/www/public/functions/helpers/strings.php @@ -7,7 +7,7 @@ ---------------------------------- */ -function str_equals_any($haystack, array $needles): bool +function str_equals_any(string|null $haystack, array $needles): bool { if (!$haystack) { return false; @@ -16,13 +16,21 @@ function str_equals_any($haystack, array $needles): bool return in_array($haystack, $needles); } -function str_contains_any(string $haystack, array $needles): bool +function str_contains_any(string|null $haystack, array $needles): bool { + if (!$haystack) { + return false; + } + return array_reduce($needles, fn($a, $n) => $a || str_contains($haystack, $n), false); } -function str_contains_all(string $haystack, array $needles): bool +function str_contains_all(string|null $haystack, array $needles): bool { + if (!$haystack) { + return false; + } + return array_reduce($needles, fn($a, $n) => $a && str_contains($haystack, $n), true); } diff --git a/root/app/www/public/functions/notifications.php b/root/app/www/public/functions/notifications.php deleted file mode 100644 index d2dccbe..0000000 --- a/root/app/www/public/functions/notifications.php +++ /dev/null @@ -1,47 +0,0 @@ -'; - case 'text': - return ''; - } -} - -function sendTestNotification($platform) -{ - global $notifications, $settingsFile; - - //-- INITIALIZE THE NOTIFY CLASS - if (!$notifications) { - $notifications = new Notifications(); - logger(SYSTEM_LOG, 'Init class: Notifications()'); - } - - $return = ''; - $payload = [ - 'event' => 'test', - 'title' => 'DockWatch Test', - 'message' => 'This is a test message sent from DockWatch' - ]; - - $result = $notifications->notify($platform, $payload); - - if ($result['code'] != 200) { - $return = 'Code ' . $result['code'] . ', ' . $result['error']; - } - - return $return; -} \ No newline at end of file diff --git a/root/app/www/public/includes/constants.php b/root/app/www/public/includes/constants.php index 1c692e4..ca0b699 100644 --- a/root/app/www/public/includes/constants.php +++ b/root/app/www/public/includes/constants.php @@ -7,13 +7,28 @@ ---------------------------------- */ -define('APP_NAME', 'DockWatch'); +define('APP_NAME', 'Dockwatch'); define('APP_IMAGE', 'ghcr.io/notifiarr/dockwatch:main'); +define('APP_PORT', 9999); +define('APP_SERVER_ID', 1); +define('APP_SERVER_URL', 'http://localhost'); define('APP_MAINTENANCE_IMAGE', 'ghcr.io/notifiarr/dockwatch:develop'); +define('APP_MAINTENANCE_PORT', 9998); +define('APP_BACKUPS', 7); //-- DAYS define('ICON_REPO', 'Notifiarr/images'); define('ICON_URL', 'https://gh.notifiarr.com/images/icons/'); +//-- DATABASE +define('SETTINGS_TABLE', 'settings'); +define('SERVERS_TABLE', 'servers'); +define('CONTAINER_SETTINGS_TABLE', 'container_settings'); +define('CONTAINER_GROUPS_TABLE', 'container_groups'); +define('CONTAINER_GROUPS_LINK_TABLE', 'container_group_link'); +define('NOTIFICATION_PLATFORM_TABLE', 'notification_platform'); +define('NOTIFICATION_TRIGGER_TABLE', 'notification_trigger'); +define('NOTIFICATION_LINK_TABLE', 'notification_link'); + //-- CRON FREQUENCY define('DEFAULT_CRON', '0 0 * * *'); @@ -23,6 +38,8 @@ define('LOGS_PATH', APP_DATA_PATH . 'logs/'); define('TMP_PATH', APP_DATA_PATH . 'tmp/'); define('COMPOSE_PATH', APP_DATA_PATH . 'compose/'); +define('DATABASE_PATH', APP_DATA_PATH . 'database/'); +define('MIGRATIONS_PATH', ABSOLUTE_PATH . 'migrations/'); //-- DATA FILES define('SERVERS_FILE', APP_DATA_PATH . 'servers.json'); @@ -81,4 +98,4 @@ 'dockwatch', //-- IF THIS GOES DOWN, IT WILL STOP THE CONTAINER WHICH MEANS IT CAN NEVER FINISH 'cloudflared', //-- IF THIS GOES DOWN, IT WILL KILL THE NETWORK TRAFFIC TO DOCKWATCH 'swag' //-- IS THIS GOES DOWN, IT WILL KILL THE WEB SERVICE TO DOCKWATCH - ]; \ No newline at end of file + ]; diff --git a/root/app/www/public/includes/footer.php b/root/app/www/public/includes/footer.php index e1c7865..ae762bb 100644 --- a/root/app/www/public/includes/footer.php +++ b/root/app/www/public/includes/footer.php @@ -179,7 +179,7 @@
Consider not running the dockwatch update time at the same time as other containers. When dockwatch starts its update process, everything after it will be ignored since it is stopping its self!
- + Remote control of self restarts and updates is not supported. Wait 30-45 seconds after using these buttons and refresh the page so the process outlined above can complete. If you have notifications enabled you will see the maintenance container start and shortly after the dockwatch container start.

diff --git a/root/app/www/public/includes/header.php b/root/app/www/public/includes/header.php index ac51e46..8914229 100644 --- a/root/app/www/public/includes/header.php +++ b/root/app/www/public/includes/header.php @@ -8,25 +8,17 @@ */ $_SESSION['IN_DOCKWATCH'] = true; - -$fetchServers = false; if (!$_SESSION['serverList'] || ($_SESSION['serverListUpdated'] + 300) < time()) { - $fetchServers = true; -} + $serverPings = apiRequestServerPings(); + $serverList = ''; - foreach ($serversFile as $serverIndex => $serverDetails) { - $ping = curl($serverDetails['url'] . '/api/?request=ping', ['x-api-key: ' . $serverDetails['apikey']], 'GET', '', [], 5); - $disabled = ''; - if ($ping['code'] != 200) { - $disabled = ' [HTTP: ' . $ping['code'] . ']'; - } - $serverList .= ''; - $link = $_SESSION['serverIndex'] == $serverIndex ? $serverDetails['url'] : $link; + $serverList .= ''; + $link = $_SESSION['activeServerId'] == $serverPing['id'] ? $serverPing['url'] : $link; } $serverList .= ''; - $serverList .= ' '; + $serverList .= ' '; $_SESSION['serverList'] = $serverList; $_SESSION['serverListUpdated'] = time(); @@ -40,7 +32,7 @@ - Dockwatch<?= ($settingsFile['global']['serverName'] ? ' - ' . $settingsFile['global']['serverName'] : '') ?> + <?= APP_NAME ?><?= $settingsTable['serverName'] ? ' - ' . $settingsTable['serverName'] : '' ?> @@ -67,8 +59,8 @@ @@ -86,20 +78,20 @@ diff --git a/root/app/www/public/index.php b/root/app/www/public/index.php index e4f8cd8..7ae906e 100644 --- a/root/app/www/public/index.php +++ b/root/app/www/public/index.php @@ -19,15 +19,15 @@ } $loadError = ''; -if (!$serversFile) { - $loadError = 'Servers file missing or corrupt'; +if (!$serversTable) { + $loadError = 'Instances table is empty'; } if (!file_exists(REGCTL_PATH . REGCTL_BINARY)) { $loadError = 'The required regctl binary is missing from \'' . REGCTL_PATH . REGCTL_BINARY . '\''; } -if (!$dockerCommunicateAPI) { +if (!$isDockerApiAvailable) { $loadError = 'There is a problem talking to the docker API. You either did not mount /var/run/docker.sock or you are passing in a DOCKER_HOST that is not valid. Try using the IP instead of container name for the docker host variable.'; if ($_SERVER['DOCKER_HOST']) { @@ -52,7 +52,7 @@ -
+
If you are seeing this, it means the user:group running this container does not have permission to run docker commands. Please fix that, restart the container and try again.

An example for Ubuntu: @@ -77,4 +77,4 @@
apiIsAvailable(); + $isDockerApiAvailable = $docker->apiIsAvailable(); - //-- INITIALIZE THE SHELL CLASS - $shell = new Shell(); -} - -//-- GET THE SERVERS LIST -$serversFile = getFile(SERVERS_FILE); -$_SESSION['serverIndex'] = is_numeric($_SESSION['serverIndex']) ? $_SESSION['serverIndex'] : 0; - -define('ACTIVE_SERVER_NAME', $serversFile[$_SESSION['serverIndex']]['name']); -define('ACTIVE_SERVER_URL', rtrim($serversFile[$_SESSION['serverIndex']]['url'], '/')); -define('ACTIVE_SERVER_APIKEY', $serversFile[$_SESSION['serverIndex']]['apikey']); + //-- INITIALIZE THE NOTIFY CLASS + $notifications = new Notifications(); + logger(SYSTEM_LOG, 'Init class: Notifications()'); -if (!str_contains_any($_SERVER['PHP_SELF'], ['/api/']) && !str_contains($_SERVER['PWD'], 'oneshot')) { - if (!IS_SSE) { - //-- CHECK IF SELECTED SERVER CAN BE TALKED TO - $ping = apiRequest('ping'); - if (!is_array($ping) || $ping['code'] != 200) { - if ($_SESSION['serverIndex'] == 0) { - exit('The connection to this container in the servers file is broken'); - } else { - $_SESSION['serverIndex'] = 0; - header('Location: /'); - exit(); - } - } + if (!str_contains_any($_SERVER['PHP_SELF'], ['/api/']) && !str_contains($_SERVER['PWD'], 'oneshot')) { + $stateFile = apiRequest2('file-state'); + $pullsFile = apiRequest2('file-pull'); } +} - //-- SETTINGS - $settingsFile = getServerFile('settings'); - $apiError = $settingsFile['code'] != 200 ? $settingsFile['file'] : $apiError; - $settingsFile = $settingsFile['file']; - +if (!str_contains_any($_SERVER['PHP_SELF'], ['/api/']) && !str_contains($_SERVER['PWD'], 'oneshot')) { //-- LOGIN, DEFINE AFTER LOADING SETTINGS - define('LOGIN_FAILURE_LIMIT', ($settingsFile['global']['loginFailures'] ? $settingsFile['global']['loginFailures']: 10)); - define('LOGIN_FAILURE_TIMEOUT', ($settingsFile['global']['loginFailures'] ? $settingsFile['global']['loginTimeout']: 10)); //-- MINUTES TO DISABLE LOGINS - - if (!IS_SSE) { - //-- STATE - $stateFile = getServerFile('state'); - $apiError = $stateFile['code'] != 200 ? $stateFile['file'] : $apiError; - $stateFile = $stateFile['file']; - - //-- PULLS - $pullsFile = getServerFile('pull'); - $apiError = $pullsFile['code'] != 200 ? $pullsFile['file'] : $apiError; - $pullsFile = $pullsFile['file']; - } + define('LOGIN_FAILURE_LIMIT', ($settingsTable['loginFailures'] ? $settingsTable['loginFailures']: 10)); + define('LOGIN_FAILURE_TIMEOUT', ($settingsTable['loginFailures'] ? $settingsTable['loginTimeout']: 10)); //-- MINUTES TO DISABLE LOGINS if (file_exists(LOGIN_FILE) && !str_contains($_SERVER['PHP_SELF'], '/crons/')) { define('USE_AUTH', true); @@ -137,10 +133,6 @@ } } else { logger(SYSTEM_LOG, 'Starting'); - - //-- INITIALIZE THE NOTIFY CLASS - $notifications = new Notifications(); - logger(SYSTEM_LOG, 'Init class: Notifications()'); } } diff --git a/root/app/www/public/migrations/001_initial_setup.php b/root/app/www/public/migrations/001_initial_setup.php new file mode 100644 index 0000000..a11f762 --- /dev/null +++ b/root/app/www/public/migrations/001_initial_setup.php @@ -0,0 +1,274 @@ + down)', 'state'), + ('added', 'Added', 'Send a notification when a container is added', 'state'), + ('removed', 'Removed', 'Send a notification when a container is removed', 'state'), + ('prune', 'Prune', 'Send a notification when an image or volume is pruned', 'prune'), + ('cpuHigh', 'CPU usage', 'Send a notification when container CPU usage exceeds threshold (set in Settings)', 'usage'), + ('memHigh', 'Memory usage', 'Send a notification when container memory usage exceeds threshold (set in Settings)', 'usage'), + ('health', 'Health change', 'Send a notification when container becomes unhealthy', 'health')"; + +$q[] = "CREATE TABLE " . NOTIFICATION_LINK_TABLE . " ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + platform_id INTEGER NOT NULL, + platform_parameters TEXT NOT NULL, + trigger_ids TEXT NOT NULL + )"; + +$q[] = "CREATE TABLE " . SERVERS_TABLE . " ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + url TEXT NOT NULL, + apikey TEXT NOT NULL + )"; + +$globalSettings = [ + 'serverName' => '', + 'maintenanceIP' => '', + 'maintenancePort' => 9998, + 'loginFailures' => 6, + 'loginTimeout' => 60, + 'updates' => 2, + 'updatesFrequency' => '0 2 * * *', + 'autoPruneImages' => false, + 'autoPruneVolumes' => false, + 'autoPruneNetworks' => false, + 'autoPruneHour' => 12, + 'cpuThreshold' => '', + 'cpuAmount' => '', + 'memThreshold' => '', + 'sseEnabled' => false, + 'cronLogLength' => 1, + 'notificationLogLength' => 1, + 'uiLogLength' => 1, + 'apiLogLength' => 1, + 'environment' => 0, + 'overrideBlacklist' => false, + 'externalLoading' => 0, + 'taskStatsDisabled' => 0, + 'taskStateDisabled' => 0, + 'taskPullsDisabled' => 0, + 'taskHousekeepingDisabled' => 0, + 'taskHealthDisabled' => 0, + 'taskPruneDisabled' => 0 + ]; + +foreach ($globalSettings as $key => $val) { + $settingRows[] = "('" . $key . "', '" . $val . "')"; +} + +$q[] = "INSERT INTO " . SETTINGS_TABLE . " + (`name`, `value`) + VALUES " . implode(', ', $settingRows); + +//-- PRE-DB SUPPORT, POPULATE THE NEW TABLES WITH EXISTING DATA +if (file_exists(APP_DATA_PATH . 'servers.json')) { + $serversFile = getFile(SERVERS_FILE); + $serverRows = []; + + foreach ($serversFile as $server) { + $serverRows[] = "('" . $server['name'] . "', '" . $server['url'] . "', '" . $server['apikey'] . "')"; + } + + if ($serverRows) { + $q[] = "INSERT INTO " . SERVERS_TABLE . " + (`name`, `url`, `apikey`) + VALUES " . implode(', ', $serverRows); + } +} else { + $q[] = "INSERT INTO " . SERVERS_TABLE . " + (`name`, `url`, `apikey`) + VALUES + ('" . APP_NAME . "', '" . APP_SERVER_URL . "', '" . generateApikey() . "')"; +} + +if (file_exists(APP_DATA_PATH . 'settings.json')) { + $settingsFile = getFile(SETTINGS_FILE); + + if ($settingsFile['tasks']) { + foreach ($settingsFile['tasks'] as $task => $taskSettings) { + $q[] = "UPDATE " . SETTINGS_TABLE . " + SET value = '" . $taskSettings['disabled'] . "' + WHERE name = 'task" . ucfirst($task) . "Disabled'"; + } + } + + if ($settingsFile['global']) { + foreach ($settingsFile['global'] as $key => $val) { + $q[] = "UPDATE " . SETTINGS_TABLE . " + SET value = '" . $val . "' + WHERE name = '" . $key . "'"; + } + } + + if ($settingsFile['containers']) { + $containerSettingsRows = []; + + foreach ($settingsFile['containers'] as $hash => $settings) { + $containerSettingsRows[] = "('" . $hash . "', '" . intval($settings['updates']) . "', '" . $settings['frequency'] . "', '" . intval($settings['restartUnhealthy']) . "', '" . intval($settings['disableNotifications']) . "', '" . intval($settings['shutdownDelay']) . "', '" . intval($settings['shutdownDelaySeconds']) . "')"; + } + + if ($containerSettingsRows) { + $q[] = "INSERT INTO " . CONTAINER_SETTINGS_TABLE . " + (`hash`, `updates`, `frequency`, `restartUnhealthy`, `disableNotifications`, `shutdownDelay`, `shutdownDelaySeconds`) + VALUES " . implode(', ', $containerSettingsRows); + } + } + + if ($settingsFile['containerGroups']) { + $containerGroupRows = []; + + foreach ($settingsFile['containerGroups'] as $groupHash => $groupData) { + $containerGroupRows[] = "('" . $groupHash . "', '" . $groupData['name'] . "')"; + } + + if ($containerGroupRows) { + $q[] = "INSERT INTO " . CONTAINER_GROUPS_TABLE . " + (`hash`, `name`) + VALUES " . implode(', ', $containerGroupRows); + } + } + + if ($settingsFile['notifications']) { + if ($settingsFile['notifications']['platforms'] && $settingsFile['notifications']['triggers']) { + $triggerIds = []; + if ($settingsFile['notifications']['triggers']['updated']['active']) { + $triggerIds[] = 1; + } + if ($settingsFile['notifications']['triggers']['updates']['active']) { + $triggerIds[] = 2; + } + if ($settingsFile['notifications']['triggers']['stateChange']['active']) { + $triggerIds[] = 3; + } + if ($settingsFile['notifications']['triggers']['added']['active']) { + $triggerIds[] = 4; + } + if ($settingsFile['notifications']['triggers']['removed']['active']) { + $triggerIds[] = 5; + } + if ($settingsFile['notifications']['triggers']['prune']['active']) { + $triggerIds[] = 6; + } + if ($settingsFile['notifications']['triggers']['cpuHigh']['active']) { + $triggerIds[] = 7; + } + if ($settingsFile['notifications']['triggers']['memHigh']['active']) { + $triggerIds[] = 8; + } + if ($settingsFile['notifications']['triggers']['health']['active']) { + $triggerIds[] = 9; + } + + $q[] = "INSERT INTO " . NOTIFICATION_LINK_TABLE . " + (`name`, `platform_id`, `platform_parameters`, `trigger_ids`) + VALUES + ('Notifiarr', '" . NotificationPlatforms::NOTIFIARR . "', '{\"apikey\":\"" . $settingsFile['notifications']['platforms'][NotificationPlatforms::NOTIFIARR]['apikey'] . "\"}', '[" . implode(',', $triggerIds) . "]')"; + } + } +} + +//-- ALWAYS NEED TO BUMP THE MIGRATION ID +$q[] = "INSERT INTO " . SETTINGS_TABLE . " + (`name`, `value`) + VALUES + ('migration', '001')"; + +foreach ($q as $query) { + $db->query($query); +} + +//-- PRE-DB SUPPORT, POPULATE THE NEW TABLES WITH EXISTING DATA +if ($settingsFile) { + $q = $containerLinkRows = []; + $containers = $database->getContainers(); + $containerGroups = $database->getContainerGroups(); + + if ($settingsFile['containerGroups']) { + foreach ($settingsFile['containerGroups'] as $groupHash => $groupData) { + if ($groupData['containers']) { + foreach ($groupData['containers'] as $groupContainerHash) { + $container = $database->getContainerFromHash($groupContainerHash, $containers); + $group = $database->getContainerGroupFromHash($groupHash, $containerGroups); + + if ($group['id'] && $container['id']) { + $containerLinkRows[] = "('" . $group['id'] . "', '" . $container['id'] . "')"; + } + } + } + } + + if ($containerLinkRows) { + $q[] = "INSERT INTO " . CONTAINER_GROUPS_LINK_TABLE . " + (`group_id`, `container_id`) + VALUES " . implode(', ', $containerLinkRows); + } + } + + foreach ($q as $query) { + $db->query($query); + } +} diff --git a/root/app/www/public/sse.php b/root/app/www/public/sse.php index 4b414c6..4a81569 100644 --- a/root/app/www/public/sse.php +++ b/root/app/www/public/sse.php @@ -15,7 +15,7 @@ $sseFile = getServerFile('sse'); $sseFile = $sseFile['code'] != 200 ? [] : $sseFile['file']; -if ($settingsFile['global']['sseEnabled']) { +if ($settingsTable['sseEnabled']) { //-- DONT SEND ALL THE DATA EVERY TIME, WASTE if (!$sseFile['pushed']) { $sseFile['pushed'] = time(); diff --git a/root/app/www/public/startup.php b/root/app/www/public/startup.php index 4effedf..50a3837 100644 --- a/root/app/www/public/startup.php +++ b/root/app/www/public/startup.php @@ -14,11 +14,15 @@ echo 'require_once ' . ABSOLUTE_PATH . 'loader.php' . "\n"; require_once ABSOLUTE_PATH . 'loader.php'; -//-- SETTINGS -$settingsFile = getFile(SETTINGS_FILE); +//-- INITIALIZE THE NOTIFY CLASS +if (!$database) { + $database = new Database(); +} //-- INITIALIZE THE NOTIFY CLASS -$notifications = new Notifications(); +if (!$notifications) { + $notifications = new Notifications(); +} //-- INITIALIZE THE MAINTENANCE CLASS $maintenance = new Maintenance(); @@ -29,10 +33,14 @@ //-- STARTUP NOTIFICATION $notify['state']['changed'][] = ['container' => $name, 'previous' => '.....', 'current' => 'Started/Restarted']; -if ($settingsFile['notifications']['triggers']['stateChange']['platform']) { + +if ($database->isNotificationTriggerEnabled('stateChange')) { $payload = ['event' => 'state', 'changes' => $notify['state']['changed']]; - $notifications->notify($settingsFile['notifications']['triggers']['stateChange']['platform'], $payload); + $notifications->notify(0, 'stateChange', $payload); + logger(STARTUP_LOG, 'Sending ' . $name . ' started notification'); +} else { + logger(STARTUP_LOG, 'Skipping ' . $name . ' started notification, no senders found with stateChange enabled'); } //-- MAINTENANCE CHECK diff --git a/root/etc/php82/conf.d/dockwatch.ini b/root/etc/php82/conf.d/dockwatch.ini index 0299379..0024d7f 100644 --- a/root/etc/php82/conf.d/dockwatch.ini +++ b/root/etc/php82/conf.d/dockwatch.ini @@ -8,4 +8,4 @@ max_input_vars = 10000 error_log = '/config/log/php/errors.log' error_reporting = -1 log_errors = 1 -session.gc_maxlifetime = 86400 +session.gc_maxlifetime = 86400 \ No newline at end of file
Orphan images - > + > Automatically try to prune all orphan images daily
Orphan volumes - > + > Automatically try to prune all orphan volumes daily
Orphan networks - > + > Automatically try to prune all orphan networks daily
CPU1 - + If a container usage is above this number, send a notification (if notification is enabled)
CPUs - + Detected count:
Memory1 - + If a container usage is above this number, send a notification (if notification is enabled)
Enabled2,3 - > + > SSE will update the container list UI every minute with current status of Updates, State, Health, CPU and Memory
Crons - + How long to store cron run log files (min 1 day)
Notifications - + How long to store logs generated when notifications are sent (min 1 day)
UI - + How long to store logs generated from using the UI (min 1 day)
API - + How long to store logs generated when api requests are made (min 1 day)
Environment Location of webroot, requires a container restart after changing. Do not change this without working files externally!
Override BlacklistOverride blacklist - > + > Generally not recommended, it's at your own risk.
Page Loading3Page loading3 Internal: On a full page refresh you will go back to the overview. (state lost)
External: On a full page refresh you will stay on this current page. (state saved in URL)
>> Stats changes 1m
>> SSE 1m
>> State changes 5m
>> Pulls 5m
>> Housekeeping 10m
>> Health 15m
>> Prune 24h