Enable deleting apps by using CLI
This commit is contained in:
parent
5893ee39a3
commit
caa9b2e79b
4 changed files with 95 additions and 33 deletions
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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}"
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in a new issue