forked from michael.heier/citadel-core
Implement quick updates (#56)
Co-authored-by: nolim1t - f6287b82CC84bcbd <nolim1t@users.noreply.github.com> Co-authored-by: Philipp Walter <philippwalter@pm.me>
This commit is contained in:
parent
61f5f9f1e0
commit
cea004770c
|
@ -3,7 +3,7 @@
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
# A collection of fully FLOSS app definitions and FLOSS apps for Citadel.
|
# A collection of fully FLOSS app definitions and FLOSS apps for Citadel.
|
||||||
https://github.com/runcitadel/apps v3-stable
|
https://github.com/runcitadel/apps v4-beta
|
||||||
|
|
||||||
# Some apps modified version of Umbrel apps, and their app definitions aren't FLOSS yet.
|
# Some apps modified version of Umbrel apps, and their app definitions aren't FLOSS yet.
|
||||||
# Include them anyway, but as a separate repo.
|
# Include them anyway, but as a separate repo.
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
from lib.citadelutils import classToDict
|
|
||||||
from lib.composegenerator.shared.env import validateEnv
|
|
||||||
|
|
||||||
from lib.composegenerator.v3.types import App, generateApp
|
|
||||||
from lib.composegenerator.v3.generate import convertContainerPermissions
|
|
||||||
|
|
||||||
def createCleanConfigFromV3(app: dict, nodeRoot: str):
|
|
||||||
parsedApp: App = generateApp(app)
|
|
||||||
for container in range(len(parsedApp.containers)):
|
|
||||||
# TODO: Make this dynamic and not hardcoded
|
|
||||||
if parsedApp.containers[container].requires and "c-lightning" in parsedApp.containers[container].requires:
|
|
||||||
parsedApp.containers[container] = None
|
|
||||||
parsedApp = convertContainerPermissions(parsedApp)
|
|
||||||
parsedApp = validateEnv(parsedApp)
|
|
||||||
finalApp = classToDict(parsedApp)
|
|
||||||
try:
|
|
||||||
finalApp['permissions'] = finalApp['metadata']['dependencies']
|
|
||||||
except:
|
|
||||||
finalApp['permissions'] = []
|
|
||||||
finalApp['id'] = finalApp['metadata']['id']
|
|
||||||
del finalApp['metadata']
|
|
||||||
# Set version of the cache file format
|
|
||||||
finalApp['version'] = "1"
|
|
||||||
return finalApp
|
|
|
@ -6,9 +6,11 @@ import stat
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
|
import random
|
||||||
from typing import List
|
from typing import List
|
||||||
from sys import argv
|
from sys import argv
|
||||||
import os
|
import os
|
||||||
|
import fcntl
|
||||||
import requests
|
import requests
|
||||||
import shutil
|
import shutil
|
||||||
import json
|
import json
|
||||||
|
@ -32,9 +34,31 @@ from lib.validate import findAndValidateApps
|
||||||
from lib.metadata import getAppRegistry
|
from lib.metadata import getAppRegistry
|
||||||
from lib.entropy import deriveEntropy
|
from lib.entropy import deriveEntropy
|
||||||
|
|
||||||
|
class FileLock:
|
||||||
|
"""Implements a file-based lock using flock(2).
|
||||||
|
The lock file is saved in directory dir with name lock_name.
|
||||||
|
dir is the current directory by default.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, lock_name, dir="."):
|
||||||
|
self.lock_file = open(os.path.join(dir, lock_name), "w")
|
||||||
|
|
||||||
|
def acquire(self, blocking=True):
|
||||||
|
"""Acquire the lock.
|
||||||
|
If the lock is not already acquired, return None. If the lock is
|
||||||
|
acquired and blocking is True, block until the lock is released. If
|
||||||
|
the lock is acquired and blocking is False, raise an IOError.
|
||||||
|
"""
|
||||||
|
ops = fcntl.LOCK_EX
|
||||||
|
if not blocking:
|
||||||
|
ops |= fcntl.LOCK_NB
|
||||||
|
fcntl.flock(self.lock_file, ops)
|
||||||
|
|
||||||
|
def release(self):
|
||||||
|
"""Release the lock. Return None even if lock not currently acquired"""
|
||||||
|
fcntl.flock(self.lock_file, fcntl.LOCK_UN)
|
||||||
|
|
||||||
# For an array of threads, join them and wait for them to finish
|
# For an array of threads, join them and wait for them to finish
|
||||||
|
|
||||||
|
|
||||||
def joinThreads(threads: List[threading.Thread]):
|
def joinThreads(threads: List[threading.Thread]):
|
||||||
for thread in threads:
|
for thread in threads:
|
||||||
thread.join()
|
thread.join()
|
||||||
|
@ -50,26 +74,58 @@ updateIgnore = os.path.join(appsDir, ".updateignore")
|
||||||
appDataDir = os.path.join(nodeRoot, "app-data")
|
appDataDir = os.path.join(nodeRoot, "app-data")
|
||||||
userFile = os.path.join(nodeRoot, "db", "user.json")
|
userFile = os.path.join(nodeRoot, "db", "user.json")
|
||||||
legacyScript = os.path.join(nodeRoot, "scripts", "app")
|
legacyScript = os.path.join(nodeRoot, "scripts", "app")
|
||||||
|
with open(os.path.join(nodeRoot, "db", "dependencies.yml"), "r") as file:
|
||||||
|
dependencies = yaml.safe_load(file)
|
||||||
|
|
||||||
|
|
||||||
# Returns a list of every argument after the second one in sys.argv joined into a string by spaces
|
# Returns a list of every argument after the second one in sys.argv joined into a string by spaces
|
||||||
|
|
||||||
|
|
||||||
def getArguments():
|
def getArguments():
|
||||||
arguments = ""
|
arguments = ""
|
||||||
for i in range(3, len(argv)):
|
for i in range(3, len(argv)):
|
||||||
arguments += argv[i] + " "
|
arguments += argv[i] + " "
|
||||||
return arguments
|
return arguments
|
||||||
|
|
||||||
|
def handleAppV4(app):
|
||||||
|
composeFile = os.path.join(appsDir, app, "docker-compose.yml")
|
||||||
|
os.chown(os.path.join(appsDir, app), 1000, 1000)
|
||||||
|
os.system("docker run --rm -v {}:/apps -u 1000:1000 {} /app-cli convert --app-name '{}' --port-map /apps/ports.json /apps/{}/app.yml /apps/{}/result.yml --services 'lnd'".format(appsDir, dependencies['app-cli'], app, app, app))
|
||||||
|
with open(os.path.join(appsDir, app, "result.yml"), "r") as resultFile:
|
||||||
|
resultYml = yaml.safe_load(resultFile)
|
||||||
|
with open(composeFile, "w") as dockerComposeFile:
|
||||||
|
yaml.dump(resultYml["spec"], dockerComposeFile)
|
||||||
|
torDaemons = ["torrc-apps", "torrc-apps-2", "torrc-apps-3"]
|
||||||
|
torFileToAppend = torDaemons[random.randint(0, len(torDaemons) - 1)]
|
||||||
|
with open(os.path.join(nodeRoot, "tor", torFileToAppend), 'a') as f:
|
||||||
|
f.write(resultYml["new_tor_entries"])
|
||||||
|
mainPort = resultYml["port"]
|
||||||
|
registryFile = os.path.join(nodeRoot, "apps", "registry.json")
|
||||||
|
registry: list = []
|
||||||
|
lock = FileLock("citadeL_registry_lock", dir="/tmp")
|
||||||
|
lock.acquire()
|
||||||
|
if os.path.isfile(registryFile):
|
||||||
|
with open(registryFile, 'r') as f:
|
||||||
|
registry = json.load(f)
|
||||||
|
else:
|
||||||
|
raise Exception("Registry file not found")
|
||||||
|
|
||||||
|
for registryApp in registry:
|
||||||
|
if registryApp['id'] == app:
|
||||||
|
registry[registry.index(registryApp)]['port'] = resultYml["port"]
|
||||||
|
break
|
||||||
|
|
||||||
|
with open(registryFile, 'w') as f:
|
||||||
|
json.dump(registry, f, indent=4, sort_keys=True)
|
||||||
|
lock.release()
|
||||||
|
|
||||||
def getAppYml(name):
|
def getAppYml(name):
|
||||||
with open(os.path.join(appsDir, "sourceMap.json"), "r") as f:
|
with open(os.path.join(appsDir, "sourceMap.json"), "r") as f:
|
||||||
sourceMap = json.load(f)
|
sourceMap = json.load(f)
|
||||||
if not name in sourceMap:
|
if not name in sourceMap:
|
||||||
print("Warning: App {} is not in the source map".format(name))
|
print("Warning: App {} is not in the source map".format(name), file=sys.stderr)
|
||||||
sourceMap = {
|
sourceMap = {
|
||||||
name: {
|
name: {
|
||||||
"githubRepo": "runcitadel/core",
|
"githubRepo": "runcitadel/apps",
|
||||||
"branch": "v2"
|
"branch": "v4-stable"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
url = 'https://raw.githubusercontent.com/{}/{}/apps/{}/app.yml'.format(sourceMap[name]["githubRepo"], sourceMap[name]["branch"], name)
|
url = 'https://raw.githubusercontent.com/{}/{}/apps/{}/app.yml'.format(sourceMap[name]["githubRepo"], sourceMap[name]["branch"], name)
|
||||||
|
@ -89,16 +145,31 @@ def update(verbose: bool = False):
|
||||||
json.dump(registry["ports"], f, sort_keys=True)
|
json.dump(registry["ports"], f, sort_keys=True)
|
||||||
print("Wrote registry to registry.json")
|
print("Wrote registry to registry.json")
|
||||||
|
|
||||||
|
os.system("docker pull {}".format(dependencies['app-cli']))
|
||||||
|
threads = list()
|
||||||
# Loop through the apps and generate valid compose files from them, then put these into the app dir
|
# Loop through the apps and generate valid compose files from them, then put these into the app dir
|
||||||
for app in apps:
|
for app in apps:
|
||||||
composeFile = os.path.join(appsDir, app, "docker-compose.yml")
|
try:
|
||||||
appYml = os.path.join(appsDir, app, "app.yml")
|
composeFile = os.path.join(appsDir, app, "docker-compose.yml")
|
||||||
with open(composeFile, "w") as f:
|
appYml = os.path.join(appsDir, app, "app.yml")
|
||||||
appCompose = getApp(appYml, app)
|
with open(appYml, 'r') as f:
|
||||||
if appCompose:
|
appDefinition = yaml.safe_load(f)
|
||||||
f.write(yaml.dump(appCompose, sort_keys=False))
|
if 'citadel_version' in appDefinition:
|
||||||
if verbose:
|
thread = threading.Thread(target=handleAppV4, args=(app,))
|
||||||
print("Wrote " + app + " to " + composeFile)
|
thread.start()
|
||||||
|
threads.append(thread)
|
||||||
|
else:
|
||||||
|
appCompose = getApp(appDefinition, app)
|
||||||
|
with open(composeFile, "w") as f:
|
||||||
|
if appCompose:
|
||||||
|
f.write(yaml.dump(appCompose, sort_keys=False))
|
||||||
|
if verbose:
|
||||||
|
print("Wrote " + app + " to " + composeFile)
|
||||||
|
except Exception as err:
|
||||||
|
print("Failed to convert app {}".format(app))
|
||||||
|
print(err)
|
||||||
|
|
||||||
|
joinThreads(threads)
|
||||||
print("Generated configuration successfully")
|
print("Generated configuration successfully")
|
||||||
|
|
||||||
|
|
||||||
|
@ -158,12 +229,7 @@ def stopInstalled():
|
||||||
joinThreads(threads)
|
joinThreads(threads)
|
||||||
|
|
||||||
# Loads an app.yml and converts it to a docker-compose.yml
|
# Loads an app.yml and converts it to a docker-compose.yml
|
||||||
|
def getApp(app, appId: str):
|
||||||
|
|
||||||
def getApp(appFile: str, appId: str):
|
|
||||||
with open(appFile, 'r') as f:
|
|
||||||
app = yaml.safe_load(f)
|
|
||||||
|
|
||||||
if not "metadata" in app:
|
if not "metadata" in app:
|
||||||
raise Exception("Error: Could not find metadata in " + appFile)
|
raise Exception("Error: Could not find metadata in " + appFile)
|
||||||
app["metadata"]["id"] = appId
|
app["metadata"]["id"] = appId
|
||||||
|
@ -175,6 +241,7 @@ def getApp(appFile: str, appId: str):
|
||||||
print("Warning: App {} uses version 2 of the app.yml format, which is scheduled for removal in Citadel 0.2.0".format(appId))
|
print("Warning: App {} uses version 2 of the app.yml format, which is scheduled for removal in Citadel 0.2.0".format(appId))
|
||||||
return createComposeConfigFromV2(app, nodeRoot)
|
return createComposeConfigFromV2(app, nodeRoot)
|
||||||
elif 'version' in app and str(app['version']) == "3":
|
elif 'version' in app and str(app['version']) == "3":
|
||||||
|
print("Warning: App {} uses version 3 of the app.yml format, which is scheduled for removal in Citadel 0.3.0".format(appId))
|
||||||
return createComposeConfigFromV3(app, nodeRoot)
|
return createComposeConfigFromV3(app, nodeRoot)
|
||||||
else:
|
else:
|
||||||
raise Exception("Error: Unsupported version of app.yml")
|
raise Exception("Error: Unsupported version of app.yml")
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
|
import traceback
|
||||||
|
|
||||||
from lib.composegenerator.next.stage1 import createCleanConfigFromV3
|
|
||||||
from lib.composegenerator.v2.networking import getMainContainer
|
from lib.composegenerator.v2.networking import getMainContainer
|
||||||
from lib.composegenerator.v1.networking import getFreePort
|
from lib.composegenerator.shared.networking import assignIpV4
|
||||||
from lib.entropy import deriveEntropy
|
from lib.entropy import deriveEntropy
|
||||||
from typing import List
|
from typing import List
|
||||||
import json
|
import json
|
||||||
|
@ -41,11 +41,15 @@ def getAppRegistry(apps, app_path):
|
||||||
app_metadata = []
|
app_metadata = []
|
||||||
for app in apps:
|
for app in apps:
|
||||||
app_yml_path = os.path.join(app_path, app, 'app.yml')
|
app_yml_path = os.path.join(app_path, app, 'app.yml')
|
||||||
app_cache_path = os.path.join(app_path, app, 'app.cache.json')
|
|
||||||
if os.path.isfile(app_yml_path):
|
if os.path.isfile(app_yml_path):
|
||||||
try:
|
try:
|
||||||
with open(app_yml_path, 'r') as f:
|
with open(app_yml_path, 'r') as f:
|
||||||
app_yml = yaml.safe_load(f.read())
|
app_yml = yaml.safe_load(f.read())
|
||||||
|
version = False
|
||||||
|
if 'version' in app_yml:
|
||||||
|
version = int(app_yml['version'])
|
||||||
|
elif 'citadel_version' in app_yml:
|
||||||
|
version = int(app_yml['citadel_version'])
|
||||||
metadata: dict = app_yml['metadata']
|
metadata: dict = app_yml['metadata']
|
||||||
metadata['id'] = app
|
metadata['id'] = app
|
||||||
metadata['path'] = metadata.get('path', '')
|
metadata['path'] = metadata.get('path', '')
|
||||||
|
@ -55,14 +59,14 @@ def getAppRegistry(apps, app_path):
|
||||||
if "mainContainer" in metadata:
|
if "mainContainer" in metadata:
|
||||||
metadata.pop("mainContainer")
|
metadata.pop("mainContainer")
|
||||||
app_metadata.append(metadata)
|
app_metadata.append(metadata)
|
||||||
if(app_yml["version"] != 3):
|
if version < 3:
|
||||||
getPortsOldApp(app_yml, app)
|
getPortsOldApp(app_yml, app)
|
||||||
else:
|
elif version == 3:
|
||||||
getPortsV3App(app_yml, app)
|
getPortsV3App(app_yml, app)
|
||||||
with open(app_cache_path, 'w') as f:
|
elif version == 4:
|
||||||
json.dump(createCleanConfigFromV3(app_yml, os.path.dirname(app_path)), f)
|
getPortsV4App(app_yml, app)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(traceback.format_exc())
|
||||||
print("App {} is invalid!".format(app))
|
print("App {} is invalid!".format(app))
|
||||||
appPortsToMap()
|
appPortsToMap()
|
||||||
return {
|
return {
|
||||||
|
@ -97,12 +101,12 @@ def getNewPort(usedPorts):
|
||||||
lastPort2 = lastPort2 + 1
|
lastPort2 = lastPort2 + 1
|
||||||
return lastPort2
|
return lastPort2
|
||||||
|
|
||||||
def validatePort(appContainer, port, appId, priority: int, isDynamic = False):
|
def validatePort(containerName, appContainer, port, appId, priority: int, isDynamic = False):
|
||||||
if port not in appPorts and port not in citadelPorts and port != 0:
|
if port not in appPorts and port not in citadelPorts and port != 0:
|
||||||
appPorts[port] = {
|
appPorts[port] = {
|
||||||
"app": appId,
|
"app": appId,
|
||||||
"port": port,
|
"port": port,
|
||||||
"container": appContainer["name"],
|
"container": containerName,
|
||||||
"priority": priority,
|
"priority": priority,
|
||||||
"dynamic": isDynamic,
|
"dynamic": isDynamic,
|
||||||
}
|
}
|
||||||
|
@ -115,7 +119,7 @@ def validatePort(appContainer, port, appId, priority: int, isDynamic = False):
|
||||||
appPorts[port] = {
|
appPorts[port] = {
|
||||||
"app": appId,
|
"app": appId,
|
||||||
"port": port,
|
"port": port,
|
||||||
"container": appContainer["name"],
|
"container": containerName,
|
||||||
"priority": priority,
|
"priority": priority,
|
||||||
"dynamic": isDynamic,
|
"dynamic": isDynamic,
|
||||||
}
|
}
|
||||||
|
@ -128,7 +132,7 @@ def validatePort(appContainer, port, appId, priority: int, isDynamic = False):
|
||||||
appPorts[newPort] = {
|
appPorts[newPort] = {
|
||||||
"app": appId,
|
"app": appId,
|
||||||
"port": port,
|
"port": port,
|
||||||
"container": appContainer["name"],
|
"container": containerName,
|
||||||
"priority": priority,
|
"priority": priority,
|
||||||
"dynamic": isDynamic,
|
"dynamic": isDynamic,
|
||||||
}
|
}
|
||||||
|
@ -136,28 +140,44 @@ def validatePort(appContainer, port, appId, priority: int, isDynamic = False):
|
||||||
def getPortsOldApp(app, appId):
|
def getPortsOldApp(app, appId):
|
||||||
for appContainer in app["containers"]:
|
for appContainer in app["containers"]:
|
||||||
if "port" in appContainer:
|
if "port" in appContainer:
|
||||||
validatePort(appContainer, appContainer["port"], appId, 0)
|
validatePort(appContainer["name"], appContainer, appContainer["port"], appId, 0)
|
||||||
if "ports" in appContainer:
|
if "ports" in appContainer:
|
||||||
for port in appContainer["ports"]:
|
for port in appContainer["ports"]:
|
||||||
realPort = int(str(port).split(":")[0])
|
realPort = int(str(port).split(":")[0])
|
||||||
validatePort(appContainer, realPort, appId, 2)
|
validatePort(appContainer["name"], appContainer, realPort, appId, 2)
|
||||||
|
|
||||||
|
|
||||||
def getPortsV3App(app, appId):
|
def getPortsV3App(app, appId):
|
||||||
for appContainer in app["containers"]:
|
for appContainer in app["containers"]:
|
||||||
if "port" in appContainer:
|
if "port" in appContainer:
|
||||||
if "preferredOutsidePort" in appContainer and "requiresPort" in appContainer and appContainer["requiresPort"]:
|
if "preferredOutsidePort" in appContainer and "requiresPort" in appContainer and appContainer["requiresPort"]:
|
||||||
validatePort(appContainer, appContainer["preferredOutsidePort"], appId, 2)
|
validatePort(appContainer["name"], appContainer, appContainer["preferredOutsidePort"], appId, 2)
|
||||||
elif "preferredOutsidePort" in appContainer:
|
elif "preferredOutsidePort" in appContainer:
|
||||||
|
|
||||||
validatePort(appContainer, appContainer["preferredOutsidePort"], appId, 1)
|
validatePort(appContainer["name"], appContainer, appContainer["preferredOutsidePort"], appId, 1)
|
||||||
else:
|
else:
|
||||||
validatePort(appContainer, appContainer["port"], appId, 0)
|
validatePort(appContainer["name"], appContainer, appContainer["port"], appId, 0)
|
||||||
elif "requiredPorts" not in appContainer and "requiredUdpPorts" not in appContainer:
|
elif "requiredPorts" not in appContainer and "requiredUdpPorts" not in appContainer:
|
||||||
validatePort(appContainer, getNewPort(appPorts.keys()), appId, 0, True)
|
validatePort(appContainer["name"], appContainer, getNewPort(appPorts.keys()), appId, 0, True)
|
||||||
if "requiredPorts" in appContainer:
|
if "requiredPorts" in appContainer:
|
||||||
for port in appContainer["requiredPorts"]:
|
for port in appContainer["requiredPorts"]:
|
||||||
validatePort(appContainer, port, appId, 2)
|
validatePort(appContainer["name"], appContainer, port, appId, 2)
|
||||||
if "requiredUdpPorts" in appContainer:
|
if "requiredUdpPorts" in appContainer:
|
||||||
for port in appContainer["requiredUdpPorts"]:
|
for port in appContainer["requiredUdpPorts"]:
|
||||||
validatePort(appContainer, port, appId, 2)
|
validatePort(appContainer["name"], appContainer, port, appId, 2)
|
||||||
|
|
||||||
|
def getPortsV4App(app, appId):
|
||||||
|
for appContainerName in app["services"].keys():
|
||||||
|
appContainer = app["services"][appContainerName]
|
||||||
|
if "enable_networking" in appContainer and not appContainer["enable_networking"]:
|
||||||
|
return
|
||||||
|
assignIpV4(appId, appContainerName)
|
||||||
|
if "port" in appContainer:
|
||||||
|
validatePort(appContainerName, appContainer, appContainer["port"], appId, 0)
|
||||||
|
if "required_ports" in appContainer:
|
||||||
|
if "tcp" in appContainer["required_ports"]:
|
||||||
|
for port in appContainer["required_ports"]["tcp"].keys():
|
||||||
|
validatePort(appContainerName, appContainer, port, appId, 2)
|
||||||
|
if "udp" in appContainer["required_ports"]:
|
||||||
|
for port in appContainer["required_ports"]["udp"].keys():
|
||||||
|
validatePort(appContainerName, appContainer, port, appId, 2)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import os
|
||||||
import yaml
|
import yaml
|
||||||
from jsonschema import validate
|
from jsonschema import validate
|
||||||
import yaml
|
import yaml
|
||||||
|
import traceback
|
||||||
|
|
||||||
scriptDir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")
|
scriptDir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")
|
||||||
|
|
||||||
|
@ -33,7 +34,7 @@ def validateApp(app: dict):
|
||||||
return True
|
return True
|
||||||
# Catch and log any errors, and return false
|
# Catch and log any errors, and return false
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(traceback.format_exc())
|
||||||
return False
|
return False
|
||||||
elif 'version' in app and str(app['version']) == "3":
|
elif 'version' in app and str(app['version']) == "3":
|
||||||
try:
|
try:
|
||||||
|
@ -41,12 +42,13 @@ def validateApp(app: dict):
|
||||||
return True
|
return True
|
||||||
# Catch and log any errors, and return false
|
# Catch and log any errors, and return false
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(traceback.format_exc())
|
||||||
return False
|
return False
|
||||||
else:
|
elif 'version' not in app and 'citadel_version' not in app:
|
||||||
print("Unsupported app version")
|
print("Unsupported app version")
|
||||||
return False
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
# Read in an app.yml file and pass it to the validation function
|
# Read in an app.yml file and pass it to the validation function
|
||||||
# Returns true if valid, false otherwise
|
# Returns true if valid, false otherwise
|
||||||
|
@ -72,14 +74,17 @@ def findApps(dir: str):
|
||||||
def findAndValidateApps(dir: str):
|
def findAndValidateApps(dir: str):
|
||||||
apps = []
|
apps = []
|
||||||
app_data = {}
|
app_data = {}
|
||||||
for root, dirs, files in os.walk(dir, topdown=False):
|
for subdir in os.scandir(dir):
|
||||||
for name in dirs:
|
if not subdir.is_dir():
|
||||||
app_dir = os.path.join(root, name)
|
continue
|
||||||
if os.path.isfile(os.path.join(app_dir, "app.yml")):
|
app_dir = subdir.path
|
||||||
apps.append(name)
|
if os.path.isfile(os.path.join(app_dir, "app.yml")):
|
||||||
# Read the app.yml and append it to app_data
|
apps.append(subdir.name)
|
||||||
with open(os.path.join(app_dir, "app.yml"), 'r') as f:
|
# Read the app.yml and append it to app_data
|
||||||
app_data[name] = yaml.safe_load(f)
|
with open(os.path.join(app_dir, "app.yml"), 'r') as f:
|
||||||
|
app_data[subdir.name] = yaml.safe_load(f)
|
||||||
|
else:
|
||||||
|
print("App {} has no app.yml".format(subdir.name))
|
||||||
# Now validate all the apps using the validateAppFile function by passing the app.yml as an argument to it, if an app is invalid, remove it from the list
|
# Now validate all the apps using the validateAppFile function by passing the app.yml as an argument to it, if an app is invalid, remove it from the list
|
||||||
for app in apps:
|
for app in apps:
|
||||||
appyml = app_data[app]
|
appyml = app_data[app]
|
||||||
|
@ -113,12 +118,13 @@ def findAndValidateApps(dir: str):
|
||||||
should_continue=False
|
should_continue=False
|
||||||
if not should_continue:
|
if not should_continue:
|
||||||
continue
|
continue
|
||||||
for container in appyml['containers']:
|
if 'containers' in appyml:
|
||||||
if 'permissions' in container:
|
for container in appyml['containers']:
|
||||||
for permission in container['permissions']:
|
if 'permissions' in container:
|
||||||
if permission not in appyml['metadata']['dependencies'] and permission not in ["root", "hw"]:
|
for permission in container['permissions']:
|
||||||
print("WARNING: App {}'s container '{}' requires the '{}' permission, but the app doesn't list it in it's dependencies".format(app, container['name'], permission))
|
if permission not in appyml['metadata']['dependencies'] and permission not in ["root", "hw"]:
|
||||||
apps.remove(app)
|
print("WARNING: App {}'s container '{}' requires the '{}' permission, but the app doesn't list it in it's dependencies".format(app, container['name'], permission))
|
||||||
# Skip to the next iteration of the loop
|
apps.remove(app)
|
||||||
continue
|
# Skip to the next iteration of the loop
|
||||||
|
continue
|
||||||
return apps
|
return apps
|
||||||
|
|
5
db/dependencies.yml
Normal file
5
db/dependencies.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
compose: v2.6.0
|
||||||
|
dashboard: ghcr.io/runcitadel/dashboard:main@sha256:25b6fb413c10f47e186309c8737926c241c0f2bec923b2c08dd837b828f14dbd
|
||||||
|
manager: ghcr.io/runcitadel/manager:main@sha256:db5775e986d53e762e43331540bb1c05a27b362da94d587c4a4591c981c00ee4
|
||||||
|
middleware: ghcr.io/runcitadel/middleware:main@sha256:2fbbfb2e818bf0462f74a6aaab192881615ae018e6dcb62a50d05f82ec622cb0
|
||||||
|
app-cli: ghcr.io/runcitadel/app-cli:main@sha256:f532923eac28cfac03579cbb440397bcf16c8730f291b39eeada8278331f7054
|
|
@ -100,7 +100,7 @@ services:
|
||||||
ipv4_address: $LND_IP
|
ipv4_address: $LND_IP
|
||||||
dashboard:
|
dashboard:
|
||||||
container_name: dashboard
|
container_name: dashboard
|
||||||
image: ghcr.io/runcitadel/dashboard:v0.0.15@sha256:a2cf5ad79367fb083db0f61e5a296aafee655c99af0c228680644c248ec674a5
|
image: ghcr.io/runcitadel/dashboard:main@sha256:25b6fb413c10f47e186309c8737926c241c0f2bec923b2c08dd837b828f14dbd
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
stop_grace_period: 1m30s
|
stop_grace_period: 1m30s
|
||||||
networks:
|
networks:
|
||||||
|
@ -108,7 +108,7 @@ services:
|
||||||
ipv4_address: $DASHBOARD_IP
|
ipv4_address: $DASHBOARD_IP
|
||||||
manager:
|
manager:
|
||||||
container_name: manager
|
container_name: manager
|
||||||
image: ghcr.io/runcitadel/manager:v0.0.15@sha256:9fb5a86d9e40a04f93d5b6110d43a0f9a5c4ad6311a843b5442290013196a5ce
|
image: ghcr.io/runcitadel/manager:main@sha256:db5775e986d53e762e43331540bb1c05a27b362da94d587c4a4591c981c00ee4
|
||||||
depends_on:
|
depends_on:
|
||||||
- tor
|
- tor
|
||||||
- redis
|
- redis
|
||||||
|
@ -162,7 +162,7 @@ services:
|
||||||
ipv4_address: $MANAGER_IP
|
ipv4_address: $MANAGER_IP
|
||||||
middleware:
|
middleware:
|
||||||
container_name: middleware
|
container_name: middleware
|
||||||
image: ghcr.io/runcitadel/middleware:v0.0.11@sha256:e472da8cbfa67d9a9dbf321334fe65cdf20a0f9b6d6bab33fdf07210f54e7002
|
image: ghcr.io/runcitadel/middleware:main@sha256:2fbbfb2e818bf0462f74a6aaab192881615ae018e6dcb62a50d05f82ec622cb0
|
||||||
depends_on:
|
depends_on:
|
||||||
- manager
|
- manager
|
||||||
- bitcoin
|
- bitcoin
|
||||||
|
@ -223,6 +223,7 @@ services:
|
||||||
ipv4_address: $ELECTRUM_IP
|
ipv4_address: $ELECTRUM_IP
|
||||||
redis:
|
redis:
|
||||||
container_name: redis
|
container_name: redis
|
||||||
|
user: 1000:1000
|
||||||
image: redis:7.0.0-bullseye@sha256:ad0705f2e2344c4b642449e658ef4669753d6eb70228d46267685045bf932303
|
image: redis:7.0.0-bullseye@sha256:ad0705f2e2344c4b642449e658ef4669753d6eb70228d46267685045bf932303
|
||||||
working_dir: /data
|
working_dir: /data
|
||||||
volumes:
|
volumes:
|
||||||
|
|
25
events/triggers/quick-update
Executable file
25
events/triggers/quick-update
Executable file
|
@ -0,0 +1,25 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# SPDX-FileCopyrightText: 2021-2022 Citadel and contributors
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
CITADEL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../..)"
|
||||||
|
|
||||||
|
RELEASE=$(cat "$CITADEL_ROOT"/statuses/update-status.json | jq .updateTo -r)
|
||||||
|
|
||||||
|
cat <<EOF > "$CITADEL_ROOT"/statuses/update-status.json
|
||||||
|
{"state": "installing", "progress": 30, "description": "Starting update", "updateTo": "$RELEASE"}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
curl "https://raw.githubusercontent.com/runcitadel/core/${RELEASE}/db/dependencies.yml" > "$CITADEL_ROOT"/db/dependencies
|
||||||
|
cat <<EOF > "$CITADEL_ROOT"/statuses/update-status.json
|
||||||
|
{"state": "installing", "progress": 70, "description": "Starting new containers", "updateTo": "$RELEASE"}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
"${CITADEL_ROOT}/scripts/start"
|
||||||
|
|
||||||
|
cat <<EOF > "$CITADEL_ROOT"/statuses/update-status.json
|
||||||
|
{"state": "success", "progress": 100, "description": "Successfully installed Citadel $RELEASE", "updateTo": ""}
|
||||||
|
EOF
|
||||||
|
|
|
@ -7,3 +7,4 @@
|
||||||
CITADEL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../..)"
|
CITADEL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../..)"
|
||||||
|
|
||||||
"${CITADEL_ROOT}/scripts/set-update-channel" "${1}"
|
"${CITADEL_ROOT}/scripts/set-update-channel" "${1}"
|
||||||
|
"${CITADEL_ROOT}/scripts/start"
|
||||||
|
|
|
@ -2,5 +2,6 @@
|
||||||
"version": "0.0.7",
|
"version": "0.0.7",
|
||||||
"name": "Citadel 0.0.7",
|
"name": "Citadel 0.0.7",
|
||||||
"requires": ">=0.0.1",
|
"requires": ">=0.0.1",
|
||||||
|
"isQuickUpdate": false,
|
||||||
"notes": "While we are busy with the next huge update, you may need to wait longer for updates. This update updates Bitcoin Knots and LND to their latest versions to ensure apps can utilize their latest features. In addition, this update includes the Citadel CLI. More information on that will be published soon."
|
"notes": "While we are busy with the next huge update, you may need to wait longer for updates. This update updates Bitcoin Knots and LND to their latest versions to ensure apps can utilize their latest features. In addition, this update includes the Citadel CLI. More information on that will be published soon."
|
||||||
}
|
}
|
||||||
|
|
39
scripts/configure
vendored
39
scripts/configure
vendored
|
@ -31,13 +31,14 @@ if not is_arm64 and not is_amd64:
|
||||||
print('Citadel only works on arm64 and amd64!')
|
print('Citadel only works on arm64 and amd64!')
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
dependencies = False
|
||||||
|
|
||||||
# Check the output of "docker compose version", if it matches "Docker Compose version v2.0.0-rc.3", return true
|
# Check the output of "docker compose version", if it matches "Docker Compose version v2.0.0-rc.3", return true
|
||||||
# Otherwise, return false
|
# Otherwise, return false
|
||||||
def is_compose_rc_or_outdated():
|
def is_compose_version_except(target_version):
|
||||||
try:
|
try:
|
||||||
output = subprocess.check_output(['docker', 'compose', 'version'])
|
output = subprocess.check_output(['docker', 'compose', 'version'])
|
||||||
if output.decode('utf-8').strip() != 'Docker Compose version v2.3.3':
|
if output.decode('utf-8').strip() != 'Docker Compose version {}'.format(target_version):
|
||||||
print("Using outdated Docker Compose, updating...")
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
@ -48,17 +49,19 @@ def is_compose_rc_or_outdated():
|
||||||
def download_docker_compose():
|
def download_docker_compose():
|
||||||
# Skip if os.path.expanduser('~/.docker/cli-plugins/docker-compose') exists
|
# Skip if os.path.expanduser('~/.docker/cli-plugins/docker-compose') exists
|
||||||
subprocess.check_call(["mkdir", "-p", os.path.expanduser('~/.docker/cli-plugins/')])
|
subprocess.check_call(["mkdir", "-p", os.path.expanduser('~/.docker/cli-plugins/')])
|
||||||
if (os.path.exists(os.path.expanduser('~/.docker/cli-plugins/docker-compose')) or os.path.exists('/usr/lib/docker/cli-plugins/docker-compose')) and not is_compose_rc_or_outdated():
|
|
||||||
print("Found {}\n".format(subprocess.check_output(['docker', 'compose', 'version']).decode('utf-8').strip()))
|
|
||||||
return
|
|
||||||
|
|
||||||
print("Installing Docker Compose...\n")
|
|
||||||
|
|
||||||
if is_arm64:
|
if is_arm64:
|
||||||
subprocess.check_call(['wget', 'https://github.com/docker/compose/releases/download/v2.3.3/docker-compose-linux-aarch64', '-O', os.path.expanduser('~/.docker/cli-plugins/docker-compose')])
|
compose_arch = 'aarch64'
|
||||||
elif is_amd64:
|
elif is_amd64:
|
||||||
subprocess.check_call(['wget', 'https://github.com/docker/compose/releases/download/v2.3.3/docker-compose-linux-x86_64', '-O', os.path.expanduser('~/.docker/cli-plugins/docker-compose')])
|
compose_arch = 'x86_64'
|
||||||
os.chmod(os.path.expanduser('~/.docker/cli-plugins/docker-compose'), 0o755)
|
# We validate that no other case than the two above can happen before
|
||||||
|
|
||||||
|
if is_compose_version_except(dependencies['compose']):
|
||||||
|
print("Docker compose not found or not required version, updating.")
|
||||||
|
compose_url = 'https://github.com/docker/compose/releases/download/{}/docker-compose-linux-{}'.format(dependencies['compose'], compose_arch)
|
||||||
|
compose_file = os.path.expanduser('~/.docker/cli-plugins/docker-compose')
|
||||||
|
subprocess.check_call(['wget', compose_url, '-O', compose_file])
|
||||||
|
os.chmod(compose_file, 0o755)
|
||||||
|
|
||||||
|
|
||||||
if not shutil.which("wget"):
|
if not shutil.which("wget"):
|
||||||
print('Wget is not installed!')
|
print('Wget is not installed!')
|
||||||
|
@ -72,6 +75,9 @@ if not shutil.which("docker"):
|
||||||
CITADEL_ROOT=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
CITADEL_ROOT=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
os.chdir(CITADEL_ROOT)
|
os.chdir(CITADEL_ROOT)
|
||||||
|
|
||||||
|
with open("./db/dependencies.yml", "r") as file:
|
||||||
|
dependencies = yaml.safe_load(file)
|
||||||
|
|
||||||
updating = False
|
updating = False
|
||||||
status_dir = os.path.join(CITADEL_ROOT, 'statuses')
|
status_dir = os.path.join(CITADEL_ROOT, 'statuses')
|
||||||
# Make sure to use the main status dir for updates
|
# Make sure to use the main status dir for updates
|
||||||
|
@ -365,6 +371,15 @@ print("Generated configuration files\n")
|
||||||
print("Checking if Docker Compose is installed...")
|
print("Checking if Docker Compose is installed...")
|
||||||
download_docker_compose()
|
download_docker_compose()
|
||||||
|
|
||||||
|
print("Updating core services...")
|
||||||
|
print()
|
||||||
|
with open("docker-compose.yml", 'r') as stream:
|
||||||
|
compose = yaml.safe_load(stream)
|
||||||
|
for service in ["manager", "middleware", "dashboard"]:
|
||||||
|
compose["services"][service]["image"] = dependencies[service]
|
||||||
|
with open("docker-compose.yml", "w") as stream:
|
||||||
|
yaml.dump(compose, stream, sort_keys=False)
|
||||||
|
|
||||||
if not reconfiguring:
|
if not reconfiguring:
|
||||||
print("Updating apps...\n")
|
print("Updating apps...\n")
|
||||||
os.system('./scripts/app --invoked-by-configure update')
|
os.system('./scripts/app --invoked-by-configure update')
|
||||||
|
|
|
@ -10,7 +10,7 @@ NODE_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/..)"
|
||||||
# If $1 is not given, fail
|
# If $1 is not given, fail
|
||||||
if [ -z "$1" ]; then
|
if [ -z "$1" ]; then
|
||||||
echo "Usage: $0 <channel>"
|
echo "Usage: $0 <channel>"
|
||||||
echo "Channel can currently either be 'stable' or 'beta'"
|
echo "Channel can currently either be 'stable', 'beta' or 'c-lightning'"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
sed -i "s/UPDATE_CHANNEL=.*/UPDATE_CHANNEL=${1}/" "${NODE_ROOT}/.env"
|
sed -i "s/UPDATE_CHANNEL=.*/UPDATE_CHANNEL=${1}/" "${NODE_ROOT}/.env"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user