diff --git a/areas/apps/models.py b/areas/apps/models.py index 0858592..23a6249 100644 --- a/areas/apps/models.py +++ b/areas/apps/models.py @@ -58,9 +58,9 @@ class App(db.Model): if ks_ready and hr_ready: return "App installed and running" if not hr_ready: - return f"App failed installing: {hr_message}" + return f"App HelmRelease status: {hr_message}" if not ks_ready: - return f"App failed installing: {ks_message}" + return f"App Kustomization status: {ks_message}" return "App is installing..." @@ -71,6 +71,32 @@ class App(db.Model): # Create add- 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): """Generates passwords for app installation""" # Create app variables secret @@ -90,11 +116,10 @@ class App(db.Model): "add-app-kustomization.yaml.jinja") 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 def variables_template_filepath(self): @@ -116,6 +141,20 @@ class App(db.Model): return 'stackspin-apps' 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 def check_condition(status): """ diff --git a/cliapp/cliapp/cli.py b/cliapp/cliapp/cli.py index c467e85..cf6fbc5 100644 --- a/cliapp/cliapp/cli.py +++ b/cliapp/cliapp/cli.py @@ -88,27 +88,23 @@ def list_app(): ) @click.argument("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 """ 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") return - # Deleting will (probably) fail if there are still roles attached. This is a - # PoC implementation only. Actually management of apps and roles will be - # done by the backend application - db.session.delete(obj) - db.session.commit() - current_app.logger.info("Success") + deleted = app_obj.delete() + current_app.logger.info(f"Success: {deleted}") return -@app_cli.command("get_status") +@app_cli.command("status") @click.argument("slug") -def get_status_app(slug): +def status_app(slug): """Gets the current app status from the Kubernetes cluster :param slug: str Slug of app to remove """ @@ -125,8 +121,8 @@ def get_status_app(slug): @app_cli.command("install") @click.argument("slug") def install_app(slug): - """Gets the current app status from the Kubernetes cluster - :param slug: str Slug of app to remove + """Installs app into Kubernetes cluster + :param slug: str Slug of app to install """ current_app.logger.info(f"Installing app: {slug}") @@ -140,11 +136,28 @@ def install_app(slug): if current_status == APP_NOT_INSTALLED_STATUS: app.install() current_app.logger.info( - f"App {slug} installing... use `get_status` to see status") + f"App {slug} installing... use `status` to see status") else: current_app.logger.error("App {slug} should have 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) diff --git a/docker-compose.yml b/docker-compose.yml index 5080cd9..98695cd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,7 +31,7 @@ services: # ENV variables that are deployment-specific - SECRET_KEY=$FLASK_SECRET_KEY - HYDRA_CLIENT_SECRET=$HYDRA_CLIENT_SECRET - # - OAUTHLIB_INSECURE_TRANSPORT=1 + - KUBECONFIG=/.kube/config ports: - "5000:5000" user: "${KUBECTL_UID}:${KUBECTL_GID}" diff --git a/helpers/kubernetes.py b/helpers/kubernetes.py index d8f9b7b..3169c63 100644 --- a/helpers/kubernetes.py +++ b/helpers/kubernetes.py @@ -13,6 +13,8 @@ from kubernetes.client.exceptions import ApiException from kubernetes.utils import create_from_yaml 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): """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", plural="kustomizations", 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: print(f"Could not create {app_slug} Kustomization because of exception {ex}") return 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): """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() :rtype: object """ - config.load_kube_config() api = client.CustomObjectsApi() api_response = api.list_namespaced_custom_object( 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() :rtype: object """ - config.load_kube_config() api = client.CustomObjectsApi() api_response = api.list_namespaced_custom_object( group="helm.toolkit.fluxcd.io", @@ -244,7 +256,6 @@ def get_all_helmreleases(namespace='stackspin'): def get_kustomization(name, namespace='flux-system'): """Returns all info of a Flux kustomization with name 'name'""" - config.load_kube_config() api = client.CustomObjectsApi() try: resource = api.get_namespaced_custom_object( @@ -264,7 +275,6 @@ def get_kustomization(name, namespace='flux-system'): def get_helmrelease(name, namespace='stackspin-apps'): """Returns all info of a Flux helmrelease with name 'name'""" - config.load_kube_config() api = client.CustomObjectsApi() try: resource = api.get_namespaced_custom_object(