Reorganized for new stable/waypoint versioning design.
This commit is contained in:
338
ADDING-APPS.md
338
ADDING-APPS.md
@@ -6,26 +6,65 @@ This guide is for contributors and maintainers who want to create or modify Wild
|
||||
|
||||
Wild Cloud apps are Kubernetes applications packaged as Kustomize configurations with standardized conventions for configuration management, secrets handling, and deployment.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
Each app has a two-level structure: an `app.yaml` meta file at the root, and version-specific files inside `versions/`. Version directories are named by **slot** (typically the major version), not by the full version string. The actual version lives in `manifest.yaml` inside the slot.
|
||||
|
||||
```
|
||||
myapp/
|
||||
├── app.yaml # App identity, latest slot pointer, upgrade routing
|
||||
└── versions/
|
||||
├── 2/ # Current latest slot (manifest.yaml has version: 2.3.1)
|
||||
│ ├── manifest.yaml # Version-specific config (requires, defaultConfig, etc.)
|
||||
│ ├── kustomization.yaml
|
||||
│ └── *.yaml # Kubernetes resource templates
|
||||
└── 1/ # Waypoint slot (only if upgrade routing needs it)
|
||||
├── manifest.yaml
|
||||
├── kustomization.yaml
|
||||
└── *.yaml
|
||||
```
|
||||
|
||||
Most apps have **one** version directory. A second appears only when a waypoint is needed for upgrade routing.
|
||||
|
||||
## Required Files
|
||||
|
||||
Each app directory must contain:
|
||||
|
||||
1. **`manifest.yaml`** - App metadata and configuration schema
|
||||
2. **`kustomization.yaml`** - Kustomize configuration with Wild Cloud labels
|
||||
3. **Resource files** - Kubernetes manifests (deployments, services, ingresses, etc.)
|
||||
1. **`app.yaml`** - App identity, latest slot pointer, and upgrade routing rules
|
||||
2. **`versions/{slot}/manifest.yaml`** - Version-specific configuration schema
|
||||
3. **`versions/{slot}/kustomization.yaml`** - Kustomize configuration with Wild Cloud labels
|
||||
4. **`versions/{slot}/*.yaml`** - Kubernetes resource templates
|
||||
|
||||
## App Manifest (`manifest.yaml`)
|
||||
## App Meta (`app.yaml`)
|
||||
|
||||
The manifest defines the app's metadata, dependencies, configuration schema, and secret requirements.
|
||||
|
||||
This is the contents of an example `manifest.yaml` file for an app named "immich":
|
||||
The `app.yaml` file at the app root defines identity, display info, and upgrade routing. These fields are version-independent.
|
||||
|
||||
```yaml
|
||||
name: immich
|
||||
is: immich
|
||||
description: Immich is a self-hosted photo and video backup solution that allows you to store, manage, and share your media files securely.
|
||||
version: 1.0.0
|
||||
icon: https://immich.app/assets/images/logo.png
|
||||
latest: "1"
|
||||
```
|
||||
|
||||
### App Meta Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `name` | Yes | App identifier (must match directory name) |
|
||||
| `is` | Yes | Unique id for this app. Used for `requires` mapping |
|
||||
| `description` | Yes | Brief app description shown in listings |
|
||||
| `icon` | No | URL to app icon for UI display |
|
||||
| `category` | No | Category (e.g., `infrastructure`) |
|
||||
| `latest` | Yes | Slot name -- directory name under `versions/` (not a version string) |
|
||||
| `upgrade` | No | Upgrade routing rules (see Upgrade Metadata below) |
|
||||
|
||||
## Version Manifest (`versions/{slot}/manifest.yaml`)
|
||||
|
||||
Each version slot contains a `manifest.yaml` with version-specific installation details: dependencies, configuration schema, and secret requirements.
|
||||
|
||||
```yaml
|
||||
version: 1.135.3-1
|
||||
requires:
|
||||
- name: pg
|
||||
alias: db # Use a different reference name in templates
|
||||
@@ -46,21 +85,17 @@ defaultConfig:
|
||||
defaultSecrets:
|
||||
- key: password # Random value will be generated if empty
|
||||
- key: dbUrl
|
||||
default: "postgresql://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?pool=30" # Can reference secrets and config as long as they have been defined before this line. Reference config with {{ .app.? }} and secrets with {{ .secrets.? }}
|
||||
default: "postgresql://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?pool=30"
|
||||
requiredSecrets:
|
||||
- db.password # References postgres app via 'db' alias
|
||||
- redis.auth # References redis app via 'redis' name (no alias)
|
||||
```
|
||||
|
||||
### Manifest Fields
|
||||
### Version Manifest Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `name` | Yes | App identifier (must match directory name) |
|
||||
| `is` | Yes | Unique id for this app. Used for `requires` mapping |
|
||||
| `description` | Yes | Brief app description shown in listings |
|
||||
| `version` | Yes | App version (see Versioning Convention below) |
|
||||
| `icon` | No | URL to app icon for UI display |
|
||||
| `requires` | No | List of dependency apps with optional aliases |
|
||||
| `defaultConfig` | Yes | Default configuration values merged into operator's `config.yaml` |
|
||||
| `defaultSecrets` | No | This app's secrets (no 'default' = auto-generated) |
|
||||
@@ -85,6 +120,27 @@ Wild Cloud uses a two-part version scheme inspired by Debian packaging: `<upstre
|
||||
|
||||
The web UI uses version comparison to detect available updates. If the deployed version differs from the wild-directory version, operators see an update indicator and can apply it from the app detail panel.
|
||||
|
||||
### Slot Naming Convention
|
||||
|
||||
Version directory names are **slot names**, not version strings. The slot is a stable label; the actual version lives in `manifest.yaml` inside the slot.
|
||||
|
||||
**Rules:**
|
||||
- Use the **major version** as the slot name (e.g., `1`, `2`, `5`, `v3`)
|
||||
- Preserve the `v` prefix if the upstream project uses it (e.g., `v1` for cert-manager)
|
||||
- **Never** put packaging revisions (`-1`, `-2`) in directory names
|
||||
- **Never** put minor/patch versions in directory names unless creating a waypoint that needs to be distinct from another slot at the same major version
|
||||
|
||||
**Examples:**
|
||||
|
||||
| App | Slot name | Version in manifest |
|
||||
|-----|-----------|-------------------|
|
||||
| Ghost 5.118.1-2 | `5` | `5.118.1-2` |
|
||||
| cert-manager v1.17.2 | `v1` | `v1.17.2` |
|
||||
| Immich 1.135.3-1 | `1` | `1.135.3-1` |
|
||||
| Traefik v3.4 | `v3` | `v3.4` |
|
||||
|
||||
When bumping versions (upstream or packaging), update files inside the existing slot. Only create a new directory when you need a new waypoint.
|
||||
|
||||
### Upgrade Metadata
|
||||
|
||||
Most apps can upgrade from any version to any other version directly — no special metadata is needed. The `upgrade` field is **optional** and only required when an app has breaking changes that need controlled upgrade paths.
|
||||
@@ -93,76 +149,100 @@ Most apps can upgrade from any version to any other version directly — no spec
|
||||
|
||||
**When you need `upgrade:`** Apps with breaking database schema changes, incompatible config formats, or upstream requirements for sequential version upgrades (e.g., Discourse requires stepping through major versions).
|
||||
|
||||
#### The `upgrade` block
|
||||
#### The `upgrade` block in `app.yaml`
|
||||
|
||||
Upgrade routing rules live in `app.yaml`, centralized for all versions. The system iteratively re-evaluates these rules after each waypoint step.
|
||||
|
||||
```yaml
|
||||
# app.yaml
|
||||
name: myapp
|
||||
latest: "3"
|
||||
upgrade:
|
||||
from:
|
||||
- version: ">=3.5.0" # Can upgrade directly from 3.5.x
|
||||
- version: ">=3.4.0"
|
||||
via: "3.5.3-1" # Must pass through 3.5.x first
|
||||
via: "2" # Must pass through slot "2" first (a waypoint)
|
||||
- version: "<3.4.0"
|
||||
blocked: true
|
||||
notes: "Requires sequential major upgrades. See upstream docs."
|
||||
preUpgrade:
|
||||
backup: required # "none", "recommended", or "required"
|
||||
```
|
||||
|
||||
Note: `latest` and `via` are **slot names** (directory names), not version strings. The system reads the actual version from the manifest inside each slot.
|
||||
|
||||
Version-specific upgrade behavior (migrations, configMigrations) lives in the version's `manifest.yaml`:
|
||||
|
||||
```yaml
|
||||
# versions/3/manifest.yaml
|
||||
version: 3.6.0
|
||||
upgrade:
|
||||
migrations:
|
||||
pre:
|
||||
- migrations/pre-deploy.yaml # K8s Job YAML paths relative to app dir
|
||||
- migrations/pre-deploy.yaml # K8s Job YAML paths relative to version dir
|
||||
post:
|
||||
- migrations/post-deploy.yaml
|
||||
configMigrations:
|
||||
oldKeyName: newKeyName # Renames config keys automatically
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
**`app.yaml` upgrade fields:**
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `from` | List of version constraint rules, evaluated in order (first match wins) |
|
||||
| `from[].version` | Version constraint: `>=`, `>`, `<=`, `<`, `=`, or `>0` (matches any) |
|
||||
| `from[].via` | Waypoint version in `.versions/` — upgrade must pass through this version first |
|
||||
| `from[].via` | Waypoint slot name in `versions/` — upgrade must pass through this slot first |
|
||||
| `from[].blocked` | If true, upgrade is blocked with an error message |
|
||||
| `from[].notes` | Human-readable message shown when blocked or as context |
|
||||
| `preUpgrade.backup` | Backup requirement: `"required"` blocks upgrade until backup is done, `"recommended"` shows a warning |
|
||||
| `migrations.pre` | K8s Job YAMLs to run before deploying each version step |
|
||||
| `migrations.post` | K8s Job YAMLs to run after deploying each version step |
|
||||
|
||||
**Version `manifest.yaml` upgrade fields:**
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `migrations.pre` | K8s Job YAMLs to run before deploying this version step |
|
||||
| `migrations.post` | K8s Job YAMLs to run after deploying this version step |
|
||||
| `configMigrations` | Map of old config key → new config key for automatic renaming |
|
||||
|
||||
#### Waypoint versions (`.versions/` directory)
|
||||
#### Waypoint versions
|
||||
|
||||
When an upgrade requires passing through an intermediate version, store that version's files in a `.versions/` subdirectory:
|
||||
When an upgrade requires passing through an intermediate version, add that version's files as a new slot in the `versions/` directory alongside the latest:
|
||||
|
||||
```
|
||||
myapp/
|
||||
├── manifest.yaml # Latest version (e.g., 3.6.0)
|
||||
├── kustomization.yaml
|
||||
├── *.yaml
|
||||
└── .versions/
|
||||
└── 3.5.3-1/ # Waypoint version
|
||||
├── manifest.yaml # version: 3.5.3-1 (with its own upgrade rules)
|
||||
├── app.yaml # Routing rules + latest pointer
|
||||
└── versions/
|
||||
├── 3/ # Latest slot (version: 3.6.0)
|
||||
│ ├── manifest.yaml
|
||||
│ ├── kustomization.yaml
|
||||
│ └── *.yaml
|
||||
└── 2/ # Waypoint slot (version: 2.8.0)
|
||||
├── manifest.yaml
|
||||
├── kustomization.yaml
|
||||
└── *.yaml
|
||||
```
|
||||
|
||||
Each waypoint is a complete app package. The system computes a chain automatically — for example, upgrading from 3.4.0 to 3.6.0 might produce: `3.4.0 → 3.5.3-1 → 3.6.0`.
|
||||
Each waypoint is a complete app package. The system computes a chain automatically — for example, upgrading from 2.3.0 to 3.6.0 might produce: `2.3.0 → 2.8.0 (slot "2") → 3.6.0 (slot "3")`.
|
||||
|
||||
**Creating a waypoint:**
|
||||
**Creating a waypoint:** The current latest slot becomes the waypoint (leave it in place), then create a new slot for the new major version:
|
||||
|
||||
```bash
|
||||
mkdir -p wild-directory/myapp/.versions
|
||||
rsync -a --exclude='.versions' wild-directory/myapp/ wild-directory/myapp/.versions/3.5.3-1/
|
||||
# Now update wild-directory/myapp/manifest.yaml to the new version + upgrade rules
|
||||
# Current slot "2" (with version 2.8.0) stays as a waypoint
|
||||
# Create the new slot for the next major version
|
||||
mkdir -p wild-directory/myapp/versions/3
|
||||
# ... add manifest.yaml, kustomization.yaml, *.yaml for 3.0.0 ...
|
||||
# Update app.yaml: set latest to "3", add upgrade routing rules with via: "2"
|
||||
```
|
||||
|
||||
#### Migration jobs
|
||||
|
||||
Migration jobs are K8s Job manifests that run database migrations or other one-time tasks during an upgrade step. They must be **idempotent** (safe to re-run) since a failed upgrade might be retried.
|
||||
|
||||
Place migration job files in the waypoint or app directory and reference them from the `migrations` field:
|
||||
Place migration job files in the version slot directory and reference them from that version's `manifest.yaml`:
|
||||
|
||||
```yaml
|
||||
# migrations/db-migrate.yaml
|
||||
# versions/3/migrations/db-migrate.yaml
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
@@ -177,17 +257,18 @@ spec:
|
||||
command: ["bundle", "exec", "rake", "db:migrate"]
|
||||
```
|
||||
|
||||
Each migration step belongs to the version that introduces the breaking change. If version 3.6.0 requires a schema migration, the migration lives in the 3.6.0 manifest (or its waypoint), not on 3.5.x.
|
||||
Each migration step belongs to the version that introduces the breaking change. If version 3.6.0 requires a schema migration, the migration lives in the slot `3/` directory.
|
||||
|
||||
#### Example: simple app with waypoint
|
||||
|
||||
```yaml
|
||||
# myapp/manifest.yaml (version 2.0.0)
|
||||
version: 2.0.0
|
||||
# myapp/app.yaml
|
||||
name: myapp
|
||||
latest: "2"
|
||||
upgrade:
|
||||
from:
|
||||
- version: ">=1.0.0"
|
||||
via: "1.0.0-1"
|
||||
via: "1"
|
||||
- version: "<1.0.0"
|
||||
blocked: true
|
||||
notes: "Versions before 1.0.0 are not supported"
|
||||
@@ -195,7 +276,171 @@ upgrade:
|
||||
backup: recommended
|
||||
```
|
||||
|
||||
This creates a 2-step upgrade path: `1.x → 1.0.0-1 → 2.0.0`. The waypoint at `.versions/1.0.0-1/` has no `upgrade` block, so it accepts any version directly.
|
||||
This creates a 2-step upgrade path: `1.x → slot "1" (e.g., version 1.0.0-1) → slot "2" (e.g., version 2.0.0)`. The waypoint at `versions/1/` is a complete app package used as an intermediate step.
|
||||
|
||||
### Adding a New Version
|
||||
|
||||
When an upstream app releases a new version, you update the Wild Directory package to track it. The process depends on whether the new version has breaking changes.
|
||||
|
||||
#### Simple version bump (no breaking changes)
|
||||
|
||||
Most version updates are simple — update the container image tag, adjust any changed config, and update the version in `manifest.yaml`. No directory rename or `app.yaml` change needed.
|
||||
|
||||
```bash
|
||||
# 1. Update files inside the existing slot
|
||||
# - Bump version in manifest.yaml (e.g., 1.2.0 → 1.3.0)
|
||||
# - Update container image tags in deployment YAMLs
|
||||
# - Adjust defaultConfig if the new version adds/changes config
|
||||
vi wild-directory/myapp/versions/1/manifest.yaml
|
||||
vi wild-directory/myapp/versions/1/deployment.yaml
|
||||
|
||||
# 2. app.yaml doesn't change — latest still points to slot "1"
|
||||
|
||||
# 3. Test
|
||||
wild app add myapp && wild app deploy myapp
|
||||
```
|
||||
|
||||
The directory structure stays the same:
|
||||
```
|
||||
myapp/
|
||||
├── app.yaml # latest: "1" (unchanged)
|
||||
└── versions/
|
||||
└── 1/
|
||||
├── manifest.yaml # version: 1.3.0 (bumped)
|
||||
└── *.yaml
|
||||
```
|
||||
|
||||
#### Version bump with breaking changes (waypoint required)
|
||||
|
||||
When the new version can't safely upgrade from all previous versions — e.g., a database schema change requires stepping through an intermediate version — create a new slot for the new major version, keep the old slot as a waypoint, and add routing rules.
|
||||
|
||||
```bash
|
||||
# 1. The current slot (2/) becomes a waypoint — leave it in place
|
||||
# 2. Create a new slot for the new major version
|
||||
mkdir -p wild-directory/myapp/versions/3
|
||||
# ... add new version files (manifest.yaml, kustomization.yaml, *.yaml) ...
|
||||
|
||||
# 3. Update app.yaml: point latest to new slot, add upgrade routing rules
|
||||
```
|
||||
|
||||
```yaml
|
||||
# app.yaml
|
||||
name: myapp
|
||||
latest: "3"
|
||||
upgrade:
|
||||
from:
|
||||
- version: ">=2.5.0" # 2.5.x can upgrade directly
|
||||
- version: ">=2.0.0"
|
||||
via: "2" # Older 2.x must pass through slot 2 first
|
||||
- version: "<2.0.0"
|
||||
blocked: true
|
||||
notes: "Upgrade to 2.x first. See upstream migration guide."
|
||||
preUpgrade:
|
||||
backup: recommended
|
||||
```
|
||||
|
||||
The resulting directory:
|
||||
```
|
||||
myapp/
|
||||
├── app.yaml # latest: "3", upgrade routing rules
|
||||
└── versions/
|
||||
├── 3/ # New latest (manifest.yaml has version: 3.0.0)
|
||||
│ ├── manifest.yaml
|
||||
│ └── *.yaml
|
||||
└── 2/ # Waypoint (manifest.yaml has version: 2.5.0)
|
||||
├── manifest.yaml
|
||||
└── *.yaml
|
||||
```
|
||||
|
||||
#### Version bump with database migrations
|
||||
|
||||
When the new version requires a schema migration (e.g., `ALTER TABLE`, new indexes, data transformations), add migration job files to the slot directory and reference them from the version's `manifest.yaml`. Since this is a minor/patch update within the same major version, update files in-place in the existing slot.
|
||||
|
||||
```bash
|
||||
# 1. Update files inside the existing slot
|
||||
# - Bump version in manifest.yaml (e.g., 2.0.0 → 2.1.0)
|
||||
# - Update container image tags in deployment YAMLs
|
||||
vi wild-directory/myapp/versions/2/manifest.yaml
|
||||
vi wild-directory/myapp/versions/2/deployment.yaml
|
||||
|
||||
# 2. Add migration job files
|
||||
mkdir -p wild-directory/myapp/versions/2/migrations
|
||||
```
|
||||
|
||||
Create the migration job:
|
||||
```yaml
|
||||
# versions/2/migrations/pre-deploy.yaml
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: myapp-migrate-2-1-0
|
||||
namespace: myapp
|
||||
spec:
|
||||
backoffLimit: 3
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 999
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: migrate
|
||||
image: myapp:2.1.0
|
||||
command: ["bundle", "exec", "rake", "db:migrate"]
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: [ALL]
|
||||
env:
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: myapp-secrets
|
||||
key: dbUrl
|
||||
```
|
||||
|
||||
Reference the migration in the version manifest:
|
||||
```yaml
|
||||
# versions/2/manifest.yaml
|
||||
version: 2.1.0
|
||||
upgrade:
|
||||
migrations:
|
||||
pre:
|
||||
- migrations/pre-deploy.yaml
|
||||
defaultConfig:
|
||||
# ...
|
||||
```
|
||||
|
||||
`app.yaml` doesn't change — `latest` still points to slot `"2"`.
|
||||
|
||||
Migration jobs must be **idempotent** — safe to re-run if an upgrade is retried after a partial failure. Use `CREATE IF NOT EXISTS`, `ALTER TABLE IF NOT EXISTS`, etc.
|
||||
|
||||
**Pre vs post migrations:**
|
||||
- `pre` — runs before deploying the new version's manifests (schema changes that the new code needs)
|
||||
- `post` — runs after deploying (data backfills, cleanup that the old code didn't need)
|
||||
|
||||
#### Version bump with config key renames
|
||||
|
||||
When a version renames config keys (e.g., `dbHost` → `db.host`), use `configMigrations` to automatically rename them during upgrade:
|
||||
|
||||
```yaml
|
||||
# versions/2/manifest.yaml
|
||||
version: 2.1.0
|
||||
upgrade:
|
||||
configMigrations:
|
||||
dbHost: db.host
|
||||
dbPort: db.port
|
||||
dbName: db.name
|
||||
defaultConfig:
|
||||
db:
|
||||
host: "{{ .apps.pg.host }}"
|
||||
port: "5432"
|
||||
name: myapp
|
||||
```
|
||||
|
||||
The system renames the keys in the instance's `config.yaml` before recompiling templates with the new version.
|
||||
|
||||
### Dependency Configuration
|
||||
|
||||
@@ -666,9 +911,16 @@ labels:
|
||||
|
||||
Before submitting a new or modified app, verify:
|
||||
|
||||
- [ ] **Manifest**
|
||||
- [ ] **App Meta (`app.yaml`)**
|
||||
- [ ] `name` matches directory name
|
||||
- [ ] All required fields present (`name`, `description`, `version`, `defaultConfig`)
|
||||
- [ ] `latest` points to a valid version in `versions/`
|
||||
- [ ] `description` present
|
||||
- [ ] `upgrade` rules correct (if applicable)
|
||||
|
||||
- [ ] **Version Manifest (`versions/{slot}/manifest.yaml`)**
|
||||
- [ ] `version` field present with full version string (e.g., `1.135.3-1`)
|
||||
- [ ] Slot directory follows naming convention (major version, e.g., `1`, `v1`)
|
||||
- [ ] All required fields present (`version`, `defaultConfig`)
|
||||
- [ ] All template variables defined in `defaultConfig`
|
||||
- [ ] `defaultSecrets` uses maps with 'key' and 'default' attributes
|
||||
- [ ] `requiredSecrets` references use `<app-ref>.<key>` format
|
||||
|
||||
692
admin/docs/design.md
Normal file
692
admin/docs/design.md
Normal file
@@ -0,0 +1,692 @@
|
||||
# Wild Directory Versioning and Upgrade System
|
||||
|
||||
Design specification for how Wild Cloud packages its third-party applications,
|
||||
tracks versions, and manages upgrades between versions.
|
||||
|
||||
## Problem
|
||||
|
||||
Wild Cloud packages third-party applications as Kustomize templates. Each
|
||||
application needs:
|
||||
|
||||
1. **Identity** that doesn't change between versions (name, description, icon)
|
||||
2. **Version-specific files** (k8s manifests, config schema, secrets schema)
|
||||
3. **Upgrade routing** when breaking changes prevent direct version jumps
|
||||
4. A clear, low-friction workflow for maintainers bumping versions
|
||||
|
||||
The system must handle both simple apps (any version replaces any other) and
|
||||
complex apps (database migrations, mandatory stepping stones between major
|
||||
versions).
|
||||
|
||||
## Design
|
||||
|
||||
### Two-level structure: `app.yaml` + `versions/`
|
||||
|
||||
Each app in the Wild Directory has a root-level `app.yaml` for identity and a
|
||||
`versions/` directory containing one or more version slots:
|
||||
|
||||
```
|
||||
ghost/
|
||||
+-- app.yaml
|
||||
+-- versions/
|
||||
+-- 5/
|
||||
+-- manifest.yaml
|
||||
+-- kustomization.yaml
|
||||
+-- deployment.yaml
|
||||
+-- ...
|
||||
```
|
||||
|
||||
**`app.yaml`** holds version-independent fields: name, description, icon,
|
||||
category, the `latest` pointer, and upgrade routing rules. These are facts
|
||||
about the app itself, not about any particular release.
|
||||
|
||||
**`versions/{slot}/manifest.yaml`** holds version-specific fields: the precise
|
||||
version string, dependency declarations, default config, default secrets,
|
||||
deploy configuration, and per-version migration jobs. These are facts about a
|
||||
particular release.
|
||||
|
||||
When the API installs an app, it reads both files and merges them. The
|
||||
installed manifest in the instance data directory contains the complete picture
|
||||
(name, description, icon, version, config, etc.).
|
||||
|
||||
### Version slots vs version strings
|
||||
|
||||
A version directory is a **slot**, not a precise version identifier.
|
||||
|
||||
The directory name is a stable label chosen by the maintainer. The actual
|
||||
version string lives in `manifest.yaml` inside the directory. These are
|
||||
intentionally decoupled:
|
||||
|
||||
| Concept | Where it lives | Example |
|
||||
|---------|---------------|---------|
|
||||
| Slot name | Directory name under `versions/` | `5` |
|
||||
| Actual version | `version` field in `manifest.yaml` | `5.118.1-2` |
|
||||
| Latest pointer | `latest` field in `app.yaml` | `5` |
|
||||
| Waypoint pointer | `via` field in upgrade rules | `2` |
|
||||
|
||||
The slot name should be the simplest stable identifier that distinguishes it
|
||||
from other slots. For semver apps, use the major version (`1`, `2`, `3`). For
|
||||
apps with non-semver schemes, use whatever upstream version boundary makes
|
||||
sense (`5`, `v4`, etc.).
|
||||
|
||||
**Packaging revisions** (`-1`, `-2`, etc.) never appear in directory names.
|
||||
They only appear in `manifest.yaml`'s `version` field. A packaging revision
|
||||
is a Wild Cloud-side fix (template improvement, security context change, config
|
||||
restructure) that doesn't change the upstream software.
|
||||
|
||||
**Minor and patch versions** also don't require new directories unless they
|
||||
introduce a breaking change that requires a waypoint. Updating Ghost from
|
||||
5.118.1 to 5.119.0 means editing files inside `versions/5/` and bumping the
|
||||
`version` field. The directory stays the same.
|
||||
|
||||
This follows how established package systems work:
|
||||
|
||||
| System | Directory/file identity | Where version lives |
|
||||
|--------|------------------------|-------------------|
|
||||
| Debian source packages | Package name (stable) | `debian/changelog` |
|
||||
| Helm charts | Chart name directory | `Chart.yaml` |
|
||||
| Homebrew | Formula file per package | Version attribute in file |
|
||||
| Nix packages | Package name directory | Derivation attribute |
|
||||
| FreeBSD ports | Port name directory | `Makefile` variable |
|
||||
| **Wild Cloud** | **Slot directory** | **`manifest.yaml`** |
|
||||
|
||||
Wild Directory is a source package collection (templates compiled at install
|
||||
time), not an artifact repository (pre-built binaries stored per version). The
|
||||
source package pattern is the right fit.
|
||||
|
||||
### When directories are created and destroyed
|
||||
|
||||
**Most apps have exactly one version directory.** This is the common case for
|
||||
apps where any version can replace any other (Ghost, Redis, most stateless
|
||||
services).
|
||||
|
||||
**A second directory appears only when a waypoint is needed** -- when a
|
||||
breaking change means some installed versions can't jump directly to the
|
||||
latest and must pass through an intermediate version first.
|
||||
|
||||
**A directory is removed when it's no longer needed as a waypoint.** If
|
||||
version `2/` was a waypoint for upgrading from 1.x to 3.x, but you later
|
||||
decide to drop support for 1.x entirely, you can remove `2/` and update the
|
||||
routing rules to block `<2.0.0`.
|
||||
|
||||
### `app.yaml` specification
|
||||
|
||||
```yaml
|
||||
# Required
|
||||
name: ghost # Must match directory name
|
||||
is: ghost # Unique type id, used for `requires` matching
|
||||
description: "Ghost is a..." # Shown in app listings
|
||||
latest: "5" # Slot name pointing to a directory in versions/
|
||||
|
||||
# Optional
|
||||
icon: "https://..." # URL to app icon
|
||||
category: infrastructure # Category for filtering
|
||||
|
||||
# Optional -- only needed for apps with breaking upgrade paths
|
||||
upgrade:
|
||||
from:
|
||||
- version: ">=3.0.0" # Constraint against installed version
|
||||
- version: ">=2.0.0"
|
||||
via: "2" # Slot name of waypoint directory
|
||||
- version: "<2.0.0"
|
||||
blocked: true
|
||||
notes: "Upgrade to 2.x first"
|
||||
preUpgrade:
|
||||
backup: recommended # "none", "recommended", or "required"
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `name` | string | yes | App identifier, must match directory name |
|
||||
| `is` | string | yes | Type id for dependency matching |
|
||||
| `description` | string | yes | Human-readable description |
|
||||
| `latest` | string | yes | Slot name -- directory name under `versions/` |
|
||||
| `icon` | string | no | URL to icon image |
|
||||
| `category` | string | no | Grouping category (e.g. `infrastructure`) |
|
||||
| `upgrade` | object | no | Routing rules for version upgrades |
|
||||
| `upgrade.from` | list | no | Ordered list of version constraint rules |
|
||||
| `upgrade.from[].version` | string | yes | Version constraint: `>=`, `>`, `<=`, `<`, `=`, or `>0` |
|
||||
| `upgrade.from[].via` | string | no | Slot name of waypoint to pass through |
|
||||
| `upgrade.from[].blocked` | bool | no | If true, upgrade is blocked |
|
||||
| `upgrade.from[].notes` | string | no | Human-readable message |
|
||||
| `upgrade.preUpgrade` | object | no | Pre-upgrade requirements |
|
||||
| `upgrade.preUpgrade.backup` | string | no | `"none"`, `"recommended"`, or `"required"` |
|
||||
|
||||
**`latest` is a slot name, not a version string.** It tells the API which
|
||||
directory to look in. The actual version is read from the manifest inside that
|
||||
directory.
|
||||
|
||||
**`via` is also a slot name.** It tells the upgrade planner which waypoint
|
||||
directory to route through.
|
||||
|
||||
### Version `manifest.yaml` specification
|
||||
|
||||
```yaml
|
||||
# Required
|
||||
version: 5.118.1-2
|
||||
|
||||
# Optional
|
||||
requires:
|
||||
- name: pg
|
||||
alias: db
|
||||
- name: redis
|
||||
defaultConfig:
|
||||
namespace: ghost
|
||||
domain: ghost.{{ .cloud.domain }}
|
||||
# ...
|
||||
defaultSecrets:
|
||||
- key: password
|
||||
- key: dbUrl
|
||||
default: "postgresql://..."
|
||||
requiredSecrets:
|
||||
- db.password
|
||||
deploy:
|
||||
# ...
|
||||
scripts:
|
||||
# ...
|
||||
|
||||
# Optional -- only when this version has step-specific upgrade behavior
|
||||
upgrade:
|
||||
migrations:
|
||||
pre:
|
||||
- migrations/pre-deploy.yaml
|
||||
post:
|
||||
- migrations/post-deploy.yaml
|
||||
configMigrations:
|
||||
oldKey: new.key
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `version` | string | yes | Precise version string (e.g. `5.118.1-2`) |
|
||||
| `requires` | list | no | Dependencies with optional aliases |
|
||||
| `defaultConfig` | map | yes | Configuration schema merged into instance config |
|
||||
| `defaultSecrets` | list | no | App's own secrets |
|
||||
| `requiredSecrets` | list | no | Secrets needed from dependencies |
|
||||
| `deploy` | object | no | Deployment behavior (CRDs, phases, etc.) |
|
||||
| `scripts` | list | no | Operational scripts |
|
||||
| `upgrade.migrations.pre` | list | no | K8s Job YAMLs to run before deploying this version |
|
||||
| `upgrade.migrations.post` | list | no | K8s Job YAMLs to run after deploying this version |
|
||||
| `upgrade.configMigrations` | map | no | Old config key -> new config key renames |
|
||||
|
||||
**Note the field split:** Identity and routing live in `app.yaml`. Installation
|
||||
details live here. The version manifest never contains `name`, `is`,
|
||||
`description`, `icon`, `category`, or `upgrade.from` routing rules.
|
||||
|
||||
### Versioning convention
|
||||
|
||||
Wild Cloud uses a two-part version scheme: `<upstream>-<revision>`.
|
||||
|
||||
- **Upstream** tracks the third-party software version: `5.118.1`, `v4.0.18`,
|
||||
`1.135.3`
|
||||
- **Revision** tracks Wild Cloud packaging changes: `-1`, `-2`, etc.
|
||||
|
||||
A version without a revision suffix (e.g., `5.118.1`) is the initial
|
||||
packaging. Revision `-1` is the first packaging fix that doesn't change
|
||||
upstream software.
|
||||
|
||||
This is the same convention Debian uses for its source packages.
|
||||
|
||||
## Upgrade system
|
||||
|
||||
### The 90% case: no routing rules needed
|
||||
|
||||
Most apps can upgrade from any version to any other directly. The app has no
|
||||
`upgrade` block in `app.yaml`. When the API detects a version mismatch between
|
||||
installed and latest, it produces a single-step plan: install the latest
|
||||
version over the current one.
|
||||
|
||||
### The 10% case: routing rules in `app.yaml`
|
||||
|
||||
Apps with breaking changes between versions need routing rules. These live in
|
||||
`app.yaml`, centralized for all versions. The rules are evaluated iteratively:
|
||||
|
||||
1. Compare installed version against `upgrade.from` rules (first match wins)
|
||||
2. If the matching rule has `via`, step to that waypoint
|
||||
3. Re-evaluate the same rules with the waypoint's version as the new
|
||||
"installed" version
|
||||
4. Repeat until reaching latest or hitting a block
|
||||
|
||||
This is iterative re-evaluation, not recursive descent into waypoint
|
||||
manifests. The routing logic lives in one place (`app.yaml`), not scattered
|
||||
across version manifests.
|
||||
|
||||
### Rule evaluation
|
||||
|
||||
Rules are evaluated in order. First match wins. This means more-specific
|
||||
rules must come before less-specific ones:
|
||||
|
||||
```yaml
|
||||
upgrade:
|
||||
from:
|
||||
- version: ">=3.0.0" # Already past the breaking change, direct
|
||||
- version: ">=2.0.0"
|
||||
via: "2" # Must step through 2.x waypoint first
|
||||
- version: "<2.0.0"
|
||||
blocked: true # Too old, no supported path
|
||||
```
|
||||
|
||||
An installed version of `2.5.0` matches `>=2.0.0`, steps to waypoint `2/`
|
||||
(which contains, say, version `2.8.0`). Re-evaluation with `2.8.0` matches
|
||||
`>=2.0.0` again... but this time the waypoint IS the current version, so the
|
||||
visited-set check catches the cycle.
|
||||
|
||||
To avoid this, ensure a rule exists that matches post-waypoint versions
|
||||
and routes them directly:
|
||||
|
||||
```yaml
|
||||
upgrade:
|
||||
from:
|
||||
- version: ">=2.5.0" # Post-waypoint versions go direct
|
||||
- version: ">=2.0.0"
|
||||
via: "2" # Pre-waypoint versions step through
|
||||
- version: "<2.0.0"
|
||||
blocked: true
|
||||
```
|
||||
|
||||
After stepping to waypoint `2/` (version `2.8.0`), re-evaluation matches
|
||||
`>=2.5.0` (no `via`), which means direct upgrade to latest.
|
||||
|
||||
### Version constraints
|
||||
|
||||
Supported operators: `>=`, `>`, `<=`, `<`, `=`, and the special `>0` (matches
|
||||
any version). Constraints compare major.minor.patch numerically. The packaging
|
||||
revision is ignored in constraint matching so `>=5.118.0` matches `5.118.1-2`.
|
||||
|
||||
### Waypoints
|
||||
|
||||
A waypoint is a version slot that exists solely as a stepping stone in an
|
||||
upgrade path. It contains a complete app package (manifest, kustomize, k8s
|
||||
resources) that can be installed and run.
|
||||
|
||||
Waypoints are created when an upstream app has breaking changes that require
|
||||
sequential upgrades. For example, Discourse requires stepping through each
|
||||
major version. A database schema migration might require running version N
|
||||
before jumping to version N+2.
|
||||
|
||||
Waypoint directories are referenced by slot name in `via` fields:
|
||||
|
||||
```yaml
|
||||
# app.yaml
|
||||
latest: "3"
|
||||
upgrade:
|
||||
from:
|
||||
- version: ">=2.5.0"
|
||||
- version: ">=2.0.0"
|
||||
via: "2"
|
||||
```
|
||||
|
||||
```
|
||||
myapp/
|
||||
+-- app.yaml
|
||||
+-- versions/
|
||||
+-- 3/ # Latest (version: 3.0.0)
|
||||
+-- 2/ # Waypoint (version: 2.8.0)
|
||||
```
|
||||
|
||||
### Per-version migrations
|
||||
|
||||
Migration jobs (database schema changes, data transformations) are version-
|
||||
specific, not app-level. They live in the version's directory and are
|
||||
referenced from that version's `manifest.yaml`:
|
||||
|
||||
```yaml
|
||||
# versions/3/manifest.yaml
|
||||
version: 3.0.0
|
||||
upgrade:
|
||||
migrations:
|
||||
pre:
|
||||
- migrations/pre-deploy.yaml
|
||||
post:
|
||||
- migrations/post-deploy.yaml
|
||||
```
|
||||
|
||||
```
|
||||
versions/3/
|
||||
+-- manifest.yaml
|
||||
+-- migrations/
|
||||
| +-- pre-deploy.yaml # K8s Job: runs before deploying 3.0.0
|
||||
| +-- post-deploy.yaml # K8s Job: runs after deploying 3.0.0
|
||||
+-- kustomization.yaml
|
||||
+-- deployment.yaml
|
||||
+-- ...
|
||||
```
|
||||
|
||||
Migration jobs must be idempotent (safe to re-run on retry). Use
|
||||
`CREATE IF NOT EXISTS`, `ALTER TABLE IF NOT EXISTS`, etc.
|
||||
|
||||
**Pre-migrations** run before deploying the new version's manifests. Use for
|
||||
schema changes the new code depends on.
|
||||
|
||||
**Post-migrations** run after deploying. Use for data backfills or cleanup
|
||||
the old code didn't need.
|
||||
|
||||
### Config migrations
|
||||
|
||||
When a version renames config keys, `configMigrations` in the version manifest
|
||||
tells the system to rename them automatically in the instance's `config.yaml`
|
||||
before recompiling templates:
|
||||
|
||||
```yaml
|
||||
# versions/2/manifest.yaml
|
||||
version: 2.0.0
|
||||
upgrade:
|
||||
configMigrations:
|
||||
dbHost: db.host
|
||||
dbPort: db.port
|
||||
```
|
||||
|
||||
## API path resolution
|
||||
|
||||
The API resolves app paths through `resolveAppDir()`:
|
||||
|
||||
1. Read `app.yaml` from the app root
|
||||
2. Use `latest` (or a specific requested slot) to find `versions/{slot}/`
|
||||
3. Verify `manifest.yaml` exists in that directory
|
||||
4. Return the directory path and parsed `AppMeta`
|
||||
|
||||
For old-style apps (no `app.yaml`, `manifest.yaml` at root), the function
|
||||
falls back to the legacy path. This provides backward compatibility during
|
||||
migration.
|
||||
|
||||
When installing, `applyMeta()` merges identity fields from `app.yaml` onto
|
||||
the version manifest:
|
||||
|
||||
```
|
||||
app.yaml + versions/5/manifest.yaml = installed manifest.yaml
|
||||
(name, is, desc, (version, requires, (complete picture)
|
||||
icon, category) defaultConfig, ...)
|
||||
```
|
||||
|
||||
The installed manifest in the instance data directory always contains all
|
||||
fields because the instance needs the complete picture -- it doesn't have
|
||||
access to the Wild Directory's `app.yaml` at runtime.
|
||||
|
||||
## Drift detection
|
||||
|
||||
The API detects when an installed app's version differs from the Wild
|
||||
Directory's latest. For new-style apps:
|
||||
|
||||
1. Read `app.yaml` to get the `latest` slot name
|
||||
2. Read `versions/{latest}/manifest.yaml` to get the actual version string
|
||||
3. Compare against the installed manifest's version
|
||||
|
||||
If they differ, the app is marked as having source drift with the available
|
||||
version noted. The upgrade planner then computes whether an upgrade path
|
||||
exists and how many steps it requires.
|
||||
|
||||
## Maintainer workflows
|
||||
|
||||
### Simple version bump
|
||||
|
||||
The app upstream releases a new version with no breaking changes. Edit files
|
||||
in-place:
|
||||
|
||||
```bash
|
||||
# 1. Update files inside the existing slot
|
||||
vi wild-directory/ghost/versions/5/manifest.yaml # bump version: 5.119.0
|
||||
vi wild-directory/ghost/versions/5/deployment.yaml # update image tag
|
||||
|
||||
# 2. app.yaml doesn't change -- latest still points to "5"
|
||||
|
||||
# 3. Test
|
||||
wild app add ghost && wild app deploy ghost
|
||||
```
|
||||
|
||||
No directory created, renamed, or deleted. No `app.yaml` change.
|
||||
|
||||
### Packaging fix
|
||||
|
||||
A Wild Cloud template fix, no upstream change:
|
||||
|
||||
```bash
|
||||
# 1. Bump packaging revision in manifest
|
||||
vi wild-directory/ghost/versions/5/manifest.yaml # version: 5.118.1-3
|
||||
|
||||
# 2. Fix whatever needs fixing
|
||||
vi wild-directory/ghost/versions/5/deployment.yaml
|
||||
|
||||
# 3. app.yaml doesn't change
|
||||
```
|
||||
|
||||
### Breaking upstream version (new waypoint)
|
||||
|
||||
The app upstream releases a major version with schema changes:
|
||||
|
||||
```bash
|
||||
# 1. Current slot becomes a waypoint -- leave it in place
|
||||
# versions/2/ stays, containing version 2.8.0
|
||||
|
||||
# 2. Create the new slot
|
||||
mkdir -p wild-directory/myapp/versions/3
|
||||
# ... add manifest.yaml, kustomization.yaml, *.yaml for 3.0.0 ...
|
||||
|
||||
# 3. Update app.yaml
|
||||
```
|
||||
|
||||
```yaml
|
||||
# app.yaml
|
||||
name: myapp
|
||||
latest: "3"
|
||||
upgrade:
|
||||
from:
|
||||
- version: ">=2.5.0" # Post-waypoint, direct
|
||||
- version: ">=2.0.0"
|
||||
via: "2" # Step through waypoint
|
||||
- version: "<2.0.0"
|
||||
blocked: true
|
||||
```
|
||||
|
||||
### Adding a new app
|
||||
|
||||
```bash
|
||||
mkdir -p wild-directory/newapp/versions/1
|
||||
|
||||
# Create app.yaml
|
||||
cat > wild-directory/newapp/app.yaml <<EOF
|
||||
name: newapp
|
||||
is: newapp
|
||||
description: My new application
|
||||
latest: "1"
|
||||
EOF
|
||||
|
||||
# Create version manifest + k8s resources in versions/1/
|
||||
# ... manifest.yaml, kustomization.yaml, deployment.yaml, etc.
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Ghost (simple app, one slot)
|
||||
|
||||
```
|
||||
ghost/
|
||||
+-- app.yaml
|
||||
| name: ghost
|
||||
| is: ghost
|
||||
| description: Ghost is a powerful app for...
|
||||
| icon: https://...
|
||||
| latest: "5"
|
||||
+-- versions/
|
||||
+-- 5/
|
||||
+-- manifest.yaml # version: 5.118.1-2
|
||||
+-- kustomization.yaml
|
||||
+-- deployment.yaml
|
||||
+-- service.yaml
|
||||
+-- ingress.yaml
|
||||
+-- namespace.yaml
|
||||
+-- pvc.yaml
|
||||
+-- db-init-job.yaml
|
||||
```
|
||||
|
||||
Upgrading from 5.100.0 to 5.118.1-2: single step, no routing rules needed.
|
||||
|
||||
### e2e-test-app (two slots, waypoint)
|
||||
|
||||
```
|
||||
e2e-test-app/
|
||||
+-- app.yaml
|
||||
| name: e2e-test-app
|
||||
| is: e2e-test-app
|
||||
| description: End-to-end test application...
|
||||
| latest: "2"
|
||||
| upgrade:
|
||||
| from:
|
||||
| - version: ">=1.0.0"
|
||||
| via: "1"
|
||||
| - version: "<1.0.0"
|
||||
| blocked: true
|
||||
| notes: "Versions before 1.0.0 are not supported"
|
||||
| preUpgrade:
|
||||
| backup: recommended
|
||||
+-- versions/
|
||||
+-- 2/
|
||||
| +-- manifest.yaml # version: 2.0.0
|
||||
| +-- kustomization.yaml
|
||||
| +-- ...
|
||||
+-- 1/
|
||||
+-- manifest.yaml # version: 1.0.0-1
|
||||
+-- kustomization.yaml
|
||||
+-- ...
|
||||
```
|
||||
|
||||
Upgrading from 0.5.0: blocked ("Versions before 1.0.0 are not supported").
|
||||
Upgrading from 1.2.0: two steps -- 1.2.0 -> 1.0.0-1 (waypoint) -> 2.0.0.
|
||||
|
||||
### SMTP (infrastructure service, one slot)
|
||||
|
||||
```
|
||||
smtp/
|
||||
+-- app.yaml
|
||||
| name: smtp
|
||||
| is: smtp
|
||||
| description: SMTP relay service...
|
||||
| category: infrastructure
|
||||
| latest: "1"
|
||||
+-- versions/
|
||||
+-- 1/
|
||||
+-- manifest.yaml # version: 1.0.0
|
||||
```
|
||||
|
||||
### Complex app with migrations
|
||||
|
||||
```
|
||||
discourse/
|
||||
+-- app.yaml
|
||||
| name: discourse
|
||||
| is: discourse
|
||||
| description: Discourse forum...
|
||||
| latest: "3"
|
||||
| upgrade:
|
||||
| from:
|
||||
| - version: ">=2.5.0"
|
||||
| - version: ">=2.0.0"
|
||||
| via: "2"
|
||||
| - version: "<2.0.0"
|
||||
| blocked: true
|
||||
| notes: "See upstream migration guide"
|
||||
| preUpgrade:
|
||||
| backup: required
|
||||
+-- versions/
|
||||
+-- 3/
|
||||
| +-- manifest.yaml # version: 3.6.0
|
||||
| +-- migrations/
|
||||
| | +-- pre-deploy.yaml
|
||||
| +-- kustomization.yaml
|
||||
| +-- ...
|
||||
+-- 2/
|
||||
+-- manifest.yaml # version: 2.8.0
|
||||
+-- kustomization.yaml
|
||||
+-- ...
|
||||
```
|
||||
|
||||
## Implementation notes
|
||||
|
||||
### Go types
|
||||
|
||||
```go
|
||||
// app.yaml
|
||||
type AppMeta struct {
|
||||
Name string `yaml:"name"`
|
||||
Is string `yaml:"is,omitempty"`
|
||||
Description string `yaml:"description"`
|
||||
Icon string `yaml:"icon,omitempty"`
|
||||
Category string `yaml:"category,omitempty"`
|
||||
Latest string `yaml:"latest"` // slot name
|
||||
Upgrade *UpgradeConfig `yaml:"upgrade,omitempty"`
|
||||
}
|
||||
|
||||
// version manifest.yaml (version-specific fields only)
|
||||
type AppManifest struct {
|
||||
Version string `yaml:"version"` // precise version string
|
||||
Requires []AppDependency `yaml:"requires,omitempty"`
|
||||
DefaultConfig map[string]interface{} `yaml:"defaultConfig,omitempty"`
|
||||
DefaultSecrets []SecretDefinition `yaml:"defaultSecrets,omitempty"`
|
||||
RequiredSecrets []string `yaml:"requiredSecrets,omitempty"`
|
||||
Deploy *DeployConfig `yaml:"deploy,omitempty"`
|
||||
Scripts []Script `yaml:"scripts,omitempty"`
|
||||
Upgrade *UpgradeConfig `yaml:"upgrade,omitempty"` // migrations only
|
||||
// Identity fields (Name, Is, Description, etc.) populated by applyMeta()
|
||||
}
|
||||
```
|
||||
|
||||
### Backward compatibility
|
||||
|
||||
The API supports both new-style (`app.yaml` + `versions/`) and old-style
|
||||
(`manifest.yaml` at root, `.versions/` for waypoints). `resolveAppDir()`
|
||||
checks for `app.yaml` first and falls back to the old layout.
|
||||
`ComputeUpgradePlan()` similarly checks for `app.yaml` before falling back
|
||||
to recursive manifest-based routing.
|
||||
|
||||
Old-style support exists for migration purposes. New apps should always use
|
||||
the new-style layout.
|
||||
|
||||
### Source URI in installed manifests
|
||||
|
||||
The `source` field in installed manifests points to the app root
|
||||
(`file:///wild-directory/ghost`), not the version directory. `Fetch()` and
|
||||
`Update()` resolve through `resolveAppDir()` from the root, which reads
|
||||
`app.yaml` to find the current latest slot.
|
||||
|
||||
## Design rationale
|
||||
|
||||
### Why decouple directory name from version string?
|
||||
|
||||
Coupling them creates unnecessary churn. Every packaging fix requires creating
|
||||
a new directory and deleting the old one (`5.118.1/` -> `5.118.1-1/`). Every
|
||||
minor upstream release does the same. This churn has no benefit -- the files
|
||||
inside are what matter, not the directory name.
|
||||
|
||||
Decoupling follows the source package pattern used by every major package
|
||||
system. The directory is a stable container. The version is metadata inside it.
|
||||
|
||||
### Why centralize routing rules in `app.yaml`?
|
||||
|
||||
Scattering `upgrade.from` rules across version manifests means each waypoint
|
||||
must know how to route TO itself. Adding a new waypoint requires editing
|
||||
multiple manifests. Reading the upgrade path requires opening every version
|
||||
manifest in the chain.
|
||||
|
||||
Centralizing in `app.yaml` means one file describes the complete routing
|
||||
topology. The upgrade planner reads one file and iteratively applies rules.
|
||||
Adding a new waypoint means editing one file.
|
||||
|
||||
### Why iterative re-evaluation instead of recursive descent?
|
||||
|
||||
Recursive descent reads the target manifest, finds a `via`, recurses into the
|
||||
waypoint, which has its own `via`, and so on. This scatters routing logic
|
||||
across files and makes the path hard to reason about.
|
||||
|
||||
Iterative re-evaluation reads `app.yaml` once, applies rules, steps to a
|
||||
waypoint, then applies the SAME rules again with the new version. The routing
|
||||
table is evaluated like firewall rules -- same table, different input. This
|
||||
is simpler to implement, debug, and maintain.
|
||||
|
||||
### Why not use a flat file for all versions?
|
||||
|
||||
A single YAML file listing all versions and their configs would centralize
|
||||
everything but would grow unwieldy for apps with many k8s resource files.
|
||||
Each version slot needs its own `kustomization.yaml`, deployment specs,
|
||||
service definitions, etc. Directories are the natural unit.
|
||||
5
cert-manager/app.yaml
Normal file
5
cert-manager/app.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
name: cert-manager
|
||||
is: cert-manager
|
||||
description: X.509 certificate management for Kubernetes
|
||||
category: infrastructure
|
||||
latest: "v1"
|
||||
@@ -1,8 +1,4 @@
|
||||
name: cert-manager
|
||||
is: cert-manager
|
||||
description: X.509 certificate management for Kubernetes
|
||||
version: v1.17.2
|
||||
category: infrastructure
|
||||
requires:
|
||||
- name: traefik
|
||||
defaultConfig:
|
||||
5
coredns/app.yaml
Normal file
5
coredns/app.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
name: coredns
|
||||
is: coredns
|
||||
description: DNS server for internal cluster DNS resolution
|
||||
category: infrastructure
|
||||
latest: "v1"
|
||||
@@ -1,8 +1,4 @@
|
||||
name: coredns
|
||||
is: coredns
|
||||
description: DNS server for internal cluster DNS resolution
|
||||
version: v1.12.0
|
||||
category: infrastructure
|
||||
requires:
|
||||
- name: metallb
|
||||
defaultConfig:
|
||||
5
crowdsec/app.yaml
Normal file
5
crowdsec/app.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
name: crowdsec
|
||||
is: crowdsec
|
||||
description: CrowdSec security engine with Traefik bouncer for threat detection and rate limiting
|
||||
category: infrastructure
|
||||
latest: "v1"
|
||||
@@ -1,8 +1,4 @@
|
||||
name: crowdsec
|
||||
is: crowdsec
|
||||
description: CrowdSec security engine with Traefik bouncer for threat detection and rate limiting
|
||||
version: v1.7.8
|
||||
category: infrastructure
|
||||
requires:
|
||||
- name: longhorn
|
||||
- name: traefik
|
||||
5
decidim/app.yaml
Normal file
5
decidim/app.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
name: decidim
|
||||
is: decidim
|
||||
description: Decidim is a participatory democracy framework for cities and organizations. Built in Ruby on Rails, it enables citizen participation through proposals, debates, and voting. Includes Sidekiq for background job processing.
|
||||
icon: https://raw.githubusercontent.com/decidim/decidim/develop/logo.svg
|
||||
latest: "0"
|
||||
@@ -1,8 +1,4 @@
|
||||
name: decidim
|
||||
is: decidim
|
||||
description: Decidim is a participatory democracy framework for cities and organizations. Built in Ruby on Rails, it enables citizen participation through proposals, debates, and voting. Includes Sidekiq for background job processing.
|
||||
version: 0.31.0-1
|
||||
icon: https://raw.githubusercontent.com/decidim/decidim/develop/logo.svg
|
||||
requires:
|
||||
- name: postgres
|
||||
installed_as: postgres
|
||||
5
discourse/app.yaml
Normal file
5
discourse/app.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
name: discourse
|
||||
is: discourse
|
||||
description: Discourse is a modern, open-source discussion platform designed for online communities and forums.
|
||||
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/discourse.svg
|
||||
latest: "3"
|
||||
@@ -1,8 +1,4 @@
|
||||
name: discourse
|
||||
is: discourse
|
||||
description: Discourse is a modern, open-source discussion platform designed for online communities and forums.
|
||||
version: 3.5.3-1
|
||||
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/discourse.svg
|
||||
requires:
|
||||
- name: postgres
|
||||
- name: redis
|
||||
@@ -41,4 +37,4 @@ defaultSecrets:
|
||||
default: "postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable"
|
||||
requiredSecrets:
|
||||
- postgres.password
|
||||
- redis.password
|
||||
- redis.password
|
||||
5
docker-registry/app.yaml
Normal file
5
docker-registry/app.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
name: docker-registry
|
||||
is: docker-registry
|
||||
description: Private Docker image registry for cluster
|
||||
category: infrastructure
|
||||
latest: "3"
|
||||
@@ -1,8 +1,4 @@
|
||||
name: docker-registry
|
||||
is: docker-registry
|
||||
description: Private Docker image registry for cluster
|
||||
version: "3.0.0"
|
||||
category: infrastructure
|
||||
requires:
|
||||
- name: traefik
|
||||
- name: cert-manager
|
||||
13
e2e-test-app/app.yaml
Normal file
13
e2e-test-app/app.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
name: e2e-test-app
|
||||
is: e2e-test-app
|
||||
description: End-to-end test application for automated integration testing. Includes PVC and PostgreSQL dependency to exercise all backup strategies.
|
||||
latest: "2"
|
||||
upgrade:
|
||||
from:
|
||||
- version: ">=1.0.0"
|
||||
via: "1"
|
||||
- version: "<1.0.0"
|
||||
blocked: true
|
||||
notes: "Versions before 1.0.0 are not supported for upgrade"
|
||||
preUpgrade:
|
||||
backup: recommended
|
||||
@@ -1,6 +1,3 @@
|
||||
name: e2e-test-app
|
||||
is: e2e-test-app
|
||||
description: End-to-end test application for automated integration testing. Includes PVC and PostgreSQL dependency to exercise all backup strategies.
|
||||
version: 1.0.0-1
|
||||
requires:
|
||||
- name: postgres
|
||||
@@ -1,16 +1,4 @@
|
||||
name: e2e-test-app
|
||||
is: e2e-test-app
|
||||
description: End-to-end test application for automated integration testing. Includes PVC and PostgreSQL dependency to exercise all backup strategies.
|
||||
version: 2.0.0
|
||||
upgrade:
|
||||
from:
|
||||
- version: ">=1.0.0"
|
||||
via: "1.0.0-1"
|
||||
- version: "<1.0.0"
|
||||
blocked: true
|
||||
notes: "Versions before 1.0.0 are not supported for upgrade"
|
||||
preUpgrade:
|
||||
backup: recommended
|
||||
requires:
|
||||
- name: postgres
|
||||
defaultConfig:
|
||||
4
example-admin/app.yaml
Normal file
4
example-admin/app.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
name: example-admin
|
||||
is: example
|
||||
description: An example application that is deployed with internal-only access.
|
||||
latest: "1"
|
||||
@@ -1,6 +1,3 @@
|
||||
name: example-admin
|
||||
is: example
|
||||
description: An example application that is deployed with internal-only access.
|
||||
version: 1.0.0
|
||||
defaultConfig:
|
||||
namespace: example-admin
|
||||
4
example-app/app.yaml
Normal file
4
example-app/app.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
name: example-app
|
||||
is: example
|
||||
description: An example application that is deployed with public access.
|
||||
latest: "1"
|
||||
@@ -1,6 +1,3 @@
|
||||
name: example-app
|
||||
is: example
|
||||
description: An example application that is deployed with public access.
|
||||
version: 1.0.0
|
||||
defaultConfig:
|
||||
namespace: example-app
|
||||
5
externaldns/app.yaml
Normal file
5
externaldns/app.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
name: externaldns
|
||||
is: externaldns
|
||||
description: Automatically configures DNS records for services
|
||||
category: infrastructure
|
||||
latest: "v0"
|
||||
@@ -1,9 +1,5 @@
|
||||
name: externaldns
|
||||
is: externaldns
|
||||
description: Automatically configures DNS records for services
|
||||
version: v0.13.4
|
||||
deploymentName: external-dns
|
||||
category: infrastructure
|
||||
requires:
|
||||
- name: cert-manager
|
||||
defaultConfig:
|
||||
5
ghost/app.yaml
Normal file
5
ghost/app.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
name: ghost
|
||||
is: ghost
|
||||
description: Ghost is a powerful app for new-media creators to publish, share, and grow a business around their content.
|
||||
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/ghost.png
|
||||
latest: "5"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user