mirror of
https://github.com/TrezOne/docker-mods-uptime-kuma-timeout-fix.git
synced 2026-06-27 18:53:31 -04:00
swag:auto-uptime-kuma Initial release of the mod
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
from swagDocker import SwagDocker
|
||||
from swagUptimeKuma import SwagUptimeKuma
|
||||
import sys
|
||||
import argparse
|
||||
import os
|
||||
|
||||
|
||||
def parseCommandLine():
|
||||
"""
|
||||
Different application behavior if executed from CLI
|
||||
"""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-purge', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
if (args.purge == True):
|
||||
swagUptimeKuma.purgeData()
|
||||
swagUptimeKuma.disconnect()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def addOrUpdateMonitors(domainName, swagContainers):
|
||||
for swagContainer in swagContainers:
|
||||
containerConfig = swagDocker.parseContainerLabels(
|
||||
swagContainer.labels, ".monitor.")
|
||||
containerName = swagContainer.name
|
||||
monitorData = swagUptimeKuma.parseMonitorData(
|
||||
containerName, domainName, containerConfig)
|
||||
|
||||
if (not swagUptimeKuma.monitorExists(containerName)):
|
||||
swagUptimeKuma.addMonitor(containerName, domainName, monitorData)
|
||||
else:
|
||||
swagUptimeKuma.updateMonitor(
|
||||
containerName, domainName, monitorData)
|
||||
|
||||
|
||||
def getMonitorsToBeRemoved(swagContainers, apiMonitors):
|
||||
# Monitors to be removed are those that no longer have an existing container
|
||||
# Monitor <-> Container link is done by comparing the container name with the monitor swag tag value
|
||||
existingMonitorNames = [swagUptimeKuma.getMonitorSwagTagValue(
|
||||
monitor) for monitor in apiMonitors]
|
||||
existingContainerNames = [container.name for container in swagContainers]
|
||||
|
||||
monitorsToBeRemoved = [
|
||||
containerName for containerName in existingMonitorNames if containerName not in existingContainerNames]
|
||||
return monitorsToBeRemoved
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
url = os.environ['UPTIME_KUMA_URL']
|
||||
username = os.environ['UPTIME_KUMA_USERNAME']
|
||||
password = os.environ['UPTIME_KUMA_PASSWORD']
|
||||
domainName = os.environ['URL']
|
||||
|
||||
swagDocker = SwagDocker("swag.uptime-kuma")
|
||||
swagUptimeKuma = SwagUptimeKuma(url, username, password)
|
||||
|
||||
parseCommandLine()
|
||||
|
||||
swagContainers = swagDocker.getSwagContainers()
|
||||
|
||||
addOrUpdateMonitors(domainName, swagContainers)
|
||||
|
||||
monitorsToBeRemoved = getMonitorsToBeRemoved(
|
||||
swagContainers, swagUptimeKuma.apiMonitors)
|
||||
swagUptimeKuma.deleteMonitors(monitorsToBeRemoved)
|
||||
|
||||
swagUptimeKuma.disconnect()
|
||||
@@ -0,0 +1,20 @@
|
||||
def has_key_with_value(dictionary, key, value):
|
||||
return key in dictionary and dictionary[key] == value
|
||||
|
||||
|
||||
def merge_dicts(*dict_args):
|
||||
result = {}
|
||||
for dictionary in dict_args:
|
||||
result.update(dictionary)
|
||||
return result
|
||||
|
||||
|
||||
def write_file(filename, content):
|
||||
with open(filename, 'w+') as file:
|
||||
file.write(content)
|
||||
|
||||
|
||||
def read_file(filename):
|
||||
with open(filename, 'r') as file:
|
||||
content = file.read()
|
||||
return content
|
||||
@@ -0,0 +1,50 @@
|
||||
import docker
|
||||
|
||||
|
||||
class SwagDocker:
|
||||
"""
|
||||
A service class for interacting with Docker containers that are used by SWAG mods.
|
||||
"""
|
||||
|
||||
client = None
|
||||
_containers = None
|
||||
_labelPrefix = None
|
||||
|
||||
def __init__(self, labelPrefix: str):
|
||||
self._labelPrefix = labelPrefix
|
||||
self.client = docker.from_env()
|
||||
|
||||
def getSwagContainers(self):
|
||||
"""
|
||||
Retrieve Docker containers filtered by "swag.my_mod.enabled=true":
|
||||
>>> swag = SwagDocker("swag.my_mod")
|
||||
>>> containers = swag.getSwagContainers()
|
||||
"""
|
||||
if self._containers is None:
|
||||
self._containers = self.client.containers.list(
|
||||
filters={"label": [f"{self._labelPrefix}.enabled=true"]})
|
||||
return self._containers
|
||||
|
||||
def parseContainerLabels(self, containerLabels, extraPrefix=""):
|
||||
"""
|
||||
Having following example container labels:
|
||||
swag.my_mod.enabled: true
|
||||
swag.my_mod.config.apple: "123"
|
||||
swag.my_mod.config.orange: "456"
|
||||
|
||||
>>> for container in containers:
|
||||
>>> containerConfigA = swagDocker.parseContainerLabels(container.labels)
|
||||
# Above will return {"enabled": true, "config.apple": "123", "config.orange": "456"}
|
||||
>>> containerConfigB = swagDocker.parseContainerLabels(container.labels, ".config.")
|
||||
# Above will return {"apple": "123", "orange": "456"}
|
||||
"""
|
||||
filteredContainerLabels = {}
|
||||
fullPrefix = f"{self._labelPrefix}{extraPrefix}"
|
||||
prefix_length = len(fullPrefix)
|
||||
|
||||
for label, value in containerLabels.items():
|
||||
if label.startswith(fullPrefix):
|
||||
parsedLabel = label[prefix_length:]
|
||||
filteredContainerLabels[parsedLabel] = value
|
||||
|
||||
return filteredContainerLabels
|
||||
@@ -0,0 +1,185 @@
|
||||
from uptime_kuma_api.api import UptimeKumaApi, MonitorType
|
||||
from helpers import *
|
||||
import os
|
||||
|
||||
logPrefix = "[mod-auto-uptime-kuma]"
|
||||
|
||||
|
||||
class SwagUptimeKuma:
|
||||
swagTagName = "swag"
|
||||
swagUptimeKumaConfigDir = "/auto-uptime-kuma"
|
||||
|
||||
_api = None
|
||||
_apiSwagTag = None
|
||||
apiMonitors = None
|
||||
|
||||
defaultMonitorConfig = dict(
|
||||
type=MonitorType.HTTP,
|
||||
description="Automatically generated by SWAG auto-uptime-kuma"
|
||||
)
|
||||
|
||||
def __init__(self, url, username, password):
|
||||
self._api = UptimeKumaApi(url)
|
||||
self._api.login(username, password)
|
||||
self.apiMonitors = self._api.get_monitors()
|
||||
|
||||
if not os.path.exists(self.swagUptimeKumaConfigDir):
|
||||
print(
|
||||
f"{logPrefix} Creating config directory '{self.swagUptimeKumaConfigDir}'")
|
||||
os.makedirs(self.swagUptimeKumaConfigDir)
|
||||
|
||||
def disconnect(self):
|
||||
"""
|
||||
API has to be disconnected at the end as the connection is blocking
|
||||
"""
|
||||
self._api.disconnect()
|
||||
|
||||
def getSwagTag(self):
|
||||
"""
|
||||
The "swag" tag is used to detect in API which monitors were created using this script.
|
||||
"""
|
||||
# If the tag was not fetched yet
|
||||
if (self._apiSwagTag == None):
|
||||
for tag in self._api.get_tags():
|
||||
if (tag['name'] == self.swagTagName):
|
||||
self._apiSwagTag = tag
|
||||
break
|
||||
|
||||
# If the tag was not in API then it has to be created
|
||||
if (self._apiSwagTag == None):
|
||||
self._apiSwagTag = self._api.add_tag(
|
||||
name=self.swagTagName, color="#ff4f97")
|
||||
|
||||
return self._apiSwagTag
|
||||
|
||||
def parseMonitorData(self, containerName, domainName, monitorData):
|
||||
"""
|
||||
Some of the container label values might have to be converted before sending to API.
|
||||
Additionally merge default config with label config.
|
||||
"""
|
||||
# Convert strings that are lists in API
|
||||
for key in ["accepted_statuscodes", "notificationIDList"]:
|
||||
if (key in monitorData and type(monitorData[key]) is str):
|
||||
monitorData[key] = monitorData[key].split(",")
|
||||
|
||||
dynamicMonitorConfig = {
|
||||
"name": containerName.title(),
|
||||
"url": f"https://{containerName}.{domainName}"
|
||||
}
|
||||
|
||||
return merge_dicts(self.defaultMonitorConfig, dynamicMonitorConfig, monitorData)
|
||||
|
||||
def addMonitor(self, containerName, domainName, monitorData):
|
||||
monitorData = self.parseMonitorData(
|
||||
containerName, domainName, monitorData)
|
||||
if (has_key_with_value(self.apiMonitors, "name", monitorData['name'])):
|
||||
print(
|
||||
f"{logPrefix} Uptime Kuma already contains '{monitorData['name']}' monitor, skipping...")
|
||||
return
|
||||
|
||||
print(
|
||||
f"{logPrefix} Adding monitor '{monitorData['name']}'")
|
||||
|
||||
monitor = self._api.add_monitor(**monitorData)
|
||||
|
||||
self._api.add_monitor_tag(
|
||||
tag_id=self.getSwagTag()['id'],
|
||||
monitor_id=monitor['monitorID'],
|
||||
value=containerName
|
||||
)
|
||||
|
||||
content = self.buildContainerConfigContent(monitorData)
|
||||
write_file(
|
||||
f"{self.swagUptimeKumaConfigDir}/{containerName}.conf", content)
|
||||
|
||||
def deleteMonitor(self, containerName):
|
||||
monitorData = self.getMonitor(containerName)
|
||||
print(
|
||||
f"{logPrefix} Deleting monitor {monitorData['id']}:{monitorData['name']}")
|
||||
self._api.delete_monitor(monitorData['id'])
|
||||
|
||||
def deleteMonitors(self, containerNames):
|
||||
print(f"{logPrefix} Deleting all monitors that had their containers removed")
|
||||
if (containerNames):
|
||||
for containerName in containerNames:
|
||||
self.deleteMonitor(containerName)
|
||||
else:
|
||||
print(f"{logPrefix} Nothing to remove")
|
||||
|
||||
def updateMonitor(self, containerName, domainName, monitorData):
|
||||
"""
|
||||
Please not that due to API limitations the "update" action is actually "delete" followed by "add"
|
||||
so that in the end the monitors are actually recreated
|
||||
"""
|
||||
newContent = self.buildContainerConfigContent(monitorData)
|
||||
oldContent = self.readContainerConfigContent(containerName)
|
||||
existingMonitorData = self.getMonitor(containerName)
|
||||
|
||||
if (not oldContent == newContent):
|
||||
print(
|
||||
f"{logPrefix} Updating (Delete and Add) monitor {existingMonitorData['id']}:{existingMonitorData['name']}")
|
||||
self.deleteMonitor(containerName)
|
||||
self.addMonitor(containerName, domainName, monitorData)
|
||||
else:
|
||||
print(
|
||||
f"{logPrefix} Monitor {existingMonitorData['id']}:{existingMonitorData['name']} is unchanged, skipping...")
|
||||
|
||||
def buildContainerConfigContent(self, monitorData):
|
||||
"""
|
||||
In order to compare if container labels were changed the contents are stored in config files for each container.
|
||||
"""
|
||||
content = ""
|
||||
for key, value in monitorData.items():
|
||||
content += f'{key}={value}\n'
|
||||
return content.strip()
|
||||
|
||||
def readContainerConfigContent(self, containerName):
|
||||
fileName = f"{self.swagUptimeKumaConfigDir}/{containerName}.conf"
|
||||
if (not os.path.exists(fileName)):
|
||||
return ""
|
||||
|
||||
return read_file(fileName).strip()
|
||||
|
||||
def getMonitor(self, containerName):
|
||||
for monitor in self.apiMonitors:
|
||||
swagTagValue = self.getMonitorSwagTagValue(monitor)
|
||||
if (swagTagValue != None and swagTagValue == containerName):
|
||||
return monitor
|
||||
return None
|
||||
|
||||
def monitorExists(self, containerName):
|
||||
return True if self.getMonitor(containerName) else False
|
||||
|
||||
def getMonitorSwagTagValue(self, monitorData):
|
||||
"""
|
||||
This value is container name itself. Used to link containers with monitors
|
||||
"""
|
||||
for tag in monitorData.get('tags'):
|
||||
if (has_key_with_value(tag, "name", self.swagTagName)):
|
||||
return tag['value']
|
||||
return None
|
||||
|
||||
def purgeData(self):
|
||||
"""
|
||||
Removes all of the monitors and files created with this script
|
||||
"""
|
||||
print(f"{logPrefix} Purging all monitors added by swag")
|
||||
|
||||
for monitor in self.apiMonitors:
|
||||
containerName = self.getMonitorSwagTagValue(monitor)
|
||||
if (containerName != None):
|
||||
self.deleteMonitor(containerName)
|
||||
|
||||
if os.path.exists(self.swagUptimeKumaConfigDir):
|
||||
print(
|
||||
f"{logPrefix} Purging config directory '{self.swagUptimeKumaConfigDir}'")
|
||||
file_list = os.listdir(self.swagUptimeKumaConfigDir)
|
||||
|
||||
for filename in file_list:
|
||||
file_path = os.path.join(
|
||||
self.swagUptimeKumaConfigDir, filename)
|
||||
if os.path.isfile(file_path):
|
||||
os.remove(file_path)
|
||||
print(f"{logPrefix} Removed '{file_path}' file")
|
||||
|
||||
print(f"{logPrefix} Purging finished")
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
|
||||
# This is the init file used for adding os or pip packages to install lists.
|
||||
# It takes advantage of the built-in init-mods-package-install init script that comes with the baseimages.
|
||||
# If using this, we need to make sure we set this init as a dependency of init-mods-package-install so this one runs first
|
||||
|
||||
if ! command -v apprise; then
|
||||
echo "**** Adding apprise and its deps to package install lists ****"
|
||||
echo "apprise" >> /mod-pip-packages-to-install.list
|
||||
## Ubuntu
|
||||
if [ -f /usr/bin/apt ]; then
|
||||
echo "\
|
||||
python3 \
|
||||
python3-pip \
|
||||
runc" >> /mod-repo-packages-to-install.list
|
||||
fi
|
||||
# Alpine
|
||||
if [ -f /sbin/apk ]; then
|
||||
echo "\
|
||||
cargo \
|
||||
libffi-dev \
|
||||
openssl-dev \
|
||||
python3 \
|
||||
python3-dev \
|
||||
python3 \
|
||||
py3-pip" >> /mod-repo-packages-to-install.list
|
||||
fi
|
||||
else
|
||||
echo "**** apprise already installed, skipping ****"
|
||||
fi
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
/etc/s6-overlay/s6-rc.d/init-mod-imagename-modname-add-package/run
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
|
||||
# This is an install script that is designed to run after init-mods-package-install
|
||||
# so it can take advantage of packages installed
|
||||
# init-mods-end depends on this script so that later init and services wait until this script exits
|
||||
|
||||
echo "**** Setting up apprise ****"
|
||||
apprise blah blah
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
/etc/s6-overlay/s6-rc.d/init-mod-imagename-modname-install/run
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
|
||||
echo "[mod-swag-auto-uptime-kuma] Installing SWAG auto-uptime-kuma packages"
|
||||
|
||||
if ! pip list 2>&1 | grep -q "uptime-kuma-api\|docker"; then
|
||||
echo "\
|
||||
docker \
|
||||
uptime-kuma-api" >> /mod-pip-packages-to-install.list
|
||||
echo "[mod-swag-auto-uptime-kuma] Successfuly installed packages"
|
||||
else
|
||||
echo "[mod-swag-auto-uptime-kuma] Packages already installed, skipping..."
|
||||
fi
|
||||
@@ -0,0 +1 @@
|
||||
oneshot
|
||||
@@ -0,0 +1 @@
|
||||
/etc/s6-overlay/s6-rc.d/init-mod-swag-auto-uptime-kuma-add-package/run
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
|
||||
if [ -z "$UPTIME_KUMA_URL" ] || [ -z "$UPTIME_KUMA_USERNAME" ] || [ -z "$UPTIME_KUMA_PASSWORD" ]; then
|
||||
echo "[mod-swag-auto-uptime-kuma] Missing required environment variables. Please refer to the Readme, skipping..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "[mod-swag-auto-uptime-kuma] Executing SWAG auto-uptime-kuma mod"
|
||||
|
||||
scriptPath='/app/auto-uptime-kuma.py'
|
||||
|
||||
if [ -e "$scriptPath" ] && [ ! -x "$scriptPath" ]; then
|
||||
chmod +x "$scriptPath"
|
||||
fi
|
||||
|
||||
python3 $scriptPath
|
||||
@@ -0,0 +1 @@
|
||||
oneshot
|
||||
@@ -0,0 +1 @@
|
||||
/etc/s6-overlay/s6-rc.d/init-mod-swag-auto-uptime-kuma-install/run
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
|
||||
# This is an example service that would run for the mod
|
||||
# It depends on init-services, the baseimage hook for start of all longrun services
|
||||
|
||||
exec \
|
||||
s6-setuidgid abc run my app
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
Reference in New Issue
Block a user