Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 34 additions & 0 deletions .github/workflows/ensure_patched_templates.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Ensure Patched Templates

on:
pull_request:
branches: [ main, dev ]
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4


- name: Install uv
uses: astral-sh/setup-uv@v1
with:
version: latest
python-version: '3.11'

- name: Test patch_templates target
run: |
make patch_templates

if [ -n "$(git status --porcelain)" ]; then
echo "Error: Running patch_templates produced changes. Please run 'make patch_templates' locally and commit the changes."
echo "git status"
git status
echo "git diff"
git diff
exit 1
fi
31 changes: 31 additions & 0 deletions .github/workflows/run_tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Run Tests

on:
pull_request:
branches: [ main, dev ]
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v1
with:
version: latest
python-version: '3.11'

- name: Set up Docker
uses: docker/setup-buildx-action@v3

- name: Ensure Docker and Docker Compose is installed
run: |
docker --version
docker compose --version

- name: Run tests
run: make tests
19 changes: 10 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,19 @@ build_appwrite_playwright:
# push_appwrite_cli:
# docker tag appwrite-cli:latest appwrite-cli:$(APPWRITE_CLI_TAG)
# docker push appwrite-cli:$(APPWRITE_CLI_TAG)
clean-tests:
appwrite-lab stop test-lab

tests:
source .venv/bin/activate && pytest -m e2e

patch_templates:
@VENV=$$(mktemp -d) && \
uv venv $$VENV && \
source $$VENV/bin/activate && \
uv pip install ruamel.yaml && \
uv venv $$VENV > /dev/null 2>&1 && \
. $$VENV/bin/activate > /dev/null 2>&1 && \
uv pip install ruamel.yaml > /dev/null 2>&1 && \
python scripts/selinuxify_template_patch.py && \
rm -rf $$VENV

tests:
uv run pytest -rs -m e2e

clean-tests:
@source .venv/bin/activate && \
appwrite-lab stop test-lab

.PHONY: patch_templates tests clean-tests build_appwrite_cli build_appwrite_playwright
69 changes: 53 additions & 16 deletions appwrite_lab/_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from appwrite_lab.automations.models import BaseVarModel, AppwriteAPIKeyCreation
from ._state import State
from dataclasses import dataclass
from .models import Lab, Automation, SyncType, Project
from .models import Lab, Automation, Project
from dotenv import dotenv_values
from appwrite_lab.utils import console
from .utils import is_cli
Expand All @@ -20,7 +20,7 @@
@dataclass
class Response:
message: str
data: any = None
data: str | dict | None = None
error: bool = False
_print_data: bool = False

Expand Down Expand Up @@ -69,7 +69,14 @@ def get_formatted_labs(self, collapsed: bool = False):
"""
labs: dict = self.state.get("labs", {})
if collapsed:
headers = ["Name", "Version", "URL", "Admin Email", "Project ID", "API Key"]
headers = [
"Lab Name",
"Version",
"URL",
"Admin Email",
"Admin Password",
"Project ID",
]
data = []
for val in labs.values():
project = Project(**val.get("projects", {}).get("default"))
Expand All @@ -79,8 +86,8 @@ def get_formatted_labs(self, collapsed: bool = False):
val["version"],
val["url"],
val["admin_email"],
val["admin_password"],
project.project_id,
project.api_key,
]
)
return headers, data
Expand Down Expand Up @@ -139,7 +146,7 @@ def _deploy_service(
"""
new_env = {**os.environ, **env_vars}
cmd = [
self.compose,
*self.compose,
"-f",
template_path,
"-p",
Expand All @@ -151,7 +158,12 @@ def _deploy_service(
return self._run_cmd_safely(cmd, envs=new_env)

def deploy_appwrite_lab(
self, name: str, version: str, port: int, meta: dict[str, str]
self,
name: str,
version: str,
port: int,
meta: dict[str, str],
**kwargs: dict[str, str],
):
"""
Deploy an Appwrite lab.
Expand All @@ -161,6 +173,7 @@ def deploy_appwrite_lab(
version: The version of the service to deploy.
port: The port to use for the Appwrite service. Must not be in use by another service.
meta: Extra metadata to pass to the deployment.

"""
# sync
appwrite_config = meta.get("appwrite_config", {})
Expand Down Expand Up @@ -208,22 +221,27 @@ def deploy_appwrite_lab(
url = ""
proj_id = appwrite_config.pop("project_id", None)
proj_name = appwrite_config.pop("project_name", None)
kwargs = {
_kwargs = {
**appwrite_config,
"projects": {"default": Project(proj_id, proj_name, None)},
}
lab = Lab(
name=name,
version=version,
url=url,
**kwargs,
**_kwargs,
)

lab.generate_missing_config()
# ensure project_id and project_name are set
proj_id = proj_id or lab.projects.get("default").project_id
proj_name = proj_name or lab.projects.get("default").project_name

if kwargs.get("just_deploy", False):
return Response(
error=False,
message=f"Lab '{name}' deployed with --just-deploy flag.",
data=lab,
)
# Deploy playwright automations for creating user and API key
api_key_res = self.deploy_playwright_automation(
lab=lab,
Expand Down Expand Up @@ -360,7 +378,7 @@ def teardown_service(self, name: str):
data=None,
)
cmd = [
self.compose,
*self.compose,
"-p",
name,
"down",
Expand Down Expand Up @@ -430,16 +448,35 @@ def util(self):

@property
def compose(self):
return shutil.which(f"{self.backend}-compose")
if self.backend == "docker":
# Try docker-compose first, then fall back to docker compose
compose_cmd = shutil.which("docker-compose")
if compose_cmd:
return [compose_cmd]
else:
return [shutil.which("docker"), "compose"]
else:
return [shutil.which(f"{self.backend}-compose")]


def detect_backend():
if shutil.which("docker") and shutil.which("docker-compose"):
return "docker"
elif shutil.which("podman") and shutil.which("podman-compose"):
if shutil.which("docker"):
try:
subprocess.run(
["docker", "compose", "version"],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return "docker"
except Exception:
pass
# Check for legacy 'docker-compose' binary
if shutil.which("docker-compose"):
return "docker"
if shutil.which("podman") and shutil.which("podman-compose"):
return "podman"
else:
raise RuntimeError("Neither Docker nor Podman found.")
raise RuntimeError("Neither Docker nor Podman found.")


def run_cmd(cmd: list[str], envs: dict[str, str] | None = None):
Expand Down
6 changes: 6 additions & 0 deletions appwrite_lab/automations/functions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from playwright.async_api import Playwright, Page, BrowserContext, Browser


async def wait_until_loaded(page: Page):
"""Wait until the page is loaded."""
await page.wait_for_selector('img[alt="Appwrite Logo"]')


async def create_browser_context(playwright: Playwright, headless: bool = True):
"""Create a browser context for automation."""
browser = await playwright.chromium.launch(headless=headless)
Expand All @@ -24,6 +29,7 @@ async def select_project_after_login(page: Page, project_name: str):
async def register_user(page: Page, url: str, admin_email: str, admin_password: str):
"""Register a new user in Appwrite."""
await page.goto(f"{url}/console/register")
await wait_until_loaded(page)
await page.wait_for_timeout(200)
await page.get_by_role("textbox", name="Name").fill("Test User")
await page.get_by_role("textbox", name="Email").fill(admin_email)
Expand Down
6 changes: 4 additions & 2 deletions appwrite_lab/automations/scripts/create_user_and_api_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
create_project_ui,
create_api_key_ui,
cleanup_browser,
wait_until_loaded,
)
from ..utils import resultify
from ..models import AppwriteAPIKeyCreation, AppwriteUserCreation
Expand All @@ -27,7 +28,6 @@ async def create_user_and_api_key(playwright: Playwright) -> str:
auth = AppwriteUserCreation.from_env()
api_key_env = AppwriteAPIKeyCreation.from_env()
work_dir = os.getenv("HOME")

browser, context = await create_browser_context(playwright, headless=True)
page = await context.new_page()

Expand All @@ -37,7 +37,9 @@ async def create_user_and_api_key(playwright: Playwright) -> str:
admin_email=auth.admin_email,
admin_password=auth.admin_password,
)
await create_project_ui(page=page, project_name=auth.project_name, project_id=auth.project_id)
await create_project_ui(
page=page, project_name=auth.project_name, project_id=auth.project_id
)

api_key = await create_api_key_ui(
page=page, key_name=api_key_env.key_name, key_expiry=api_key_env.key_expiry
Expand Down
23 changes: 19 additions & 4 deletions appwrite_lab/cli/new_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ def new_lab(
help="The name of the project to use for the lab. Unset for random.",
show_envvar=False,
),
just_deploy: bool = typer.Option(
False,
is_flag=True,
help="Just deploy the lab without creating an API key or project.",
show_envvar=False,
),
):
"""
Create a new lab.
Expand All @@ -49,15 +55,24 @@ def new_lab(
project_name: The name of the project to use for the lab. Unset for random.
"""
labs = get_global_labs()
with console.status(f"Creating lab '{name}'...", spinner="dots") as status:
extra_str = " with simple deployment" if just_deploy else ""
with console.status(
f"Creating lab '{name}'{extra_str}...", spinner="dots"
) as status:
creds = {
"admin_email": email,
"admin_password": password,
"project_id": project_id,
"project_name": project_name,
}

labs.new(name=name, version=version, port=port, meta={"appwrite_config": creds})
labs.new(
name=name,
version=version,
port=port,
meta={"appwrite_config": creds},
just_deploy=just_deploy,
)
status.update(f"Creating lab '{name}'... done")


Expand Down Expand Up @@ -88,8 +103,8 @@ def new_api_key(
key = labs.create_api_key(
project_name=project_name, lab_name=lab_name, expiration=expiration
)
return key
# status.update(f"Creating API key for project '{project_name}'... done")
status.update(f"Creating API key for project '{project_name}'... done")
return key.data


@new_menu.command(name="project", help="Create a new project")
Expand Down
Loading
Loading