Skip to content

Package LightAPI as a Helm chart for zero-CRUD-code deploys - #33

Merged
iklobato merged 18 commits into
devfrom
feat/helm-chart
Aug 1, 2026
Merged

Package LightAPI as a Helm chart for zero-CRUD-code deploys#33
iklobato merged 18 commits into
devfrom
feat/helm-chart

Conversation

@iklobato

@iklobato iklobato commented Aug 1, 2026

Copy link
Copy Markdown
Owner

What

Ship LightAPI as a Helm chart so a team can stand up a full CRUD REST API over an existing database by writing only a values.yaml. No CRUD code, no per-project image build.

The chart hands the declarative config to LightApi.from_config and runs it. Adding a table means adding an entry under config.endpoints and running helm upgrade.

Changes

  • ServerCommand (lightapi/server.py) + lightapi serve console script + python -m lightapi: boot a server from LIGHTAPI_CONFIG / LIGHTAPI_HOST / LIGHTAPI_PORT.
  • Always-on /healthz route (lightapi/health.py) for liveness/readiness probes, wired once into both run() and build_app().
  • Generic Dockerfile + .dockerignore: one image, config injected at deploy time (includes psycopg2 for sync Postgres and the async extra).
  • charts/lightapi: Deployment (config via ConfigMap, secrets via Secret, /healthz probes), Service, Secret, ServiceAccount, optional Ingress and HPA.

Bug fix included

A declarative field with no constraints and no default (e.g. price: {type: float}) got an Ellipsis class attribute that was not stripped and stopped SQLAlchemy from mapping the column. The value was silently dropped on INSERT and any write hit a NOT NULL error. Fixed by leaving such fields annotation-only. Regression test added.

Testing

Full suite: 327 passing. Verified end to end on minikube with Postgres:

  • helm install / upgrade / rollback
  • config change rolls the pod (ConfigMap checksum)
  • chart Secret and existingSecret paths
  • HPA governs replicas when autoscaling is on
  • method restriction returns 405, optimistic lock returns 409
  • broken config goes CrashLoopBackOff and serves no traffic (readiness holds it out)
  • server-side dry-run of the full render (Ingress + HPA) accepted by the API server

Note

The probe is process level. If the database goes down but the process stays up, /healthz still returns 200. A readiness variant that pings the database is left as follow-up (noted in the plan doc).

https://claude.ai/code/session_01LWV5jPTwsAehx2rM7UZjtq

iklo and others added 18 commits May 19, 2026 11:46
End-user flow:
  docker run --rm -p 8000:8000 \
      -v ./lightapi.yaml:/app/lightapi.yaml:ro \
      -e DATABASE_URL=sqlite:////app/data.db \
      iklobato/lightapi:latest

What ships:
- Dockerfile (python:3.12-slim base, multi-arch friendly).
  Pre-installs lightapi[async] + psycopg2-binary so both sync PostgreSQL
  and async asyncpg/aiosqlite engines work out of the box. Drops privileges
  to a non-root `lightapi` user. Build arg LIGHTAPI_VERSION pins the wheel
  the workflow installs (kept in sync with pyproject.toml at publish time).
- docker/entrypoint.py: 60-line launcher that locates the YAML at
  /app/lightapi.yaml (overridable via LIGHTAPI_CONFIG), runs
  LightApi.from_config() + build_app(), and starts uvicorn with
  proxy_headers=True so common reverse-proxy headers are respected.
- docker/lightapi.example.yaml: minimal config used as a smoke-test fixture.
- .dockerignore: excludes .git, .venv, tests, docs, examples, site, etc.
- .github/workflows/docker-publish.yml: builds linux/amd64 + linux/arm64
  on master pushes and v*.*.* tag pushes, pushes to iklobato/lightapi
  using DOCKERHUB_USERNAME + DOCKERHUB_TOKEN repo secrets. Tag rules:
    v0.1.21 tag → :0.1.21, :0.1, :latest
    master push → :master
    workflow_dispatch → :manual-<run-number>

