Changelog support for offline / airgapped deployments
Status: proposal — not implemented. Builds on the changelog system (WAL-9853), which this note assumes as given.
Problem
fetch_changelog_index() / fetch_changelog_release()
(waldur_core/changelog/utils.py) hit
https://docs.waldur.com/latest/changelog/... over HTTPS. The legacy
fallback, get_latest_github_tag(), hits
api.github.com. Both wrap their request in try/except RequestException and
return None on failure, so a deployment with no egress to either host
doesn't crash — /api/version/ just returns {"version": "..."} with no
changelog_summary, no latest_version, no security banner.
That's a silent no-op, not a working feature. It also inverts the risk profile: airgapped deployments — the ones least able to casually check "what changed upstream" some other way — are exactly the ones this degrades for.
Goals
- A deployment with zero outbound access still gets
changelog_summary,security_alert, and/api/changelog/pending/populated, scoped to whatever changelog data it actually has on hand. - Deployments that do have some path to fetch updates (an internal mirror, a periodic sync job, a side-loaded file) can use it without code changes.
- No regression for the common case (direct internet access to docs.waldur.com) — this is additive.
Non-goals
- Making offline deployments learn about releases that happened after their last data refresh, without some operator action. There's no way around that without a network path of some kind, even an indirect one (sneakernet, scheduled sync from a jump host).
- A generic plugin system for changelog data sources. Three concrete sources (below) cover the realistic cases; more can be added later if needed.
Design: three-tier lookup
Insert two sources ahead of the existing HTTP fetch, checked in order, first hit wins:
flowchart TD
start(["fetch_changelog_index() /\nfetch_changelog_release(version)"]) --> local
local{"1. Local override dir\nCHANGELOG_LOCAL_PATH set\nand file exists?"}
local -->|yes| useLocal["Read from disk\n(no cache needed — already local)"]
local -->|no| bundled
bundled{"2. Bundled snapshot\npackaged with this image?"}
bundled -->|yes| useBundled["Read from package data\ncache 1h, same as today"]
bundled -->|no| http
http{"3. HTTP fetch\ndocs.waldur.com reachable?"}
http -->|yes| useHttp["Existing behavior\ncache 1h / 30d"]
http -->|no| none["None\n(the current silent no-op, now the last resort\nrather than the only option)"]
useLocal --> done(["changelog_summary populated"])
useBundled --> done
useHttp --> done
- Local override directory. A path (
WALDUR_CORE["CHANGELOG_LOCAL_PATH"], e.g. mounted from a ConfigMap or a plain volume) checked first, so an operator can refresh data by dropping newerindex.json/releases/{version}.jsonfiles in without a redeploy or code change. No cache layer needed here — reading a local file is already cheap, and it lets an operator's own sync job (cron,rsync, CI artifact) take effect immediately instead of waiting out the 1h cache. - Bundled snapshot.
changelog/index.json+changelog/releases/*.jsonas of build time, shipped as package data inside the image (same treeassemble_changelogalready writes to — no new format). An 8.0.7 image knows everything that was true when 8.0.7 was built. It can't discover an 8.0.9 that shipped later without a new image or tier 1 — that's the fundamental limit of "no network," not a gap in this design. - HTTP fetch. Unchanged — today's behavior, now the last resort instead of the only option.
Each tier already produces data in the same shape (index.json /
per-version release JSON per changelog/schema.json), so
build_changelog_summary(), enrich_entries_with_relevance(),
compute_changelog_impact, and every API view are untouched. This is purely a
sourcing change inside fetch_changelog_index() / fetch_changelog_release().
Alternatives considered
| Option | Trade-off |
|---|---|
Configurable CHANGELOG_BASE_URL only (point HTTP at an internal mirror) |
Simplest code change — one constant becomes a setting. But it's still "HTTP or nothing": an operator with literally zero egress (not even to an internal-only mirror reachable from the pod) gets nothing, and it doesn't help the zero-config case ("this should say something useful out of the box"). |
| Bundled snapshot only (tier 2, no tier 1) | Works out of the box, no operator setup. But there's no way to refresh it short of shipping a new image, even for operators who do have some side-channel (e.g. they already push new images through an internal registry and could push refreshed JSON the same way). |
| Local override only (tier 1, no tier 2) | Fully flexible, but a fresh install has nothing until an operator configures it — worse zero-config behavior than today's HTTP-only design for deployments that do have internet. |
| Three-tier (this proposal) | Covers both the zero-config case (bundled) and the "operator wants control" case (local override), at the cost of one more branch in two already-simple functions. |
CHANGELOG_BASE_URL becoming a setting (first bullet) is worth doing
regardless, orthogonal to tiers — it costs nothing and helps the "internal
mirror instead of docs.waldur.com" case even for deployments that do have
some internal network access.
Concrete changes
waldur_core/changelog/utils.pyfetch_changelog_index()/fetch_changelog_release(version): tryCHANGELOG_LOCAL_PATH(if set), then bundled package data, then HTTP — same public signature, same return shape.CHANGELOG_BASE_URLbecomessettings.WALDUR_CORE.get("CHANGELOG_BASE_URL", "https://docs.waldur.com/latest/changelog").- New
WALDUR_COREsettings:CHANGELOG_LOCAL_PATH(default unset — tier 1 skipped unless configured). - Packaging: bundling the snapshot means the release pipeline that already
writes
changelog/releases/{version}.json(see Release Script Integration) also needs to land in the built package/image — likelyMANIFEST.in/pyproject.tomlpackage-data entry, no new generation step. waldur-helm/waldur-docker-compose: documentCHANGELOG_LOCAL_PATHas an optional mount point, not a mandatory one (existing deployments need no changes) — the packaging-config skill's mandatory-config checklist doesn't apply here.
Open questions
- Does the bundled snapshot need
assemble_changelogto also copy the latest N releases (not just the current one) into package data, so a fresh install'schangelog_summarycan showversions_behind > 1immediately after an offline upgrade skips several versions? Or is "what changed since the version this image replaces" (typically one hop) enough? - Should
compute_changelog_impact's Celery trigger inversion_detailbe skipped when the only available source is the bundled snapshot, since there's nothing new to learn between fixed-at-build-time data refreshes (the 24h re-run in_trigger_impact_analysis_if_neededwould just repeat the same query on unchanged data until the next image)?