Enable deleting apps by using CLI

This commit is contained in:
Maarten de Waard 2022-09-22 16:14:49 +02:00
parent cea3f5a3cb
commit 6529636b3d
No known key found for this signature in database
GPG key ID: 1D3E893A657CC8DA
4 changed files with 95 additions and 33 deletions

View file

@ -58,9 +58,9 @@ class App(db.Model):
if ks_ready and hr_ready: if ks_ready and hr_ready:
return "App installed and running" return "App installed and running"
if not hr_ready: if not hr_ready:
return f"App failed installing: {hr_message}" return f"App HelmRelease status: {hr_message}"
if not ks_ready: if not ks_ready:
return f"App failed installing: {ks_message}" return f"App Kustomization status: {ks_message}"
return "App is installing..." return "App is installing..."
@ -71,6 +71,32 @@ class App(db.Model):
# Create add-<app> kustomization # Create add-<app> kustomization
self.__create_kustomization() self.__create_kustomization()
def delete(self):
"""
Fully deletes an application
This includes user roles, all kubernetes objects and also PVCs, so your
data will be *gone*
"""
# Delete all roles first
for role in self.roles:
db.session.delete(role)
# 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.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
@ -90,11 +116,10 @@ class App(db.Model):
"add-app-kustomization.yaml.jinja") "add-app-kustomization.yaml.jinja")
k8s.store_kustomization(kustomization_template_filepath, self.slug) k8s.store_kustomization(kustomization_template_filepath, self.slug)
def __delete_kustomization(self):
"""Deletes kustomization for this app"""
k8s.delete_kustomization(f"add-{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 @property
def variables_template_filepath(self): def variables_template_filepath(self):
@ -116,6 +141,20 @@ class App(db.Model):
return 'stackspin-apps' return 'stackspin-apps'
return 'stackspin' return 'stackspin'
@property
def roles(self):
"""
All roles that are linked to this app
"""
return AppRole.query.filter_by(
app_id=self.id
).all()
@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 @staticmethod
def check_condition(status): def check_condition(status):
""" """

View file

@ -88,27 +88,23 @@ def list_app():
) )
@click.argument("slug") @click.argument("slug")
def delete_app(slug): def delete_app(slug):
"""Removes app from database """Removes app from database as well as uninstalls it from the cluster
: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}")
obj = App.query.filter_by(slug=slug).first() app_obj = App.query.filter_by(slug=slug).first()
if not obj: if not app_obj:
current_app.logger.info("Not found") current_app.logger.info("Not found")
return return
# Deleting will (probably) fail if there are still roles attached. This is a deleted = app_obj.delete()
# PoC implementation only. Actually management of apps and roles will be current_app.logger.info(f"Success: {deleted}")
# done by the backend application
db.session.delete(obj)
db.session.commit()
current_app.logger.info("Success")
return return
@app_cli.command("get_status") @app_cli.command("status")
@click.argument("slug") @click.argument("slug")
def get_status_app(slug): def status_app(slug):
"""Gets the current app status from the Kubernetes cluster """Gets the current app status from the Kubernetes cluster
:param slug: str Slug of app to remove :param slug: str Slug of app to remove
""" """
@ -125,8 +121,8 @@ def get_status_app(slug):
@app_cli.command("install") @app_cli.command("install")
@click.argument("slug") @click.argument("slug")
def install_app(slug): def install_app(slug):
"""Gets the current app status from the Kubernetes cluster """Installs app into Kubernetes cluster
:param slug: str Slug of app to remove :param slug: str Slug of app to install
""" """
current_app.logger.info(f"Installing app: {slug}") current_app.logger.info(f"Installing app: {slug}")
@ -140,11 +136,28 @@ def install_app(slug):
if current_status == APP_NOT_INSTALLED_STATUS: if current_status == APP_NOT_INSTALLED_STATUS:
app.install() app.install()
current_app.logger.info( current_app.logger.info(
f"App {slug} installing... use `get_status` to see status") f"App {slug} installing... use `status` to see status")
else: else:
current_app.logger.error("App {slug} should have status" current_app.logger.error("App {slug} should have status"
f" {APP_NOT_INSTALLED_STATUS} but has status: {current_status}") f" {APP_NOT_INSTALLED_STATUS} but has status: {current_status}")
@app_cli.command("roles")
@click.argument("slug")
def roles_app(slug):
"""Gets a list of roles for this app
:param slug: str Slug of app queried
"""
current_app.logger.info(f"Getting roles for app: {slug}")
app = App.query.filter_by(slug=slug).first()
if not app:
current_app.logger.error(f"App {slug} does not exist")
return
current_app.logger.info("Roles: ")
for role in app.roles:
current_app.logger.info(role)
cli.cli.add_command(app_cli) cli.cli.add_command(app_cli)

View file

