draft of installing apps and getting app status

This commit is contained in:
Maarten de Waard 2022-08-12 13:08:03 +02:00
parent a22cd87221
commit 5893ee39a3
No known key found for this signature in database
GPG key ID: 1D3E893A657CC8DA
13 changed files with 1148 additions and 7 deletions

View file

@ -24,6 +24,14 @@ APPS_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"
@api_v1.route('/apps', methods=['GET'])
@jwt_required()

View file

@ -1,6 +1,12 @@
"""Everything to do with Apps"""
import os
from sqlalchemy import ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from database import db
import helpers.kubernetes as k8s
from .apps import APPS_WITHOUT_OAUTH, APP_NOT_INSTALLED_STATUS
class App(db.Model):
@ -16,8 +22,122 @@ class App(db.Model):
def __repr__(self):
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']
class AppRole(db.Model):
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):
"""Returns a string that describes the app state in the cluster"""
ks_status = self.get_kustomization_status()
if ks_status is not None:
ks_ready, ks_message = App.check_condition(ks_status)
else:
ks_ready = None
hr_status = self.get_helmrelease_status()
if hr_status is not None:
hr_ready, hr_message = App.check_condition(hr_status)
else:
hr_ready = None
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 failed installing: {hr_message}"
if not ks_ready:
return f"App failed installing: {ks_message}"
return "App is installing..."
def install(self):
"""Creates a Kustomization in the Kubernetes cluster that installs this application"""
# Generate the necessary passwords, etc. from a template
self.__generate_secrets()
# Create add-<app> kustomization
self.__create_kustomization()
def __generate_secrets(self):
"""Generates passwords for app installation"""
# Create app variables secret
if 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):
"""Creates the `add-{app_slug}` kustomization in the Kubernetes cluster"""
kustomization_template_filepath = \
os.path.join(self.__get_templates_dir(),
"add-app-kustomization.yaml.jinja")
k8s.store_kustomization(kustomization_template_filepath, 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")
@property
def variables_template_filepath(self):
"""Path to the variables template used to generate secrets the app needs"""
variables_template_filepath = os.path.join(self.__get_templates_dir(),
f"stackspin-{self.slug}-variables.yaml.jinja")
if os.path.exists(variables_template_filepath):
return variables_template_filepath
return None
@property
def namespace(self):
"""
Returns the Kubernetes namespace of this app
FIXME: This should probably become a database field.
"""
if self.slug in ['nextcloud', 'wordpress', 'wekan', 'zulip']:
return 'stackspin-apps'
return 'stackspin'
@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 exist, 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
class AppRole(db.Model): # pylint: disable=too-few-public-methods
"""
The AppRole object, stores the roles Users have on Apps
"""
@ -29,4 +149,5 @@ class AppRole(db.Model):
role = relationship("Role")
def __repr__(self):
return f"role_id: {self.role_id}, user_id: {self.user_id}, app_id: {self.app_id}, role: {self.role}"
return (f"role_id: {self.role_id}, user_id: {self.user_id},"
f" app_id: {self.app_id}, role: {self.role}")

View file

@ -0,0 +1,14 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
name: add-{{ app }}
namespace: flux-system
spec:
interval: 1h0m0s
path: ./flux2/cluster/optional/{{ app }}
prune: true
sourceRef:
kind: GitRepository
name: stackspin

View file

@ -0,0 +1,12 @@
---
apiVersion: v1
kind: Secret
metadata:
name: stackspin-nextcloud-variables
data:
nextcloud_password: "{{ 32 | generate_password | b64encode }}"
nextcloud_mariadb_password: "{{ 32 | generate_password | b64encode }}"
nextcloud_mariadb_root_password: "{{ 32 | generate_password | b64encode }}"
onlyoffice_database_password: "{{ 32 | generate_password | b64encode }}"
onlyoffice_jwt_secret: "{{ 32 | generate_password | b64encode }}"
onlyoffice_rabbitmq_password: "{{ 32 | generate_password | b64encode }}"

View file

@ -0,0 +1,8 @@
---
apiVersion: v1
kind: Secret
metadata:
name: stackspin-{{ app }}-oauth-variables
data:
client_id: "{{ app | b64encode }}"
client_secret: "{{ 32 | generate_password | b64encode }}"

View file

@ -0,0 +1,7 @@
apiVersion: v1
kind: Secret
metadata:
name: stackspin-wekan-variables
data:
mongodb_password: "{{ 32 | generate_password | b64encode }}"
mongodb_root_password: "{{ 32 | generate_password | b64encode }}"

View file

@ -0,0 +1,9 @@
---
apiVersion: v1
kind: Secret
metadata:
name: stackspin-wordpress-variables
data:
wordpress_admin_password: "{{ 32 | generate_password | b64encode }}"
wordpress_mariadb_password: "{{ 32 | generate_password | b64encode }}"
wordpress_mariadb_root_password: "{{ 32 | generate_password | b64encode }}"

View file

@ -0,0 +1,12 @@
apiVersion: v1
kind: Secret
metadata:
name: stackspin-zulip-variables
data:
admin_password: "{{ 32 | generate_password | b64encode }}"
memcached_password: "{{ 32 | generate_password | b64encode }}"
rabbitmq_password: "{{ 32 | generate_password | b64encode }}"
rabbitmq_erlang_cookie: "{{ 32 | generate_password | b64encode }}"
redis_password: "{{ 32 | generate_password | b64encode }}"
postgresql_password: "{{ 32 | generate_password | b64encode }}"
zulip_password: "{{ 32 | generate_password | b64encode }}"