Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/package install and launch from template. #383

Draft
wants to merge 25 commits into
base: feat/release_workflow
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
aa505b7
Added package install and launch from template functionality.
DevilsAutumn Sep 16, 2024
aed1be5
formatting and linting fixes
DevilsAutumn Sep 16, 2024
58ac45c
added template downloading.
DevilsAutumn Sep 16, 2024
c9938d0
template upload
adhiraj23zelthy Sep 24, 2024
7243af3
moved git config to tenant model
DevilsAutumn Sep 26, 2024
8636329
added git config in tenant model
DevilsAutumn Sep 26, 2024
38bde6a
modified app update api to handle extra_config.
DevilsAutumn Sep 26, 2024
f48a964
run git setup
DevilsAutumn Sep 26, 2024
5d56d31
minor
DevilsAutumn Sep 26, 2024
cf6e13a
Githup repo config frontend (#390)
adhiraj23zelthy Sep 26, 2024
2ea9344
fix: edit data
adhiraj23zelthy Sep 26, 2024
d0e1aa3
initialie repo with url
DevilsAutumn Sep 27, 2024
365c92e
minor
DevilsAutumn Oct 1, 2024
8f5e2dc
ruff fix
DevilsAutumn Oct 1, 2024
4b6febb
minor
DevilsAutumn Oct 1, 2024
9c76ac0
release fixes
deepakdinesh1123 Oct 18, 2024
15534ce
Merge branch 'feat/package_install_and_launch_from_template' into git…
DevilsAutumn Oct 22, 2024
342b77f
Merge pull request #391 from Healthlane-Technologies/git_config_tenan…
DevilsAutumn Oct 22, 2024
147ac4c
Merge pull request #387 from Healthlane-Technologies/launch_template_…
shahharsh176 Oct 24, 2024
c8230fb
added sync packages setting
DevilsAutumn Oct 22, 2024
e086873
apply package migrations
DevilsAutumn Oct 22, 2024
93ae372
fixed issue with no app template
DevilsAutumn Oct 25, 2024
c6fc81d
feat: fe sync packages
adhiraj23zelthy Oct 24, 2024
bbb0a4f
detail about github url & branches
adhiraj23zelthy Oct 25, 2024
efe80fe
Merge pull request #398 from Healthlane-Technologies/added_sync_packa…
shahharsh176 Oct 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions backend/src/zango/api/platform/tenancy/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ def get_date_format_display(self, obj):
return obj.get_date_format_display()

def update(self, instance, validated_data):
request = self.context["request"]
extra_config_str = request.data.get("extra_config")
# Convert extra_config from string to JSON if it exists
if extra_config_str:
try:
extra_config_json = json.loads(extra_config_str)
validated_data["extra_config"] = extra_config_json
except json.JSONDecodeError:
raise serializers.ValidationError(
{"extra_config": "Invalid JSON format"}
)

instance = super(TenantSerializerModel, self).update(instance, validated_data)
request = self.context["request"]
domains = request.data.getlist("domains")
Expand Down
64 changes: 64 additions & 0 deletions backend/src/zango/api/platform/tenancy/v1/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import json
import os
import zipfile


def extract_app_details_from_zip(template_zip):
settings_filename = "settings.json"
try:
with zipfile.ZipFile(template_zip, "r") as zip_file:
# Check if the settings file exists in the zip
zip_name = str(template_zip).split(".")[0]

# List all files in the zip
all_files = zip_file.namelist()

# Find the settings file path
settings_path = None
for file_path in all_files:
if file_path == os.path.join(zip_name, settings_filename):
settings_path = file_path
break

if not settings_path:
raise FileNotFoundError(
f"{settings_filename} not found in the zip file."
)

# Read the contents of the settings file
with zip_file.open(settings_path) as settings_file:
settings_content = settings_file.read()

# Parse the JSON content
settings = json.loads(settings_content)
return (
settings.get("version", None),
settings.get("app_name", None),
"nice app",
)

except zipfile.BadZipFile:
print(f"Error: {template_zip} is not a valid zip file.")
except json.JSONDecodeError:
print(f"Error: {settings_filename} is not a valid JSON file.")
except Exception as e:
print(f"An error occurred: {str(e)}")

return None, None, None


def extract_zip_to_temp_dir(app_template):
# Create a temporary directory
temp_dir = os.getcwd()

try:
# Extract the zip file to the temporary directory
with zipfile.ZipFile(app_template, "r") as zip_ref:
zip_ref.extractall(temp_dir)

# Return the path of the temporary directory
return temp_dir
except Exception as e:
# If an error occurs, remove the temporary directory and re-raise the exception
os.rmdir(temp_dir)
raise e
55 changes: 55 additions & 0 deletions backend/src/zango/api/platform/tenancy/v1/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import os
import traceback

from django_celery_results.models import TaskResult
Expand All @@ -10,6 +11,7 @@
from zango.apps.permissions.models import PolicyModel
from zango.apps.shared.tenancy.models import TenantModel, ThemesModel
from zango.apps.shared.tenancy.utils import DATEFORMAT, DATETIMEFORMAT, TIMEZONES
from zango.cli.git_setup import git_setup
from zango.core.api import (
ZangoGenericPlatformAPIView,
get_api_response,
Expand All @@ -25,6 +27,7 @@
ThemeModelSerializer,
UserRoleSerializerModel,
)
from .utils import extract_app_details_from_zip


class AppViewAPIV1(ZangoGenericPlatformAPIView):
Expand Down Expand Up @@ -92,11 +95,25 @@ def get(self, request, *args, **kwargs):

def post(self, request, *args, **kwargs):
data = request.data
app_template = data.get("app_template", None)
app_template_name = None
if app_template:
app_template_name = str(app_template).split(".")[0]
_, app_name, description = extract_app_details_from_zip(app_template)
data.update(
{
"name": app_name,
"description": description,
"app_template": app_template,
}
)
try:
app, task_id = TenantModel.create(
name=data["name"],
schema_name=data["name"],
description=data["description"],
app_template_name=app_template_name,
app_template=app_template,
tenant_type="app",
status="staged",
)
Expand Down Expand Up @@ -149,9 +166,16 @@ def get(self, request, *args, **kwargs):

return get_api_response(success, response, status)

def get_branch(self, config, key, default=None):
branch = config.get("branch", {}).get(key, default)
return branch if branch else default

def put(self, request, *args, **kwargs):
try:
obj = self.get_obj(**kwargs)
old_git_config = (
obj.extra_config.get("git_config", {}) if obj.extra_config else {}
)
serializer = TenantSerializerModel(
instance=obj,
data=request.data,
Expand All @@ -162,6 +186,34 @@ def put(self, request, *args, **kwargs):
serializer.save()
success = True
status_code = 200
if serializer.data.get("extra_config", None):
app_directory = os.path.join(os.getcwd(), "workspaces", obj.name)
new_git_config = serializer.data["extra_config"].get(
"git_config", {}
)

new_repo_url = new_git_config.get("repo_url")
old_repo_url = old_git_config.get("repo_url")

if new_repo_url and (
not old_git_config or new_repo_url != old_repo_url
):
git_setup(
[
app_directory,
"--git_repo_url",
new_repo_url,
"--dev_branch",
self.get_branch(new_git_config, "dev", "development"),
"--staging_branch",
self.get_branch(new_git_config, "staging", "staging"),
"--prod_branch",
self.get_branch(new_git_config, "prod", "main"),
"--initialize",
],
standalone_mode=False,
)

result = {
"message": "App Settings Updated Successfully",
"app_uuid": str(obj.uuid),
Expand All @@ -180,6 +232,9 @@ def put(self, request, *args, **kwargs):

result = {"message": error_message}
except Exception as e:
import traceback

print(traceback.format_exc())
success = False
result = {"message": str(e)}
status_code = 500
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import os

from django_tenants.management.commands.migrate_schemas import MigrateSchemasCommand

from django.conf import settings
Expand Down Expand Up @@ -45,8 +47,18 @@ def handle(self, *args, **options):
"dynamic_models": f"workspaces.{ options['workspace']}.migrations"
}
else:
settings.MIGRATION_MODULES = {
"dynamic_models": f"workspaces.{ options['workspace']}.packages.{options['package']}.migrations"
}
if os.path.exists(
f"workspaces/{options['workspace']}/packages/{options['package']}/migrations"
):
settings.MIGRATION_MODULES = {
"dynamic_models": f"workspaces.{ options['workspace']}.packages.{options['package']}.migrations"
}
else:
self.stdout.write(
self.style.NOTICE(
f"\n\nThe package '{options['package']}' does not have any migrations. Please ensure that you have entered the correct package name and try again."
)
)
exit(0)
options["schema_name"] = tenant_obj.schema_name
super().handle(*args, **options)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.11 on 2024-09-13 08:18

from django.db import migrations
import zango.core.storage_utils


class Migration(migrations.Migration):

dependencies = [
('tenancy', '0004_tenantmodel_fav_icon_alter_tenantmodel_logo'),
]

operations = [
migrations.AddField(
model_name='tenantmodel',
name='app_template',
field=zango.core.storage_utils.ZFileField(blank=True, null=True, upload_to=zango.core.storage_utils.RandomUniqueFileName, validators=[zango.core.storage_utils.validate_file_extension], verbose_name='template used for app'),
),
]
30 changes: 28 additions & 2 deletions backend/src/zango/apps/shared/tenancy/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import os
import re
import requests
import tempfile
import uuid

from collections import namedtuple
Expand All @@ -10,6 +13,7 @@
from django.db import models
from django.utils import timezone

from zango.api.platform.tenancy.v1.utils import extract_zip_to_temp_dir
from zango.core.model_mixins import FullAuditMixin
from zango.core.storage_utils import ZFileField

Expand Down Expand Up @@ -115,6 +119,9 @@ class TenantModel(TenantMixin, FullAuditMixin):
logo = ZFileField(verbose_name="Logo", null=True, blank=True)
fav_icon = ZFileField(verbose_name="Fav Icon", null=True, blank=True)
extra_config = models.JSONField(null=True, blank=True)
app_template = ZFileField(
verbose_name="template used for app", null=True, blank=True
)

auto_create_schema = False

Expand All @@ -127,14 +134,33 @@ def suspend(self):
self.save()

@classmethod
def create(cls, name, schema_name, description, **other_params):
def create(cls, name, schema_name, description, app_template_name, **other_params):
_check_tenant_name(name)
obj = cls.objects.create(
name=name, schema_name=schema_name, description=description, **other_params
)
app_template_path = None
if obj.app_template:
if obj.app_template.url.startswith("https://"):
downloaded_file_path = cls.download_file(obj.app_template.url)
else:
downloaded_file_path = obj.app_template
app_template_path = os.path.join(
extract_zip_to_temp_dir(downloaded_file_path), app_template_name
)
# initialize tenant's workspace
init_task = initialize_workspace.apply_async((str(obj.uuid),), countdown=1)
init_task = initialize_workspace.delay(str(obj.uuid), app_template_path)
return obj, init_task.id

@staticmethod
def download_file(url):
response = requests.get(url)
response.raise_for_status() # Raise an exception for bad status codes

# Create a temporary file
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
temp_file.write(response.content)
return temp_file.name


class Domain(DomainMixin, FullAuditMixin):
Expand Down
54 changes: 36 additions & 18 deletions backend/src/zango/apps/shared/tenancy/tasks.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import os
import shutil
import subprocess

import cookiecutter.main

Expand All @@ -12,51 +14,67 @@


@shared_task
def initialize_workspace(tenant_uuid):
def initialize_workspace(tenant_uuid, app_template_path=None):
try:
from zango.apps.shared.tenancy.models import TenantModel, ThemesModel

tenant = TenantModel.objects.get(uuid=tenant_uuid)

# Creating schema
tenant.create_schema(check_if_exists=True)

# migrating schema
call_command(
"migrate_schemas",
tenant=True,
schema_name=tenant.schema_name,
interactive=False,
)

# Create workspace Folder
project_base_dir = settings.BASE_DIR

workspace_dir = os.path.join(project_base_dir, "workspaces")
if not os.path.exists(workspace_dir):
os.makedirs(workspace_dir)

# Creating app folder with the initial files
template_directory = os.path.join(
os.path.dirname(__file__), "workspace_folder_template"
)
cookiecutter_context = {"app_name": tenant.name}
if app_template_path is not None:
app_dir = os.path.join(workspace_dir, tenant.name)
if not os.path.exists(app_dir):
os.makedirs(app_dir)

cookiecutter.main.cookiecutter(
template_directory,
extra_context=cookiecutter_context,
output_dir=workspace_dir,
no_input=True,
)
shutil.copytree(app_template_path, app_dir, dirs_exist_ok=True)

shutil.rmtree(app_template_path)
shutil.rmtree(os.path.join(app_dir, "packages"))
else:
# Creating app folder with the initial files
template_directory = os.path.join(
os.path.dirname(__file__), "workspace_folder_template"
)
cookiecutter_context = {"app_name": tenant.name}

cookiecutter.main.cookiecutter(
template_directory,
extra_context=cookiecutter_context,
output_dir=workspace_dir,
no_input=True,
)

tenant.status = "deployed"
tenant.deployed_on = timezone.now()
tenant.save(update_fields=["status", "deployed_on"])

assign_policies_to_anonymous_user(tenant.schema_name)
theme = ThemesModel.objects.create(
name="Default", tenant=tenant, config=DEFAULT_THEME_CONFIG
)
if not app_template_path:
assign_policies_to_anonymous_user(tenant.schema_name)
theme = ThemesModel.objects.create(
name="Default", tenant=tenant, config=DEFAULT_THEME_CONFIG
)
else:
try:
subprocess.run(
["zango", "update-apps", "--app_name", tenant.name], check=True
)
except subprocess.CalledProcessError as e:
print(f"Failed to update app: {e}")
if tenant.status == "deployed":
return {"result": "success"}
else:
Expand Down
Loading