@ -31,7 +31,7 @@ services:
# ENV variables that are deployment-specific # ENV variables that are deployment-specific
- SECRET_KEY=$FLASK_SECRET_KEY - SECRET_KEY=$FLASK_SECRET_KEY
- HYDRA_CLIENT_SECRET=$HYDRA_CLIENT_SECRET - HYDRA_CLIENT_SECRET=$HYDRA_CLIENT_SECRET
# - OAUTHLIB_INSECURE_TRANSPORT=1 - KUBECONFIG=/.kube/config
ports: ports:
- "5000:5000" - "5000:5000"
user: "${KUBECTL_UID}:${KUBECTL_GID}" user: "${KUBECTL_UID}:${KUBECTL_GID}"

View file

@ -13,6 +13,8 @@ from kubernetes.client.exceptions import ApiException
from kubernetes.utils import create_from_yaml from kubernetes.utils import create_from_yaml
from kubernetes.utils.create_from_yaml import FailToCreateError from kubernetes.utils.create_from_yaml import FailToCreateError
# Load the kube config once
config.load_kube_config()
def create_variables_secret(app_slug, variables_filepath): def create_variables_secret(app_slug, variables_filepath):
"""Checks if a variables secret for app_name already exists, generates it if necessary. """Checks if a variables secret for app_name already exists, generates it if necessary.
@ -117,19 +119,31 @@ def store_kustomization(kustomization_template_filepath, app_slug):
namespace="flux-system", namespace="flux-system",
plural="kustomizations", plural="kustomizations",
body=kustomization_dict) body=kustomization_dict)
# create_from_yaml(
# api_client_instance,
# yaml_objects=[kustomization_dict],
# # All kustomizations live in the flux-system namespace
# namespace="flux-system"
# )
except FailToCreateError as ex: except FailToCreateError as ex:
print(f"Could not create {app_slug} Kustomization because of exception {ex}") print(f"Could not create {app_slug} Kustomization because of exception {ex}")
return return
print(f"Kustomization created with api response: {api_response}") print(f"Kustomization created with api response: {api_response}")
def delete_kustomization(kustomization_name):
"""Deletes kustomization for an app_slug. Should also result in the
deletion of the app's HelmReleases, PVCs, OAuth2Client, etc. Nothing will
remain"""
api_client_instance = api_client.ApiClient()
custom_objects_api = client.CustomObjectsApi(api_client_instance)
body = client.V1DeleteOptions()
try:
api_response = custom_objects_api.delete_namespaced_custom_object(
group="kustomize.toolkit.fluxcd.io",
version="v1beta2",
namespace="flux-system",
plural="kustomizations",
name=kustomization_name,
body=body)
except ApiException as ex:
print(f"Could not delete {kustomization_name} Kustomization because of exception {ex}")
return False
print(f"Kustomization deleted with api response: {api_response}")
def read_template_to_dict(template_filepath, template_globals): def read_template_to_dict(template_filepath, template_globals):
"""Reads a Jinja2 template that contains yaml and turns it into a dict """Reads a Jinja2 template that contains yaml and turns it into a dict
@ -198,7 +212,6 @@ def get_all_kustomizations(namespace='flux-system'):
:return: Kustomizations as returned by CustomObjectsApi.list_namespaced_custom_object() :return: Kustomizations as returned by CustomObjectsApi.list_namespaced_custom_object()
:rtype: object :rtype: object
""" """
config.load_kube_config()
api = client.CustomObjectsApi() api = client.CustomObjectsApi()
api_response = api.list_namespaced_custom_object( api_response = api.list_namespaced_custom_object(
group="kustomize.toolkit.fluxcd.io", group="kustomize.toolkit.fluxcd.io",
@ -231,7 +244,6 @@ def get_all_helmreleases(namespace='stackspin'):
:return: Helmreleases as returned by CustomObjectsApi.list_namespaced_custom_object() :return: Helmreleases as returned by CustomObjectsApi.list_namespaced_custom_object()
:rtype: object :rtype: object
""" """
config.load_kube_config()
api = client.CustomObjectsApi() api = client.CustomObjectsApi()
api_response = api.list_namespaced_custom_object( api_response = api.list_namespaced_custom_object(
group="helm.toolkit.fluxcd.io", group="helm.toolkit.fluxcd.io",
@ -244,7 +256,6 @@ def get_all_helmreleases(namespace='stackspin'):
def get_kustomization(name, namespace='flux-system'): def get_kustomization(name, namespace='flux-system'):
"""Returns all info of a Flux kustomization with name 'name'""" """Returns all info of a Flux kustomization with name 'name'"""
config.load_kube_config()
api = client.CustomObjectsApi() api = client.CustomObjectsApi()
try: try:
resource = api.get_namespaced_custom_object( resource = api.get_namespaced_custom_object(
@ -264,7 +275,6 @@ def get_kustomization(name, namespace='flux-system'):
def get_helmrelease(name, namespace='stackspin-apps'): def get_helmrelease(name, namespace='stackspin-apps'):
"""Returns all info of a Flux helmrelease with name 'name'""" """Returns all info of a Flux helmrelease with name 'name'"""
config.load_kube_config()
api = client.CustomObjectsApi() api = client.CustomObjectsApi()
try: try:
resource = api.get_namespaced_custom_object( resource = api.get_namespaced_custom_object(