Docs:
- README: new Docker quickstart block.
- docs/deployment/docker.md: complete rewrite covering quickstart, env
  vars, SQLite-with-volume, docker compose + PostgreSQL, Redis cache,
  JWT auth with a mounted login_validator module, custom image
  extension pattern, and the publish-pipeline secrets.

Verified locally with docker build + docker run; POST /books, GET
/books, POST /authors, GET /authors all return the expected responses
against the bundled example config.
feat(docker): publish ready-to-use iklobato/lightapi image
The DOCKERHUB_TOKEN authenticates against the Docker Hub account `iklob1`
(verified by a successful `docker login -u iklob1`), not the GitHub-username
`iklobato`. Updated the image references in:

- .github/workflows/docker-publish.yml — image and job name
- README.md — Docker quickstart pull line
- docker/lightapi.example.yaml — header comment
- docs/deployment/docker.md — every image reference + secret table

`github.com/iklobato/...` URLs are unchanged (still the GitHub repo).
fix(docker): publish under iklob1/lightapi instead of iklobato/lightapi
…ict fields

Bug fixes
- login_validator exceptions now return 401 instead of 500
- SearchFilter escapes % and _ so search terms are treated as literals
- OrderingFilter with empty whitelist now disables ordering entirely
- PATCH null clears Optional/nullable fields (was silently ignored)
- from_dict fields were silently dropped (missing __annotations__)
- from_dict methods list now enforces HTTP verbs via HttpMethod mixins

Tests
- Update test_login_validator_exception_returns_500 → asserts 401

Documentation
- Complete LightApi() constructor signature in README
- Correct rate_limiter dict keys and scope (auth/login only)
- Document SearchFilter literal matching and OrderingFilter whitelist
- Document PATCH null-clearing semantics for Optional fields
- Document login_validator exception → 401 behavior
- Add CHANGELOG.md
Bugs fixed:

- yaml_loader: Ellipsis class attributes blocked SQLAlchemy instrumentation.
  Fields with no constraints (no Field() in class_attrs) were stored as
  Ellipsis, which SA's map_imperatively silently skips — the column existed
  in _all_columns but never appeared in the mapper, causing NOT NULL
  constraint failures on INSERT.
  Fixed in FieldInfoStripper.strip() (table_mapping.py): also remove Ellipsis
  attributes so SA can install InstrumentedAttribute freely.

- yaml_loader: YAML 'default' value was silently dropped.
  'default' is not in constraint_keys, so filtering pydantic_kwargs discarded
  it. pydantic_kwargs.pop("default", ...) then returned the sentinel,
  making every YAML-defined default invisible to Pydantic.
  Fixed: extract default from the raw extra dict before filtering.

- lightapi.py build_app(): CORS middleware was only applied in run(), not
  build_app(). TestClient and embedded-app users never got CORS headers.
  Fixed: apply StarletteCORSMiddleware in build_app() too.

- auth_checker.py: AllowAny permission (non-dict) did not bypass the backend
  authentication check. An endpoint with permission=AllowAny but a backend
  from defaults still required a valid token.
  Fixed: return None before the backend check when permission_cls is AllowAny
  and is not a per-method dict. permission_cls=None (unset) still triggers
  auth when a backend is configured.

- yaml_loader: dict-style methods ({GET: ..., POST: ...}) did not restrict
  HTTP verbs — _resolve_methods_bases returned (RestEndpoint,) for any dict,
  allowing all five methods.
  Fixed: extract dict keys as the method list and resolve HttpMethod mixins.

- yaml_loader: MetaConfig had no 'table' field, so meta.table in YAML was
  silently ignored. Reflected endpoints could not name their table.
  Fixed: add table field to MetaConfig and pass it through _build_meta_class.
- meta.cache: { ttl: N } — maps to Meta.cache = Cache(ttl=N); endpoint
  GET responses are cached in Redis when available, silently skipped if not.

- meta.serializer: { fields: [...] } or { read: [...], write: [...] } —
  maps to Meta.serializer = Serializer(...); controls which fields appear
  in responses, globally or per HTTP verb.

