Process feedback; make it possible to install monitoring
This commit is contained in:
parent
caa9b2e79b
commit
8e41705d39
4 changed files with 84 additions and 59 deletions
|
@ -24,13 +24,6 @@ APPS_DATA = [
|
||||||
|
|
||||||
APP_DATA = {"id": 1, "name": "Nextcloud", "selected": True, "status": "ON for everyone", "config": CONFIG_DATA},
|
APP_DATA = {"id": 1, "name": "Nextcloud", "selected": True, "status": "ON for everyone", "config": CONFIG_DATA},
|
||||||
|
|
||||||
# Apps that should not get oauth variables when they are installed
|
|
||||||
APPS_WITHOUT_OAUTH = [
|
|
||||||
"single-sign-on",
|
|
||||||
"prometheus",
|
|
||||||
"alertmanager",
|
|
||||||
]
|
|
||||||
|
|
||||||
APP_NOT_INSTALLED_STATUS = "Not installed"
|
APP_NOT_INSTALLED_STATUS = "Not installed"
|
||||||
|
|
||||||
@api_v1.route('/apps', methods=['GET'])
|
@api_v1.route('/apps', methods=['GET'])
|
||||||
|
|
|
@ -6,7 +6,7 @@ from sqlalchemy import ForeignKey, Integer, String
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from database import db
|
from database import db
|
||||||
import helpers.kubernetes as k8s
|
import helpers.kubernetes as k8s
|
||||||
from .apps import APPS_WITHOUT_OAUTH, APP_NOT_INSTALLED_STATUS
|
from .apps import APP_NOT_INSTALLED_STATUS
|
||||||
|
|
||||||
|
|
||||||
class App(db.Model):
|
class App(db.Model):
|
||||||
|
@ -22,32 +22,21 @@ class App(db.Model):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"{self.id} <{self.name}>"
|
return f"{self.id} <{self.name}>"
|
||||||
|
|
||||||
def get_kustomization_status(self):
|
|
||||||
"""Returns True if the kustomization for this App is ready"""
|
|
||||||
kustomization = k8s.get_kustomization(self.slug)
|
|
||||||
if kustomization is None:
|
|
||||||
return None
|
|
||||||
return kustomization['status']
|
|
||||||
|
|
||||||
def get_helmrelease_status(self):
|
|
||||||
"""Returns True if the kustomization for this App is ready"""
|
|
||||||
helmrelease = k8s.get_helmrelease(self.slug, self.namespace)
|
|
||||||
if helmrelease is None:
|
|
||||||
return None
|
|
||||||
return helmrelease['status']
|
|
||||||
|
|
||||||
def get_status(self):
|
def get_status(self):
|
||||||
"""Returns a string that describes the app state in the cluster"""
|
"""Returns a string that describes the app state in the cluster"""
|
||||||
ks_status = self.get_kustomization_status()
|
kustomization = self.kustomization
|
||||||
if ks_status is not None:
|
if kustomization is not None and "status" in kustomization:
|
||||||
ks_ready, ks_message = App.check_condition(ks_status)
|
ks_ready, ks_message = App.check_condition(kustomization['status'])
|
||||||
else:
|
else:
|
||||||
ks_ready = None
|
ks_ready = None
|
||||||
hr_status = self.get_helmrelease_status()
|
for helmrelease in self.helmreleases['items']:
|
||||||
if hr_status is not None:
|
hr_status = helmrelease['status']
|
||||||
hr_ready, hr_message = App.check_condition(hr_status)
|
hr_ready, hr_message = App.check_condition(hr_status)
|
||||||
else:
|
|
||||||
hr_ready = None
|
# For now, only show the message of the first HR that isn't ready
|
||||||
|
if not hr_ready:
|
||||||
|
break
|
||||||
|
|
||||||
if ks_ready is None:
|
if ks_ready is None:
|
||||||
return APP_NOT_INSTALLED_STATUS
|
return APP_NOT_INSTALLED_STATUS
|
||||||
# *Should* not happen, but just in case:
|
# *Should* not happen, but just in case:
|
||||||
|
@ -71,6 +60,15 @@ class App(db.Model):
|
||||||
# Create add-<app> kustomization
|
# Create add-<app> kustomization
|
||||||
self.__create_kustomization()
|
self.__create_kustomization()
|
||||||
|
|
||||||
|
def uninstall(self):
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
self.__delete_kustomization()
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
"""
|
"""
|
||||||
Fully deletes an application
|
Fully deletes an application
|
||||||
|
@ -80,34 +78,16 @@ class App(db.Model):
|
||||||
"""
|
"""
|
||||||
# Delete all roles first
|
# Delete all roles first
|
||||||
for role in self.roles:
|
for role in self.roles:
|
||||||
db.session.delete(role)
|
role.delete()
|
||||||
|
|
||||||
# Delete the kustomization
|
|
||||||
if self.__delete_kustomization():
|
|
||||||
|
|
||||||
# TODO: This is where we might want to poll for status changes in the
|
|
||||||
# app, so that only once the kustomization and all its stuff (other ks,
|
|
||||||
# helmrelease, etc.) is deleted, we continue
|
|
||||||
|
|
||||||
# If the kustomization delete went well, commit DB changes.
|
|
||||||
db.session.commit()
|
|
||||||
# Then delete the app
|
|
||||||
db.session.delete(self)
|
db.session.delete(self)
|
||||||
db.session.commit()
|
return db.session.commit()
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __generate_secrets(self):
|
def __generate_secrets(self):
|
||||||
"""Generates passwords for app installation"""
|
"""Generates passwords for app installation"""
|
||||||
# Create app variables secret
|
# Create app variables secret
|
||||||
if self.variables_template_filepath:
|
if self.variables_template_filepath:
|
||||||
k8s.create_variables_secret(self.slug, self.variables_template_filepath)
|
k8s.create_variables_secret(self.slug, self.variables_template_filepath)
|
||||||
# Create a secret that contains the oauth variables for Hydra Maester
|
|
||||||
if self.slug not in APPS_WITHOUT_OAUTH:
|
|
||||||
k8s.create_variables_secret(
|
|
||||||
self.slug,
|
|
||||||
os.path.join(self.__get_templates_dir(),
|
|
||||||
"stackspin-oauth-variables.yaml.jinja"))
|
|
||||||
|
|
||||||
def __create_kustomization(self):
|
def __create_kustomization(self):
|
||||||
"""Creates the `add-{app_slug}` kustomization in the Kubernetes cluster"""
|
"""Creates the `add-{app_slug}` kustomization in the Kubernetes cluster"""
|
||||||
|
@ -150,6 +130,18 @@ class App(db.Model):
|
||||||
app_id=self.id
|
app_id=self.id
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def kustomization(self):
|
||||||
|
"""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
|
@staticmethod
|
||||||
def __get_templates_dir():
|
def __get_templates_dir():
|
||||||
"""Returns directory that contains the Jinja templates used to create app secrets."""
|
"""Returns directory that contains the Jinja templates used to create app secrets."""
|
||||||
|
@ -161,7 +153,7 @@ class App(db.Model):
|
||||||
Returns a tuple that has true/false for readiness and a message
|
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
|
Ready, in this case means that the condition's type == "Ready" and its
|
||||||
status == "True". If the condition type "Ready" does not exist, the
|
status == "True". If the condition type "Ready" does not occur, the
|
||||||
status is interpreted as not ready.
|
status is interpreted as not ready.
|
||||||
|
|
||||||
The message that is returned is the message that comes with the
|
The message that is returned is the message that comes with the
|
||||||
|
@ -173,7 +165,7 @@ class App(db.Model):
|
||||||
for condition in status["conditions"]:
|
for condition in status["conditions"]:
|
||||||
if condition["type"] == "Ready":
|
if condition["type"] == "Ready":
|
||||||
return condition["status"] == "True", condition["message"]
|
return condition["status"] == "True", condition["message"]
|
||||||
return False
|
return False, "Condition with type 'Ready' not found"
|
||||||
|
|
||||||
|
|
||||||
class AppRole(db.Model): # pylint: disable=too-few-public-methods
|
class AppRole(db.Model): # pylint: disable=too-few-public-methods
|
||||||
|
|
|
@ -13,7 +13,7 @@ from flask.cli import AppGroup
|
||||||
from ory_kratos_client.api import v0alpha2_api as kratos_api
|
from ory_kratos_client.api import v0alpha2_api as kratos_api
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
|
|
||||||
from config import HYDRA_ADMIN_URL,KRATOS_ADMIN_URL,KRATOS_PUBLIC_URL
|
from config import HYDRA_ADMIN_URL, KRATOS_ADMIN_URL, KRATOS_PUBLIC_URL
|
||||||
from helpers import KratosUser
|
from helpers import KratosUser
|
||||||
from cliapp import cli
|
from cliapp import cli
|
||||||
from areas.roles import Role
|
from areas.roles import Role
|
||||||
|
@ -88,7 +88,7 @@ def list_app():
|
||||||
)
|
)
|
||||||
@click.argument("slug")
|
@click.argument("slug")
|
||||||
def delete_app(slug):
|
def delete_app(slug):
|
||||||
"""Removes app from database as well as uninstalls it from the cluster
|
"""Removes app from database
|
||||||
:param slug: str Slug of app to remove
|
:param slug: str Slug of app to remove
|
||||||
"""
|
"""
|
||||||
current_app.logger.info(f"Trying to delete app: {slug}")
|
current_app.logger.info(f"Trying to delete app: {slug}")
|
||||||
|
@ -102,6 +102,25 @@ def delete_app(slug):
|
||||||
current_app.logger.info(f"Success: {deleted}")
|
current_app.logger.info(f"Success: {deleted}")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@app_cli.command(
|
||||||
|
"uninstall",
|
||||||
|
)
|
||||||
|
@click.argument("slug")
|
||||||
|
def uninstall_app(slug):
|
||||||
|
"""Uninstalls the app from the cluster
|
||||||
|
:param slug: str Slug of app to remove
|
||||||
|
"""
|
||||||
|
current_app.logger.info(f"Trying to delete app: {slug}")
|
||||||
|
app_obj = App.query.filter_by(slug=slug).first()
|
||||||
|
|
||||||
|
if not app_obj:
|
||||||
|
current_app.logger.info("Not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
uninstalled = app_obj.uninstall()
|
||||||
|
current_app.logger.info(f"Success: {uninstalled}")
|
||||||
|
return
|
||||||
|
|
||||||
@app_cli.command("status")
|
@app_cli.command("status")
|
||||||
@click.argument("slug")
|
@click.argument("slug")
|
||||||
def status_app(slug):
|
def status_app(slug):
|
||||||
|
@ -116,7 +135,7 @@ def status_app(slug):
|
||||||
current_app.logger.error(f"App {slug} does not exist")
|
current_app.logger.error(f"App {slug} does not exist")
|
||||||
return
|
return
|
||||||
|
|
||||||
current_app.logger.info("Status: " + str(app.get_status()))
|
current_app.logger.info(f"Status: {app.get_status()}")
|
||||||
|
|
||||||
@app_cli.command("install")
|
@app_cli.command("install")
|
||||||
@click.argument("slug")
|
@click.argument("slug")
|
||||||
|
|
|
@ -110,8 +110,7 @@ def store_kustomization(kustomization_template_filepath, app_slug):
|
||||||
"""Add a kustomization that installs app {app_slug} to the cluster"""
|
"""Add a kustomization that installs app {app_slug} to the cluster"""
|
||||||
kustomization_dict = read_template_to_dict(kustomization_template_filepath,
|
kustomization_dict = read_template_to_dict(kustomization_template_filepath,
|
||||||
{"app": app_slug})
|
{"app": app_slug})
|
||||||
api_client_instance = api_client.ApiClient()
|
custom_objects_api = client.CustomObjectsApi()
|
||||||
custom_objects_api = client.CustomObjectsApi(api_client_instance)
|
|
||||||
try:
|
try:
|
||||||
api_response = custom_objects_api.create_namespaced_custom_object(
|
api_response = custom_objects_api.create_namespaced_custom_object(
|
||||||
group="kustomize.toolkit.fluxcd.io",
|
group="kustomize.toolkit.fluxcd.io",
|
||||||
|
@ -128,8 +127,7 @@ def delete_kustomization(kustomization_name):
|
||||||
"""Deletes kustomization for an app_slug. Should also result in the
|
"""Deletes kustomization for an app_slug. Should also result in the
|
||||||
deletion of the app's HelmReleases, PVCs, OAuth2Client, etc. Nothing will
|
deletion of the app's HelmReleases, PVCs, OAuth2Client, etc. Nothing will
|
||||||
remain"""
|
remain"""
|
||||||
api_client_instance = api_client.ApiClient()
|
custom_objects_api = client.CustomObjectsApi()
|
||||||
custom_objects_api = client.CustomObjectsApi(api_client_instance)
|
|
||||||
body = client.V1DeleteOptions()
|
body = client.V1DeleteOptions()
|
||||||
try:
|
try:
|
||||||
api_response = custom_objects_api.delete_namespaced_custom_object(
|
api_response = custom_objects_api.delete_namespaced_custom_object(
|
||||||
|
@ -143,6 +141,7 @@ def delete_kustomization(kustomization_name):
|
||||||
print(f"Could not delete {kustomization_name} Kustomization because of exception {ex}")
|
print(f"Could not delete {kustomization_name} Kustomization because of exception {ex}")
|
||||||
return False
|
return False
|
||||||
print(f"Kustomization deleted with api response: {api_response}")
|
print(f"Kustomization deleted with api response: {api_response}")
|
||||||
|
return api_response
|
||||||
|
|
||||||
|
|
||||||
def read_template_to_dict(template_filepath, template_globals):
|
def read_template_to_dict(template_filepath, template_globals):
|
||||||
|
@ -292,6 +291,28 @@ def get_helmrelease(name, namespace='stackspin-apps'):
|
||||||
|
|
||||||
return resource
|
return resource
|
||||||
|
|
||||||
|
def list_helmreleases(namespace='stackspin-apps', label_selector=""):
|
||||||
|
"""
|
||||||
|
Lists all helmreleases in a certain namespace (stackspin-apps by default)
|
||||||
|
|
||||||
|
Optionally takes a label selector to limit the list.
|
||||||
|
"""
|
||||||
|
api_instance = client.CustomObjectsApi()
|
||||||
|
|
||||||
|
try:
|
||||||
|
api_response = api_instance.list_namespaced_custom_object(
|
||||||
|
group="helm.toolkit.fluxcd.io",
|
||||||
|
version="v2beta1",
|
||||||
|
namespace=namespace,
|
||||||
|
plural="helmreleases",
|
||||||
|
label_selector=label_selector)
|
||||||
|
except ApiException as error:
|
||||||
|
if error.status == 404:
|
||||||
|
return None
|
||||||
|
# Raise all non-404 errors
|
||||||
|
raise error
|
||||||
|
return api_response
|
||||||
|
|
||||||
|
|
||||||
def get_readiness(app_status):
|
def get_readiness(app_status):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in a new issue