# Fission Python Template A production-ready template for building Fission serverless Python functions with best practices for configuration, database connectivity, error handling, and testing. ## Project Structure ``` project/ ├── .fission/ │ ├── deployment.json # Fission function deployment configuration │ ├── dev-deployment.json # Development overrides │ └── local-deployment.json # Local development overrides ├── src/ │ ├── __init__.py # Package initialization │ ├── vault.py # Vault encryption/decryption utilities │ ├── helpers.py # Shared utilities (DB, secrets, configs) │ ├── exceptions.py # Custom exception hierarchy │ ├── models.py # Pydantic models (request/response schemas) │ ├── build.sh # Package build script │ └── your_function.py # Your function implementations ├── test/ │ ├── __init__.py │ ├── test_*.py # Unit tests │ └── requirements.txt # Test dependencies ├── migrates/ │ └── schema.sql # Database migration scripts ├── manifests/ # Kubernetes manifests (optional) ├── specs/ # Generated Fission specs (created by fission CLI) ├── requirements.txt # Runtime dependencies ├── dev-requirements.txt # Development dependencies ├── .env.example # Environment variable template ├── pytest.ini # Pytest configuration └── README.md # Project documentation ``` ## Key Components ### Fission Configuration in Docstrings Fission reads function metadata from docstrings using the ````fission` marker: ```python def my_function(event, context): """ ```fission { "name": "my-function", "http_triggers": { "my-trigger": { "url": "/api/my-endpoint", "methods": ["GET", "POST"] } } } ``` """ # Your implementation return {"message": "Hello World"} ``` **Note:** Do not use `fission.yaml` or `fission.json`. The Fission Python builder reads the docstring annotations directly from your Python source files. ### Environment Variables & Secrets Configuration is managed through Kubernetes Secrets and ConfigMaps: - **Secrets**: Database credentials, API keys, encryption keys (sensitive) - **ConfigMaps**: Non-sensitive configuration, endpoints, feature flags Access them via helper functions: ```python from helpers import get_secret, get_config # Read secret (with optional default) db_host = get_secret("PG_HOST", "localhost") db_port = int(get_secret("PG_PORT", "5432")) # Read config api_endpoint = get_config("EXTERNAL_API_ENDPOINT") ``` **Placeholder variables** in `deployment.json`: - `${PROJECT_NAME}` - Replaced with your actual project name during project creation - Secret/configmap names follow pattern: `fission-${PROJECT_NAME}-env` and `fission-${PROJECT_NAME}-config` ### Database Connectivity Use the provided `init_db_connection()` helper: ```python from helpers import init_db_connection, db_rows_to_array conn = init_db_connection() cursor = conn.cursor() cursor.execute("SELECT * FROM items") rows = db_rows_to_array(cursor, cursor.fetchall()) ``` The helper automatically: - Reads connection parameters from secrets (PG_HOST, PG_PORT, PG_DB, PG_USER, PG_PASS, PG_DBSCHEMA) - Checks port connectivity before connecting - Uses LoggingConnection for query logging - Applies schema search path if PG_DBSCHEMA is set ### Error Handling Use the exception hierarchy from `exceptions.py`: ```python from exceptions import ValidationError, NotFoundError, ConflictError, DatabaseError def get_item(item_id: str): item = db.fetch_one(item_id) if not item: raise NotFoundError(f"Item {item_id} not found", x_user=get_user_from_headers()) return item ``` All exceptions return standardized error responses: ```json { "error_code": "NOT_FOUND", "http_status": 404, "error_msg": "Item 123 not found", "x_user": "user-456", "details": {"item_id": "123"} } ``` ### Validation with Pydantic Validate request payloads using Pydantic models: ```python from models import ItemCreateRequest from pydantic import ValidationError as PydanticValidationError def create_item(): try: data = ItemCreateRequest(**request.get_json()) except PydanticValidationError as e: raise ValidationError(str(e), details=e.errors()) ``` ## Development Workflow ### 1. Install Dependencies ```bash # Install runtime and development dependencies pip install -r dev-requirements.txt # Or just runtime dependencies pip install -r requirements.txt ``` ### 2. Local Testing Fission provides `fission spec` to test specs locally: ```bash # Verify your deployment configuration fission spec verify --file=.fission/deployment.json # Build and test locally fission function test --name your-function ``` ### 3. Unit Testing Run tests with pytest: ```bash # Run all tests pytest # Run with coverage pytest --cov=src # Run specific test file pytest test/test_my_function.py # Verbose output pytest -v ``` **Example test structure:** ```python # test/test_my_function.py import pytest from unittest.mock import patch from src.my_function import main def test_my_function_success(): event = {"key": "value"} context = {} result = main(event, context) assert result["status"] == "success" @patch("helpers.init_db_connection") def test_my_function_with_db(mock_db): # Mock database connection mock_conn = MagicMock() mock_db.return_value = mock_conn # Test function ``` ### 4. Building the Package The `build.sh` script installs dependencies and packages your code: ```bash # From project root ./src/build.sh # This produces a package.zip in the specs directory # Ready for deployment with: fission deploy ``` The build script detects the OS (Debian/Alpine) and installs the correct build dependencies (gcc, libpq-dev, python3-dev). ### 5. Deployment ```bash # Deploy to Fission fission deploy # Or deploy specific function fission function update --name my-function --env your-env ``` ## Deployment Configuration ### Executors Choose between two executor types in `deployment.json`: **poolmgr** (default) - Good for high-concurrency HTTP functions: ```json "executor": { "select": "poolmgr", "poolmgr": { "concurrency": 1, "requestsperpod": 1, "onceonly": false } } ``` **newdeploy** - Good for dedicated scaling: ```json "executor": { "select": "newdeploy", "newdeploy": { "minscale": 1, "maxscale": 5, "targetcpu": 80 } } ``` ### Resource Limits Set resource allocation in `function_common`: - `mincpu` / `maxcpu` - CPU allocation in millicores (50 = 0.05 cores) - `minmemory` / `maxmemory` - Memory in MB - Adjust based on your function's needs ### Environment-Specific Overrides Use `dev-deployment.json` for development environment (different secrets, lower resources). Fission will automatically use it when `--dev` flag is passed. ## Vault Encryption For encrypted secrets, use the vault utility functions: ```python from vault import encrypt_vault, decrypt_vault, is_valid_vault_format # Encrypt a value (run locally to generate vault string) encrypted = encrypt_vault("my-secret", "your-hex-key-here") # Result: "vault:v1:base64-encrypted-data" # Store the encrypted string in your K8s secret # The helper will auto-decrypt if is_valid_vault_format() returns True ``` **Important:** Set `CRYPTO_KEY` in your helpers.py (or via environment override) to your actual 32-byte key in hex format. ## Testing Strategies ### Unit Tests - Mock external dependencies (database, HTTP calls) - Test business logic isolation - Use `pytest-mock` for convenient mocking ### Integration Tests - Use a test database - Clean up test data after each run - Consider using `pytest.fixtures` for setup/teardown ### Local Development - Use `.fission/local-deployment.json` for local Fission setup - Override secrets/configmaps for local environment - Run with: `fission function test --local` ## Migrations Place SQL migration scripts in `migrates/`: ```sql -- migrates/001_create_items_table.sql CREATE TABLE items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, description TEXT, status VARCHAR(50) DEFAULT 'active', created TIMESTAMP DEFAULT NOW(), modified TIMESTAMP DEFAULT NOW() ); ``` Apply migrations manually via psql or using a migration tool like `alembic`. ## Best Practices 1. **Keep functions small** - Single responsibility per function 2. **Use Pydantic** - Validate all inputs with request models 3. **Standardize errors** - Use the provided exception classes 4. **Log appropriately** - Use `logger` from helpers (already configured) 5. **Track users** - Use `get_user_from_headers()` for audit trails 6. **Write tests** - Aim for high coverage of business logic 7. **Document functions** - Add docstrings with fission config block 8. **Avoid global state** - Functions should be stateless and idempotent ## Continuous Integration The template includes `.gitea/workflows/` for CI/CD: - `install-dispatch.yaml` - Triggered on installation events - `uninstall-dispatch.yaml` - Cleanup on uninstall - `dev-deployment.yaml` - Development environment updates - `analystic-dispatch.yaml` - Analytics processing Adapt these workflows for your deployment pipeline (GitHub Actions, GitLab CI, etc.). ## Troubleshooting ### Spec Generation Fails - Ensure all function files have proper fission config in docstrings - Run: `python -m py_compile src/*.py` to check syntax - Verify `build.sh` is executable: `chmod +x src/build.sh` ### Cannot Connect to Database - Check that secrets are mounted correctly: `kubectl exec -- ls /secrets/default/` - Verify PG_HOST, PG_PORT are correct - Use `check_port_open()` debug output - Test connection manually: `psql -h $PG_HOST -p $PG_PORT -U $PG_USER $PG_DB` ### Missing Dependencies - Ensure `requirements.txt` includes ALL dependencies (Flask is required!) - Check build logs for pip errors - Rebuild package: `./src/build.sh` ## Example Implementations ### CRUD Operation ```python from flask import request from helpers import init_db_connection, format_error_response from exceptions import ValidationError, NotFoundError, DatabaseError from models import ItemCreateRequest, ItemResponse def create_item(event, context): """Create a new item.""" try: # Validate input data = ItemCreateRequest(**request.get_json()) except Exception as e: raise ValidationError(str(e)) conn = init_db_connection() try: cursor = conn.cursor() cursor.execute( "INSERT INTO items (name, description, status) VALUES (%s, %s, %s) RETURNING id, created, modified", (data.name, data.description, data.status.value) ) row = cursor.fetchone() conn.commit() item = db_row_to_dict(cursor, row) return item except Exception as e: conn.rollback() raise DatabaseError(str(e)) finally: conn.close() ``` ### Webhook Receiver ```python def webhook_handler(event, context): """Process incoming webhook.""" # Webhook data is in event payload = event.get("body", {}) signature = request.headers.get("X-Webhook-Signature") # Verify signature if not verify_signature(payload, signature): raise ValidationError("Invalid signature") # Process webhook process_webhook(payload) return {"status": "processed"} ``` ## Next Steps 1. Replace placeholder values in `.fission/deployment.json` 2. Update `SECRET_NAME` and `CONFIG_NAME` in `helpers.py` (or use create-project.sh) 3. Implement your business logic in new function files 4. Write tests for your functions 5. Deploy to Kubernetes cluster with Fission ## Resources - [Fission Documentation](https://fission.io/docs/) - [Fission Python Builder](https://github.com/fission/fission-python-builder) - [Pydantic Documentation](https://docs.pydantic.dev/) - [Flask Documentation](https://flask.palletsprojects.com/)