Fix merge conflicts
Merge branch 'main' into 45-allow-database-to-have-external-apps
This commit is contained in:
commit
dd5d4f1acd
5 changed files with 265 additions and 805 deletions
|
|
@ -9,8 +9,6 @@ from database import db
|
|||
|
||||
from areas import api_v1
|
||||
|
||||
from constants import APP_NOT_INSTALLED_STATUS
|
||||
|
||||
CONFIG_DATA = [
|
||||
{
|
||||
"id": "values.yml",
|
||||
|
|
@ -31,8 +29,6 @@ APPS_DATA = [
|
|||
|
||||
APP_DATA = {"id": 1, "name": "Nextcloud", "selected": True, "status": "ON for everyone", "config": CONFIG_DATA},
|
||||
|
||||
|
||||
|
||||
@api_v1.route('/apps', methods=['GET'])
|
||||
@jwt_required()
|
||||
@cross_origin()
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ import helpers.kubernetes as k8s
|
|||
|
||||
from flask import current_app
|
||||
|
||||
from constants import APP_NOT_INSTALLED_STATUS
|
||||
|
||||
DEFAULT_APP_SUBDOMAINS = {
|
||||
"nextcloud": "files",
|
||||
"wordpress": "www",
|
||||
|
|
@ -72,45 +70,9 @@ class App(db.Model):
|
|||
return f"https://{self.slug}.{domain}"
|
||||
return f"https://{ks_config_map[domain_key]}"
|
||||
|
||||
|
||||
def get_status(self):
|
||||
"""Returns a string that describes the app state in the cluster"""
|
||||
|
||||
if self.external:
|
||||
return "External app"
|
||||
|
||||
|
||||
# TODO: Get some kind of caching for those values, as this is called
|
||||
# on every app list, causing significant delays in the interface
|
||||
|
||||
kustomization = self.kustomization
|
||||
if kustomization is not None and "status" in kustomization:
|
||||
ks_ready, ks_message = App.check_condition(kustomization['status'])
|
||||
else:
|
||||
ks_ready = None
|
||||
for helmrelease in self.helmreleases['items']:
|
||||
hr_status = helmrelease['status']
|
||||
hr_ready, hr_message = App.check_condition(hr_status)
|
||||
|
||||
# For now, only show the message of the first HR that isn't ready
|
||||
if not hr_ready:
|
||||
break
|
||||
|
||||
if ks_ready is None:
|
||||
return APP_NOT_INSTALLED_STATUS
|
||||
# *Should* not happen, but just in case:
|
||||
if (ks_ready is None and hr_ready is not None) or \
|
||||
(hr_ready is None and ks_ready is not None):
|
||||
return ("This app is in a strange state. Contact a Stackspin"
|
||||
" administrator if this status stays for longer than 5 minutes")
|
||||
if ks_ready and hr_ready:
|
||||
return "App installed and running"
|
||||
if not hr_ready:
|
||||
return f"App HelmRelease status: {hr_message}"
|
||||
if not ks_ready:
|
||||
return f"App Kustomization status: {ks_message}"
|
||||
return "App is installing..."
|
||||
|
||||
"""Returns an AppStatus object that describes the current cluster state"""
|
||||
return AppStatus(self.kustomization, self.helmreleases)
|
||||
|
||||
def install(self):
|
||||
"""Creates a Kustomization in the Kubernetes cluster that installs this application"""
|
||||
|
|
@ -123,8 +85,11 @@ class App(db.Model):
|
|||
"""
|
||||
Delete the app kustomization.
|
||||
|
||||
This triggers a deletion of the app's PVCs (so deletes all data), as
|
||||
well as any other Kustomizations and HelmReleases related to the app
|
||||
In our case, this triggers a deletion of the app's PVCs (so deletes all
|
||||
data), as well as any other Kustomizations and HelmReleases related to
|
||||
the app. It also triggers a deletion of the OAuth2Client object, but
|
||||
does not delete the secrets generated by the `install` command. It also
|
||||
does not remove the TLS secret generated by cert-manager.
|
||||
"""
|
||||
self.__delete_kustomization()
|
||||
|
||||
|
|
@ -149,6 +114,14 @@ class App(db.Model):
|
|||
if self.variables_template_filepath:
|
||||
k8s.create_variables_secret(self.slug, self.variables_template_filepath)
|
||||
|
||||
k8s.create_variables_secret(
|
||||
self.slug,
|
||||
os.path.join(
|
||||
self.__get_templates_dir(),
|
||||
"stackspin-oauth-variables.yaml.jinja"
|
||||
)
|
||||
)
|
||||
|
||||
def __create_kustomization(self):
|
||||
"""Creates the `add-{app_slug}` kustomization in the Kubernetes cluster"""
|
||||
kustomization_template_filepath = \
|
||||
|
|
@ -195,18 +168,6 @@ class App(db.Model):
|
|||
"""Returns the kustomization object for this app"""
|
||||
return k8s.get_kustomization(self.slug)
|
||||
|
||||
|
||||
@property
|
||||
def helmreleases(self):
|
||||
"""Returns the helmreleases associated with the kustomization for this app"""
|
||||
return k8s.list_helmreleases(self.namespace,
|
||||
f"kustomize.toolkit.fluxcd.io/name={self.slug}")
|
||||
|
||||
@staticmethod
|
||||
def __get_templates_dir():
|
||||
"""Returns directory that contains the Jinja templates used to create app secrets."""
|
||||
return os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates")
|
||||
|
||||
@staticmethod
|
||||
def check_condition(status):
|
||||
"""
|
||||
|
|
@ -240,6 +201,19 @@ class App(db.Model):
|
|||
"status": self.get_status()}
|
||||
|
||||
|
||||
@property
|
||||
def helmreleases(self):
|
||||
"""Returns the helmreleases associated with the kustomization for this app"""
|
||||
return k8s.get_all_helmreleases(self.namespace,
|
||||
f"kustomize.toolkit.fluxcd.io/name={self.slug}")
|
||||
|
||||
@staticmethod
|
||||
def __get_templates_dir():
|
||||
"""Returns directory that contains the Jinja templates used to create app secrets."""
|
||||
return os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates")
|
||||
|
||||
|
||||
|
||||
class AppRole(db.Model): # pylint: disable=too-few-public-methods
|
||||
"""
|
||||
The AppRole object, stores the roles Users have on Apps
|
||||
|
|
@ -254,3 +228,75 @@ class AppRole(db.Model): # pylint: disable=too-few-public-methods
|
|||
def __repr__(self):
|
||||
return (f"role_id: {self.role_id}, user_id: {self.user_id},"
|
||||
f" app_id: {self.app_id}, role: {self.role}")
|
||||
|
||||
class AppStatus(): # pylint: disable=too-few-public-methods
|
||||
"""
|
||||
Represents the status of an app in the Kubernetes cluster.
|
||||
|
||||
This class can answer a few questions, like "is the app installed?", but
|
||||
can also return raw status messages from Kustomizations and HelmReleases
|
||||
|
||||
This constructor sets three variables:
|
||||
|
||||
self.installed (bool): Whether the app should be installed
|
||||
self.ready (bool): Whether the app is installed correctly
|
||||
self.message (str): Information about the status
|
||||
|
||||
:param kustomization_status: The status of the Kustomization of this app:
|
||||
:type kustomization_status: str
|
||||
:param helmrelease_status: The status of the helmreleases of this app
|
||||
:type helmrelease_status: str[]
|
||||
"""
|
||||
def __init__(self, kustomization, helmreleases):
|
||||
self.helmreleases = {}
|
||||
if kustomization is not None and "status" in kustomization:
|
||||
ks_ready, ks_message = AppStatus.check_condition(kustomization['status'])
|
||||
self.installed = True
|
||||
else:
|
||||
ks_ready = None
|
||||
ks_message = "Kustomization does not exist"
|
||||
self.installed = False
|
||||
self.ready = False
|
||||
self.message = "Not installed"
|
||||
return
|
||||
|
||||
for helmrelease in helmreleases:
|
||||
hr_status = helmrelease['status']
|
||||
hr_ready, hr_message = AppStatus.check_condition(hr_status)
|
||||
|
||||
# For now, only show the message of the first HR that isn't ready
|
||||
if not hr_ready:
|
||||
self.ready = False
|
||||
self.message = f"HelmRelease {helmrelease['metadata']['name']} status: {hr_message}"
|
||||
return
|
||||
|
||||
# If we end up here, all HRs are ready
|
||||
if ks_ready:
|
||||
self.ready = True
|
||||
self.message = "Installed"
|
||||
else:
|
||||
self.ready = False
|
||||
self.message = f"App Kustomization status: {ks_message}"
|
||||
|
||||
def __repr__(self):
|
||||
return f"Installed: {self.installed}\tReady: {self.ready}\tMessage: {self.message}"
|
||||
|
||||
@staticmethod
|
||||
def check_condition(status):
|
||||
"""
|
||||
Returns a tuple that has true/false for readiness and a message
|
||||
|
||||
Ready, in this case means that the condition's type == "Ready" and its
|
||||
status == "True". If the condition type "Ready" does not occur, the
|
||||
status is interpreted as not ready.
|
||||
|
||||
The message that is returned is the message that comes with the
|
||||
condition with type "Ready"
|
||||
|
||||
:param status: Kubernetes resource's "status" object.
|
||||
:type status: dict
|
||||
"""
|
||||
for condition in status["conditions"]:
|
||||
if condition["type"] == "Ready":
|
||||
return condition["status"] == "True", condition["message"]
|
||||
return False, "Condition with type 'Ready' not found"
|
||||
|
|
|
|||
Reference in a new issue