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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ website-backup/
# DevContainer Toolbox - credentials folder (NEVER commit)
.devcontainer.secrets/
website/static/img/brand/tmp/

# DevContainer Toolbox - devcontainer.json backups from dev-update
.devcontainer/backup/
.claude/
20 changes: 11 additions & 9 deletions scripts/generate-docs-markdown.sh
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ for i in $(seq 0 $((template_count - 1))); do
description=$(jq -r ".templates[$i].description" "$REGISTRY")
category=$(jq -r ".templates[$i].category" "$REGISTRY")
install_type=$(jq -r ".templates[$i].install_type" "$REGISTRY")
context=$(jq -r ".templates[$i].context" "$REGISTRY")
abstract=$(jq -r ".templates[$i].abstract" "$REGISTRY")
tools=$(jq -r ".templates[$i].tools" "$REGISTRY")
readme=$(jq -r ".templates[$i].readme" "$REGISTRY")
Expand All @@ -106,8 +107,14 @@ for i in $(seq 0 $((template_count - 1))); do
continue
fi

# Install command — always dev-template now (unified command)
local_install_cmd="dev-template $tid"
# Install command — route by template context (Phase 1 task 1.1)
# context: dct → dev-template (DCT devcontainer command)
# context: uis → uis template install (UIS provision-host command, available in DCT via the uis shim from DCT v1.7.34+)
if [[ "$context" == "uis" ]]; then
local_install_cmd="uis template install $tid"
else
local_install_cmd="dev-template $tid"
fi

# Build tags array for MDX component
local_tags_mdx=$(jq -r ".templates[$i].tags | @json" "$REGISTRY")
Expand All @@ -119,7 +126,8 @@ for i in $(seq 0 $((template_count - 1))); do
"
done < <(jq -r ".templates[$i].tags[]" "$REGISTRY")

# Write MDX file
# Write MDX file (Phase 1 task 1.2: no separate ## Summary section —
# the TemplateHeader description + README intro carry the content)
cat > "$page_file" <<MDXEOF
---
title: $name
Expand All @@ -142,12 +150,6 @@ import TemplateHeader from '@site/src/components/TemplateHeader';
tools="$tools"
/>

## Summary

$summary

---

MDXEOF

# Embed README content
Expand Down
7 changes: 5 additions & 2 deletions scripts/validate-rules.conf
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ README-*.md|required_heading|Quick Start|error
README-*.md|required_heading|Prerequisites|error
README-*.md|required_heading|Project Structure|error
README-*.md|required_heading|Development|warn
README-*.md|required_heading|Docker Build|warn
README-*.md|required_heading|Kubernetes Deployment|warn
README-*.md|required_heading|CI/CD|warn

# "Docker Build" and "Kubernetes Deployment" sections were dropped in
# Phase 4 of PLAN-p1-tmp-template-docs-fixes.md — they describe a manual
# flow that bypasses GitHub Actions + ArgoCD. New templates should use a
# single "Deploy" section that references the GitOps workflow instead.

# ============================================================
# All markdown — MDX compatibility
# ============================================================
Expand Down
24 changes: 24 additions & 0 deletions templates/python-basic-webserver-database/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Environment files (credentials — never commit)
.env
.env.*

# Python virtualenv (created by `uv venv` or `python -m venv`)
.venv/

# Python bytecode and caches
__pycache__/
*.pyc
*.pyo
*.pyd
*.egg-info/

# Test/build artifacts
.pytest_cache/
.coverage
htmlcov/
dist/
build/

# IDE/editor (but keep .vscode/settings.json which the template ships)
.idea/
*.swp
Original file line number Diff line number Diff line change
@@ -1,98 +1,235 @@
# Python Basic Webserver with Database

A minimal Flask web server that connects to PostgreSQL and reads from a `tasks` table. This template demonstrates the full producer/consumer flow:
A minimal Flask web server that connects to PostgreSQL and reads from a `tasks` table. The full producer/consumer flow:

- **Producer (UIS):** `uis template install postgresql-demo` deploys PostgreSQL to the cluster
- **Consumer (this template):** `dev-template configure` creates a per-app database, runs the init SQL, and writes `DATABASE_URL` to `.env`
- **App:** reads `DATABASE_URL` from the environment and queries the `tasks` table
- **PostgreSQL** runs in your UIS-managed Kubernetes cluster (deployed once during cluster setup, or via `uis deploy postgresql`).
- **`dev-template-configure`** creates a per-app database and user, applies the init SQL, and writes `DATABASE_URL` to `.env` for local dev.
- **The Flask app** reads `DATABASE_URL` from `.env`, connects to PostgreSQL via `host.docker.internal:35432` (the local port forward UIS exposes), and serves the seeded data.

## What this is

A small but complete Flask application:

| Endpoint | Method | Returns |
|---|---|---|
| `/` | GET | Plain-text greeting with the template name and current time |
| `/tasks` | GET | JSON list of rows from the `tasks` table (the seeded data, plus anything you've added) |
| `/health` | GET | `{"status": "ok", "database": "connected"}` if the DB is reachable, or a 503 if not |

The app **requires** `DATABASE_URL` and exits immediately with a clear error if it's missing — there's no fallback. This is intentional: the template demonstrates the producer/consumer pattern where credentials always come from `dev-template-configure`.

## Prerequisites

This template uses UIS to configure PostgreSQL. Verify the UIS provision-host container is running:

```bash
docker ps --filter name=uis-provision-host --format '{{.Status}}'
```

You should see `Up X minutes`. If not, start UIS from the `urbalurba-infrastructure` repo. Inside DCT (devcontainer-toolbox v1.7.34 or later) you also have the `uis` shim, which routes `uis ...` commands to the provision-host automatically.

If PostgreSQL isn't deployed in your cluster, **don't worry** — `dev-template-configure` will detect it in step 4 and tell you exactly what to run (`uis deploy postgresql`).

## Quick Start

### 1. Deploy PostgreSQL (once per environment)
### 1. Install the template

```bash
dev-template python-basic-webserver-database
```

DCT downloads the template from the registry and copies all files to your current project directory, including `app/`, `manifests/`, `Dockerfile`, `requirements.txt`, `.gitignore`, `template-info.yaml`, and `config/init-database.sql`.

### 2. Edit `template-info.yaml`

Open `template-info.yaml` and find the `params:` section near the bottom. Set values for your app:

```yaml
params:
app_name: "my-cool-app"
database_name: "my_cool_app_db"
```

The defaults (`my-app`, `my_app_db`) work, but pick names that match your project — these become the PostgreSQL user and database names.

The full `template-info.yaml` declares the PostgreSQL dependency in the `requires:` section:

```yaml
params:
app_name: "my-app"
database_name: "my_app_db"

requires:
- service: postgresql
config:
database: "{{ params.database_name }}"
init: "config/init-database.sql"
```

DCT reads this file when you run `dev-template-configure` in the next step. The `{{ params.database_name }}` reference is substituted with the value you set above.

### 3. (Optional) Customise `config/init-database.sql`

This file is the schema and seed data UIS applies to your database. The default creates a `tasks` table with 3 rows:

```sql
CREATE TABLE IF NOT EXISTS tasks (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);

INSERT INTO tasks (title, status) VALUES
('Set up the database connection', 'done'),
('Build something with Flask + PostgreSQL', 'pending'),
('Deploy to Kubernetes via ArgoCD', 'pending')
ON CONFLICT DO NOTHING;
```

All statements are idempotent (`IF NOT EXISTS`, `ON CONFLICT DO NOTHING`) so re-running configure is safe. UIS applies the file with `psql --set ON_ERROR_STOP=on`, so any syntax error fails fast with a clear message.

For your real schema, edit this file to add your own tables, indexes, and seed data.

If PostgreSQL isn't running in your UIS cluster yet, deploy it via the `postgresql-demo` UIS stack template:
### 4. Run `dev-template-configure`

```bash
uis template install postgresql-demo
dev-template-configure
```

What happens:

1. DCT reads `template-info.yaml` and validates that the `params:` are filled in
2. DCT calls `uis configure postgresql --app <app_name> --database <database_name> --init-file -` via the bridge, piping in the substituted SQL
3. UIS creates the database and user, applies the init SQL, and writes connection details
4. UIS also creates a Kubernetes Secret in your app's namespace so the deployed pod can read `DATABASE_URL` later (when you `git push` and ArgoCD deploys)
5. DCT writes `.env` to your project root (gitignored) with the local connection string

If PostgreSQL isn't deployed in your cluster, this step fails with a clear error from UIS telling you to run `uis deploy postgresql`.

You should see something like:

```
📦 Configuring postgresql...
✅ postgresql — configured
→ .env: DATABASE_URL=postgresql://my_cool_app:Xa7mP9...@host.docker.internal:35432/my_cool_app_db (local)
→ K8s Secret: <repo-name>-db in namespace <repo-name> (cluster)
```

### 2. Configure this app's database
### 5. Verify the database

This creates a new database + user in PostgreSQL, applies the init SQL (tasks table + seed data), and writes `DATABASE_URL` to `.env`:
Inspect the seeded data without starting the app:

```bash
dev-template configure
uis connect postgresql my_cool_app_db
```

Inside psql:

```sql
SELECT * FROM tasks;
\q
```

You'll be prompted to fill in `params.app_name` and `params.database_name` in `template-info.yaml` first (or pass them via `--param`).
You should see 3 rows. If they're there, the database is set up correctly and `DATABASE_URL` is in your `.env`.

### 3. Install Python dependencies and run
### 6. Run the app

DCT ships with [`uv`](https://github.com/astral-sh/uv) for fast Python package management. Create a virtualenv, install dependencies, and run the app:

```bash
pip install -r requirements.txt
uv venv
source .venv/bin/activate
uv pip install -r requirements.txt
python app/app.py
```

Then open:
- http://localhost:3000 — home page
- http://localhost:3000/tasks — list tasks from the database
- http://localhost:3000/health — verify DB connectivity
Or one-liner (no manual activation):

The app **requires** `DATABASE_URL` and will exit immediately if it isn't set.
```bash
uv venv
uv pip install -r requirements.txt
uv run python app/app.py
```

## Prerequisites
The Flask debug server starts on port 3000.

Development tools are installed automatically by the devcontainer. If you need to reinstall, run `dev-setup`.
**VS Code tip (optional):** if you see "Error refreshing packages" from VS Code's Python extension, add this to your workspace `.vscode/settings.json`:

UIS must be running with PostgreSQL deployed (see step 1 above).
```json
{
"python-envs.alwaysUseUv": true
}
```

The error happens because `uv venv` doesn't install `pip` into the venv (it doesn't need to), and VS Code's Python extension defaults to `pip list` for package enumeration. The setting tells it to use `uv` instead. If your project's `.vscode/settings.json` already exists with other keys, just add this one — don't replace the whole file.

### 7. Open in your browser

VS Code's "Ports" tab in the bottom panel auto-forwards port 3000. Click the globe icon next to it to open these URLs:

## Project Structure
- `http://localhost:3000/` — Home page
- `http://localhost:3000/tasks` — JSON list of seeded rows
- `http://localhost:3000/health` — DB connectivity check

```plaintext
If `/tasks` shows the 3 seeded rows, your producer/consumer chain is working end-to-end: Flask → DATABASE_URL → host.docker.internal → UIS port-forward → PostgreSQL pod in K8s.

## Project structure

After installation, your project contains:

```
├── app/
│ └── app.py # Flask app reading from PostgreSQL
├── config/
│ └── init-database.sql # Tasks table + seed data (applied by uis configure)
│ └── init-database.sql # Schema + seed data (applied by uis configure)
├── manifests/
│ ├── deployment.yaml # K8s Deployment + Service (uses Secret for DATABASE_URL)
│ └── kustomization.yaml # ArgoCD configuration
├── .github/
│ └── workflows/
│ └── urbalurba-build-and-push.yaml # CI/CD pipeline
├── .gitignore # Excludes .env*, .venv/, etc.
├── Dockerfile # Container build
├── requirements.txt # Python dependencies
├── template-info.yaml # Template metadata
├── requirements.txt # Flask, psycopg2-binary, python-dotenv
├── template-info.yaml # Template metadata (read by dev-template-configure)
└── README-python-basic-webserver-database.md # This file
```

## Development

- Edit `app/app.py` — the Flask application
- Edit `config/init-database.sql` to change the schema (re-run `dev-template configure` to apply)
- Changes auto-reload in debug mode
- Edit `app/app.py` — the main Flask application. Changes auto-reload in debug mode.
- Edit `config/init-database.sql` to change the schema. Re-run `dev-template-configure` to apply the changes.
- Edit `template-info.yaml` to change `params`. Re-run `dev-template-configure` afterward (it's idempotent — safe to run repeatedly).

## Docker Build
## Deploy to your local cluster

```bash
docker build -t python-basic-webserver-database .
docker run -p 3000:3000 --env-file .env python-basic-webserver-database
```
The standard workflow uses GitHub Actions + ArgoCD — no manual `docker build` or `kubectl apply`:

## Kubernetes Deployment
1. **Push your code to GitHub**:
```bash
git push
```
GitHub Actions builds and pushes the container image to GitHub Container Registry. The image is **credential-free** — `DATABASE_URL` is injected at runtime from a Kubernetes Secret.

Before deploying, create the `DATABASE_URL` secret using the **cluster** connection string from `uis configure` output:
2. **Register the app with ArgoCD** (one-time per project, from your host machine):
```bash
./uis argocd register <app-name> <github-repo-url>
```
This creates an ArgoCD Application that watches your repo and auto-deploys updates on every push.

```bash
kubectl create secret generic <repo-name>-db \
--from-literal=DATABASE_URL='postgresql://user:pass@postgresql.default.svc.cluster.local:5432/<db>'
```
3. **Access the app** at `http://<app-name>.localhost`. ArgoCD applies the deployment manifest, K8s injects `DATABASE_URL` from the Secret UIS created in step 4 above, and the pod connects to PostgreSQL via the cluster service DNS (`postgresql.default.svc.cluster.local`).

Then apply the manifests:
You don't need to create the Kubernetes Secret manually — `dev-template-configure` already created it in the right namespace. The deployment manifest references it via `secretKeyRef`.

```bash
kubectl apply -k manifests/
```
## Try this with

This is the consumer side of the producer/consumer pattern. The producer side is:

- [PostgreSQL Demo](../demo/postgresql-demo) — a UIS stack template that deploys PostgreSQL standalone, useful for verifying your UIS setup. You don't need to install it for `python-basic-webserver-database` to work — `dev-template-configure` handles everything.

## CI/CD

The GitHub Actions workflow automatically builds and pushes the Docker image to GitHub Container Registry when changes are pushed to the main branch.
The GitHub Actions workflow (`.github/workflows/urbalurba-build-and-push.yaml`) automatically builds and pushes the Docker image to GitHub Container Registry when changes are pushed to the main branch. ArgoCD picks up the new image and deploys it.
7 changes: 7 additions & 0 deletions uis-stack-templates/postgresql-demo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Environment files (credentials — never commit)
.env
.env.*

# Editor swap files
*.swp
.idea/
Loading