- Top-level mode: "sync" | "async" — passed to LightApi(mode=...) so async
  engines can be declared entirely from YAML without a Python override.

All three features are wired through _build_meta_class and load_config.
- Add meta.cache, meta.serializer, mode, meta.table to YAML schema
  references in README, configuration guide, and YAML examples
- Add full-featured YAML example (per-method auth + cache + serializer
  + cursor pagination + reflection) to configuration guide
- Rewrite examples/yaml-configuration.md to cover every supported key
- Fix incorrect 'cache is not auto-invalidated' claim in caching.md —
  writes DO invalidate the endpoint's cache prefix
- Add YAML configuration sections to filtering.md and pagination.md
- Fix single-arg Middleware.process() signature in configuration.md
  to the correct (request, response) two-phase form
- Document AllowAny bypassing the auth backend, and login_validator
  exceptions mapping to 401

All 39 YAML blocks parse, all 41 Python blocks compile, 324 tests pass.
A declarative endpoint field with no constraints and no default (e.g.
`price: {type: float}`) was given an Ellipsis class attribute. Ellipsis is
not a FieldInfo, so FieldInfoStripper never removed it, and the leftover
class attribute stopped SQLAlchemy from mapping the column. The column was
created NOT NULL by create_all but its value was silently dropped on INSERT,
so every write to such a field raised a NOT NULL violation.

Leave these fields annotation-only: the annotation alone already makes the
generated schema field required, and the column now maps and persists.

Claude-Session: https://claude.ai/code/session_01LWV5jPTwsAehx2rM7UZjtq
Deploy a declarative CRUD API over an existing database with only a
values.yaml: the chart hands LightApi.from_config the declarative config and
runs it, so teams write no CRUD code.

- ServerCommand (lightapi/server.py) + `lightapi serve` console script +
  `python -m lightapi`: boot a server from LIGHTAPI_CONFIG/HOST/PORT.
- Always-on /healthz route (lightapi/health.py) for orchestrator probes,
  wired once into both run() and build_app().
- Generic Dockerfile: one image, config injected at deploy time.
- charts/lightapi: Deployment (config via ConfigMap, secrets via Secret,
  /healthz liveness+readiness probes), Service, Secret, ServiceAccount,
  optional Ingress and HPA.

Verified end to end on minikube: helm install/upgrade/rollback, config-change
pod roll, secret and existingSecret paths, HPA, method restriction, optimistic
locking, and broken-config CrashLoopBackOff with no traffic served.

Claude-Session: https://claude.ai/code/session_01LWV5jPTwsAehx2rM7UZjtq
- README: env vars (LIGHTAPI_CONFIG/HOST/PORT), a `lightapi serve` note, a
  health-check note, and a Kubernetes (Helm) section.
- docs/deployment/helm.md: full Helm guide with a minikube walkthrough.
- charts/lightapi/README.md: chart usage and a complete values reference.
- docs/deployment/docker.md: align with the real image (lightapi serve +
  LIGHTAPI_CONFIG) instead of the old gunicorn placeholder.
- Add the Helm page to the deployment nav.

Claude-Session: https://claude.ai/code/session_01LWV5jPTwsAehx2rM7UZjtq
Reconcile the two Docker packaging paths. Keep master's published
iklob1/lightapi image, docker/entrypoint.py, and docker-publish workflow as the
container story; keep this branch's Helm chart, /healthz route, and the YAML
column-mapping fix on top. The chart now defaults to iklob1/lightapi.

Conflicts resolved:
- Dockerfile / .dockerignore / docs/deployment/docker.md: took master's.
- lightapi/lightapi.py build_app(): kept master's CORS wiring and added the
  /healthz route via _asgi_routes().

The `lightapi serve` console command stays as a general library CLI alongside
the container entrypoint.

Claude-Session: https://claude.ai/code/session_01LWV5jPTwsAehx2rM7UZjtq
@iklobato
iklobato changed the base branch from master to dev August 1, 2026 22:43
@iklobato
iklobato merged commit b66e906 into dev Aug 1, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants