Compare commits
9 Commits
12e87635c6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8117fb8175 | ||
|
|
189fdab0bc | ||
|
|
bc7a168851 | ||
|
|
945d2225a2 | ||
|
|
6e1c676c09 | ||
|
|
6b5325c6f3 | ||
|
|
e2e3f730a5 | ||
|
|
46002ff273 | ||
|
|
acec744df8 |
458
ADDING-APPS.md
458
ADDING-APPS.md
@@ -6,69 +6,442 @@ 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
|
||||
requires:
|
||||
- name: pg
|
||||
alias: db # Use a different reference name in templates
|
||||
- name: redis # 'alias' and 'installedAs' default to 'name' value
|
||||
defaultConfig:
|
||||
serverImage: ghcr.io/immich-app/immich-server:release
|
||||
mlImage: ghcr.io/immich-app/immich-machine-learning:release
|
||||
timezone: UTC
|
||||
serverPort: 2283
|
||||
mlPort: 3003
|
||||
storage: 250Gi
|
||||
cacheStorage: 10Gi
|
||||
redisHostname: "{{ .apps.redis.host }}" # Can reference 'requires' app configurations
|
||||
dbHostname: "{{ .apps.pg.host }}"
|
||||
db: # Configuration can be nested
|
||||
name: immich
|
||||
user: immich
|
||||
host: "{{ .apps.pg.host }}"
|
||||
port: "{{ .apps.pg.port }}"
|
||||
domain: immich.{{ .cloud.domain }}
|
||||
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.? }}
|
||||
requiredSecrets:
|
||||
- db.password # References postgres app via 'db' alias
|
||||
- redis.auth # References redis app via 'redis' name (no alias)
|
||||
latest: "1"
|
||||
```
|
||||
|
||||
### Manifest Fields
|
||||
### 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 |
|
||||
| `version` | Yes | App version (follow upstream versioning) |
|
||||
| `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
|
||||
- name: redis # 'alias' and 'installedAs' default to 'name' value
|
||||
defaultConfig:
|
||||
namespace: immich
|
||||
externalDnsDomain: "{{ .cloud.domain }}"
|
||||
storage: 250Gi
|
||||
cacheStorage: 10Gi
|
||||
domain: immich.{{ .cloud.domain }}
|
||||
tlsSecretName: wildcard-wild-cloud-tls
|
||||
db: # Configuration can be nested
|
||||
host: "{{ .apps.pg.host }}" # Can reference 'requires' app configurations
|
||||
name: immich
|
||||
user: immich
|
||||
redis:
|
||||
host: "{{ .apps.redis.host }}"
|
||||
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"
|
||||
requiredSecrets:
|
||||
- db.password # References postgres app via 'db' alias
|
||||
- redis.auth # References redis app via 'redis' name (no alias)
|
||||
```
|
||||
|
||||
### Version Manifest Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `version` | Yes | App version (see Versioning Convention below) |
|
||||
| `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) |
|
||||
| `requiredSecrets` | No | List of secrets from dependency apps (format: `<app-ref>.<key>`) |
|
||||
|
||||
### Versioning Convention
|
||||
|
||||
Wild Cloud uses a two-part version scheme inspired by Debian packaging: `<upstream>-<revision>`.
|
||||
|
||||
- **Upstream version** tracks the third-party software version (e.g., `v4.0.18`, `1.120.2`)
|
||||
- **Packaging revision** tracks Wild Cloud packaging changes (template fixes, manifest cleanup, config restructuring) that don't change the upstream software version
|
||||
|
||||
**Examples:**
|
||||
- `v4.0.18` — initial packaging of upstream v4.0.18
|
||||
- `v4.0.18-1` — first packaging fix (no upstream change)
|
||||
- `v4.0.18-2` — second packaging fix
|
||||
- `v4.0.19` — upstream version bump, revision resets
|
||||
|
||||
**When to bump the packaging revision:** Any change to the app package that doesn't correspond to an upstream software update — manifest field changes, template improvements, kustomize restructuring, security context fixes, label corrections, etc.
|
||||
|
||||
**When to bump the upstream version:** When updating the container image tag or deploying a new version of the third-party software.
|
||||
|
||||
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.
|
||||
|
||||
**When you don't need `upgrade:`** Simple apps (Ghost, Redis, most stateless apps) where any version can safely replace any other version. This is the 90% case — just bump the version and the system handles it as a single-step update.
|
||||
|
||||
**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 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: "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 version dir
|
||||
post:
|
||||
- migrations/post-deploy.yaml
|
||||
configMigrations:
|
||||
oldKeyName: newKeyName # Renames config keys automatically
|
||||
```
|
||||
|
||||
**`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 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 |
|
||||
|
||||
**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
|
||||
|
||||
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/
|
||||
├── 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 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:** The current latest slot becomes the waypoint (leave it in place), then create a new slot for the new major version:
|
||||
|
||||
```bash
|
||||
# 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 version slot directory and reference them from that version's `manifest.yaml`:
|
||||
|
||||
```yaml
|
||||
# versions/3/migrations/db-migrate.yaml
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: myapp-db-migrate
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
containers:
|
||||
- name: migrate
|
||||
image: myapp:3.6.0
|
||||
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 slot `3/` directory.
|
||||
|
||||
#### Example: simple app with waypoint
|
||||
|
||||
```yaml
|
||||
# myapp/app.yaml
|
||||
name: myapp
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
- Each dependency in `requires` can have:
|
||||
@@ -538,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
|
||||
@@ -1,35 +0,0 @@
|
||||
# Decidim
|
||||
|
||||
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.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **PostgreSQL** - Database for storing participatory processes and user data
|
||||
- **Redis** - Used for Sidekiq background job processing
|
||||
|
||||
## Configuration
|
||||
|
||||
Key settings configured through your instance's `config.yaml`:
|
||||
|
||||
- **domain** - Where Decidim will be accessible (default: `decidim.{your-cloud-domain}`)
|
||||
- **siteName** - Display name for your platform (default: `Decidim`)
|
||||
- **systemAdminEmail** - System admin email (defaults to your operator email)
|
||||
- **storage** - Persistent volume size (default: `20Gi`)
|
||||
- **SMTP** - Email delivery settings inherited from your Wild Cloud instance
|
||||
|
||||
## Access
|
||||
|
||||
After deployment, Decidim will be available at:
|
||||
- `https://decidim.{your-cloud-domain}`
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
1. Add and deploy the app:
|
||||
```bash
|
||||
wild app add decidim
|
||||
wild app deploy decidim
|
||||
```
|
||||
|
||||
2. Log in with the system admin credentials configured during setup
|
||||
|
||||
3. Create your first organization and configure participatory processes
|
||||
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,45 +0,0 @@
|
||||
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
|
||||
icon: https://raw.githubusercontent.com/decidim/decidim/develop/logo.svg
|
||||
requires:
|
||||
- name: postgres
|
||||
installed_as: postgres
|
||||
- name: redis
|
||||
installed_as: redis
|
||||
- name: smtp
|
||||
defaultConfig:
|
||||
namespace: decidim
|
||||
externalDnsDomain: "{{ .cloud.domain }}"
|
||||
timezone: UTC
|
||||
port: 3000
|
||||
storage: 20Gi
|
||||
systemAdminEmail: "{{ .operator.email }}"
|
||||
siteName: "Decidim"
|
||||
domain: decidim.{{ .cloud.domain }}
|
||||
dbHostname: "{{ .apps.postgres.host }}"
|
||||
dbPort: "{{ .apps.postgres.port }}"
|
||||
dbUsername: decidim
|
||||
dbName: decidim
|
||||
redisHostname: "{{ .apps.redis.host }}"
|
||||
tlsSecretName: wildcard-wild-cloud-tls
|
||||
smtp:
|
||||
enabled: true
|
||||
host: "{{ .apps.smtp.host }}"
|
||||
port: "{{ .apps.smtp.port }}"
|
||||
user: "{{ .apps.smtp.user }}"
|
||||
from: "{{ .apps.smtp.from }}"
|
||||
tls: "{{ .apps.smtp.tls }}"
|
||||
startTls: "{{ .apps.smtp.startTls }}"
|
||||
defaultSecrets:
|
||||
- key: systemAdminPassword
|
||||
- key: secretKeyBase
|
||||
default: "{{ random.AlphaNum 128 }}"
|
||||
- key: smtpPassword
|
||||
- key: dbPassword
|
||||
- key: dbUrl
|
||||
default: "postgres://{{ .app.dbUsername }}:{{ .secrets.dbPassword }}@{{ .app.dbHostname }}:{{ .app.dbPort }}/{{ .app.dbName }}"
|
||||
requiredSecrets:
|
||||
- postgres.password
|
||||
- redis.password
|
||||
13
decidim/versions/0/README.md
Normal file
13
decidim/versions/0/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Decidim
|
||||
|
||||
Decidim is a participatory democracy framework for cities and organizations. It enables citizen participation through proposals, debates, and voting.
|
||||
|
||||
## Configuration
|
||||
|
||||
Key settings configured through your instance's `config.yaml`:
|
||||
|
||||
- **domain** - Where Decidim will be accessible (default: `decidim.{your-cloud-domain}`)
|
||||
- **siteName** - Display name for your platform (default: `Decidim`)
|
||||
- **systemAdminEmail** - System admin email (defaults to your operator email)
|
||||
- **storage** - Persistent volume size (default: `20Gi`)
|
||||
- **SMTP** - Email delivery settings inherited from your Wild Cloud instance
|
||||
@@ -54,7 +54,7 @@ spec:
|
||||
echo "Database initialization completed successfully"
|
||||
env:
|
||||
- name: POSTGRES_HOST
|
||||
value: {{ .dbHostname }}
|
||||
value: {{ .db.host }}
|
||||
- name: POSTGRES_ADMIN_USER
|
||||
value: postgres
|
||||
- name: POSTGRES_ADMIN_PASSWORD
|
||||
@@ -63,9 +63,9 @@ spec:
|
||||
name: decidim-secrets
|
||||
key: postgres.password
|
||||
- name: DB_NAME
|
||||
value: {{ .dbName }}
|
||||
value: {{ .db.name }}
|
||||
- name: DB_USER
|
||||
value: {{ .dbUsername }}
|
||||
value: {{ .db.user }}
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -55,7 +55,7 @@ spec:
|
||||
- name: RAILS_ENV
|
||||
value: "production"
|
||||
- name: PORT
|
||||
value: "{{ .port }}"
|
||||
value: "3000"
|
||||
- name: RAILS_LOG_TO_STDOUT
|
||||
value: "true"
|
||||
# Database configuration
|
||||
@@ -66,7 +66,7 @@ spec:
|
||||
key: dbUrl
|
||||
# Redis configuration
|
||||
- name: REDIS_HOSTNAME
|
||||
value: {{ .redisHostname }}
|
||||
value: {{ .redis.host }}
|
||||
- name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -112,11 +112,11 @@ spec:
|
||||
key: systemAdminPassword
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .port }}
|
||||
containerPort: 3000
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: {{ .port }}
|
||||
port: 3000
|
||||
initialDelaySeconds: 300
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 10
|
||||
@@ -124,7 +124,7 @@ spec:
|
||||
failureThreshold: 6
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: {{ .port }}
|
||||
port: 3000
|
||||
initialDelaySeconds: 180
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 10
|
||||
@@ -182,7 +182,7 @@ spec:
|
||||
key: dbUrl
|
||||
# Redis configuration
|
||||
- name: REDIS_HOSTNAME
|
||||
value: {{ .redisHostname }}
|
||||
value: {{ .redis.host }}
|
||||
- name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -23,4 +23,4 @@ spec:
|
||||
service:
|
||||
name: decidim
|
||||
port:
|
||||
number: {{ .port }}
|
||||
number: 3000
|
||||
41
decidim/versions/0/manifest.yaml
Normal file
41
decidim/versions/0/manifest.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
version: 0.31.0-1
|
||||
requires:
|
||||
- name: postgres
|
||||
installed_as: postgres
|
||||
- name: redis
|
||||
installed_as: redis
|
||||
- name: smtp
|
||||
defaultConfig:
|
||||
namespace: decidim
|
||||
externalDnsDomain: '{{ .cloud.domain }}'
|
||||
storage: 20Gi
|
||||
systemAdminEmail: '{{ .operator.email }}'
|
||||
siteName: 'Decidim'
|
||||
domain: decidim.{{ .cloud.domain }}
|
||||
tlsSecretName: wildcard-wild-cloud-tls
|
||||
db:
|
||||
host: '{{ .apps.postgres.host }}'
|
||||
port: '{{ .apps.postgres.port }}'
|
||||
name: decidim
|
||||
user: decidim
|
||||
redis:
|
||||
host: '{{ .apps.redis.host }}'
|
||||
smtp:
|
||||
enabled: true
|
||||
host: '{{ .apps.smtp.host }}'
|
||||
port: '{{ .apps.smtp.port }}'
|
||||
user: '{{ .apps.smtp.user }}'
|
||||
from: '{{ .apps.smtp.from }}'
|
||||
tls: '{{ .apps.smtp.tls }}'
|
||||
startTls: '{{ .apps.smtp.startTls }}'
|
||||
defaultSecrets:
|
||||
- key: systemAdminPassword
|
||||
- key: secretKeyBase
|
||||
default: "{{ random.AlphaNum 128 }}"
|
||||
- key: smtpPassword
|
||||
- key: dbPassword
|
||||
- key: dbUrl
|
||||
default: "postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}"
|
||||
requiredSecrets:
|
||||
- postgres.password
|
||||
- redis.password
|
||||
@@ -9,7 +9,7 @@ spec:
|
||||
component: web
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .port }}
|
||||
port: 3000
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
type: ClusterIP
|
||||
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,44 +0,0 @@
|
||||
name: discourse
|
||||
is: discourse
|
||||
description: Discourse is a modern, open-source discussion platform designed for online communities and forums.
|
||||
version: 3.5.3
|
||||
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/discourse.svg
|
||||
requires:
|
||||
- name: postgres
|
||||
- name: redis
|
||||
- name: smtp
|
||||
defaultConfig:
|
||||
namespace: discourse
|
||||
externalDnsDomain: "{{ .cloud.domain }}"
|
||||
timezone: UTC
|
||||
port: 3000
|
||||
storage: 10Gi
|
||||
adminEmail: "{{ .operator.email }}"
|
||||
adminUsername: admin
|
||||
siteName: "Community"
|
||||
domain: discourse.{{ .cloud.domain }}
|
||||
dbHostname: "{{ .apps.postgres.host }}"
|
||||
dbPort: "{{ .apps.postgres.port }}"
|
||||
dbUsername: discourse
|
||||
dbName: discourse
|
||||
redisHostname: "{{ .apps.redis.host }}"
|
||||
tlsSecretName: wildcard-wild-cloud-tls
|
||||
smtp:
|
||||
enabled: false
|
||||
host: "{{ .apps.smtp.host }}"
|
||||
port: "{{ .apps.smtp.port }}"
|
||||
user: "{{ .apps.smtp.user }}"
|
||||
from: "{{ .apps.smtp.from }}"
|
||||
tls: "{{ .apps.smtp.tls }}"
|
||||
startTls: "{{ .apps.smtp.startTls }}"
|
||||
defaultSecrets:
|
||||
- key: adminPassword
|
||||
- key: secretKeyBase
|
||||
default: "{{ random.AlphaNum 64 }}"
|
||||
- key: smtpPassword
|
||||
- key: dbPassword
|
||||
- key: dbUrl
|
||||
default: "postgres://{{ .app.dbUsername }}:{{ .secrets.dbPassword }}@{{ .app.dbHostname }}:{{ .app.dbPort }}/{{ .app.dbName }}?sslmode=disable"
|
||||
requiredSecrets:
|
||||
- postgres.password
|
||||
- redis.password
|
||||
@@ -27,7 +27,7 @@ spec:
|
||||
readOnlyRootFilesystem: false
|
||||
env:
|
||||
- name: PGHOST
|
||||
value: "{{ .dbHostname }}"
|
||||
value: "{{ .db.host }}"
|
||||
- name: PGPORT
|
||||
value: "5432"
|
||||
- name: PGUSER
|
||||
@@ -38,9 +38,9 @@ spec:
|
||||
name: discourse-secrets
|
||||
key: postgres.password
|
||||
- name: DISCOURSE_DB_USER
|
||||
value: "{{ .dbUsername }}"
|
||||
value: "{{ .db.user }}"
|
||||
- name: DISCOURSE_DB_NAME
|
||||
value: "{{ .dbName }}"
|
||||
value: "{{ .db.name }}"
|
||||
- name: DISCOURSE_DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -56,20 +56,20 @@ spec:
|
||||
- name: RAILS_ENV
|
||||
value: "production"
|
||||
- name: DISCOURSE_DB_HOST
|
||||
value: {{ .dbHostname }}
|
||||
value: {{ .db.host }}
|
||||
- name: DISCOURSE_DB_PORT
|
||||
value: "{{ .dbPort }}"
|
||||
value: "{{ .db.port }}"
|
||||
- name: DISCOURSE_DB_NAME
|
||||
value: {{ .dbName }}
|
||||
value: {{ .db.name }}
|
||||
- name: DISCOURSE_DB_USERNAME
|
||||
value: {{ .dbUsername }}
|
||||
value: {{ .db.user }}
|
||||
- name: DISCOURSE_DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: discourse-secrets
|
||||
key: dbPassword
|
||||
- name: DISCOURSE_REDIS_HOST
|
||||
value: {{ .redisHostname }}
|
||||
value: {{ .redis.host }}
|
||||
- name: DISCOURSE_REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -113,13 +113,13 @@ spec:
|
||||
value: "production"
|
||||
# Discourse database configuration
|
||||
- name: DISCOURSE_DB_HOST
|
||||
value: {{ .dbHostname }}
|
||||
value: {{ .db.host }}
|
||||
- name: DISCOURSE_DB_PORT
|
||||
value: "{{ .dbPort }}"
|
||||
value: "{{ .db.port }}"
|
||||
- name: DISCOURSE_DB_NAME
|
||||
value: {{ .dbName }}
|
||||
value: {{ .db.name }}
|
||||
- name: DISCOURSE_DB_USERNAME
|
||||
value: {{ .dbUsername }}
|
||||
value: {{ .db.user }}
|
||||
- name: DISCOURSE_DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -127,7 +127,7 @@ spec:
|
||||
key: dbPassword
|
||||
# Redis configuration
|
||||
- name: DISCOURSE_REDIS_HOST
|
||||
value: {{ .redisHostname }}
|
||||
value: {{ .redis.host }}
|
||||
- name: DISCOURSE_REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -220,13 +220,13 @@ spec:
|
||||
value: "production"
|
||||
# Discourse database configuration
|
||||
- name: DISCOURSE_DB_HOST
|
||||
value: {{ .dbHostname }}
|
||||
value: {{ .db.host }}
|
||||
- name: DISCOURSE_DB_PORT
|
||||
value: "{{ .dbPort }}"
|
||||
value: "{{ .db.port }}"
|
||||
- name: DISCOURSE_DB_NAME
|
||||
value: {{ .dbName }}
|
||||
value: {{ .db.name }}
|
||||
- name: DISCOURSE_DB_USERNAME
|
||||
value: {{ .dbUsername }}
|
||||
value: {{ .db.user }}
|
||||
- name: DISCOURSE_DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -234,7 +234,7 @@ spec:
|
||||
key: dbPassword
|
||||
# Redis configuration
|
||||
- name: DISCOURSE_REDIS_HOST
|
||||
value: {{ .redisHostname }}
|
||||
value: {{ .redis.host }}
|
||||
- name: DISCOURSE_REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
40
discourse/versions/3/manifest.yaml
Normal file
40
discourse/versions/3/manifest.yaml
Normal file
@@ -0,0 +1,40 @@
|
||||
version: 3.5.3-1
|
||||
requires:
|
||||
- name: postgres
|
||||
- name: redis
|
||||
- name: smtp
|
||||
defaultConfig:
|
||||
namespace: discourse
|
||||
externalDnsDomain: '{{ .cloud.domain }}'
|
||||
storage: 10Gi
|
||||
adminEmail: '{{ .operator.email }}'
|
||||
adminUsername: admin
|
||||
siteName: 'Community'
|
||||
domain: discourse.{{ .cloud.domain }}
|
||||
tlsSecretName: wildcard-wild-cloud-tls
|
||||
db:
|
||||
host: '{{ .apps.postgres.host }}'
|
||||
port: '{{ .apps.postgres.port }}'
|
||||
name: discourse
|
||||
user: discourse
|
||||
redis:
|
||||
host: '{{ .apps.redis.host }}'
|
||||
smtp:
|
||||
enabled: false
|
||||
host: '{{ .apps.smtp.host }}'
|
||||
port: '{{ .apps.smtp.port }}'
|
||||
user: '{{ .apps.smtp.user }}'
|
||||
from: '{{ .apps.smtp.from }}'
|
||||
tls: '{{ .apps.smtp.tls }}'
|
||||
startTls: '{{ .apps.smtp.startTls }}'
|
||||
defaultSecrets:
|
||||
- key: adminPassword
|
||||
- key: secretKeyBase
|
||||
default: "{{ random.AlphaNum 64 }}"
|
||||
- key: smtpPassword
|
||||
- key: dbPassword
|
||||
- key: dbUrl
|
||||
default: "postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable"
|
||||
requiredSecrets:
|
||||
- postgres.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,23 +0,0 @@
|
||||
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
|
||||
requires:
|
||||
- name: postgres
|
||||
defaultConfig:
|
||||
namespace: e2e-test-app
|
||||
domain: e2e-test-app.{{ .cloud.domain }}
|
||||
externalDnsDomain: "{{ .cloud.domain }}"
|
||||
tlsSecretName: wildcard-wild-cloud-tls
|
||||
storage: 1Gi
|
||||
dbHost: "{{ .apps.postgres.host }}"
|
||||
dbPort: "{{ .apps.postgres.port }}"
|
||||
dbName: e2e_test_app
|
||||
dbUser: e2e_test_app
|
||||
timezone: UTC
|
||||
defaultSecrets:
|
||||
- key: dbPassword
|
||||
- key: dbUrl
|
||||
default: "postgres://{{ .app.dbUser }}:{{ .secrets.dbPassword }}@{{ .app.dbHost }}:{{ .app.dbPort }}/{{ .app.dbName }}?sslmode=disable"
|
||||
requiredSecrets:
|
||||
- postgres.password
|
||||
@@ -28,7 +28,7 @@ spec:
|
||||
readOnlyRootFilesystem: false
|
||||
env:
|
||||
- name: PGHOST
|
||||
value: {{ .dbHost }}
|
||||
value: {{ .db.host }}
|
||||
- name: PGUSER
|
||||
value: postgres
|
||||
- name: PGPASSWORD
|
||||
@@ -37,9 +37,9 @@ spec:
|
||||
name: e2e-test-app-secrets
|
||||
key: postgres.password
|
||||
- name: DB_NAME
|
||||
value: {{ .dbName }}
|
||||
value: {{ .db.name }}
|
||||
- name: DB_USER
|
||||
value: {{ .dbUser }}
|
||||
value: {{ .db.user }}
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
20
e2e-test-app/versions/1/manifest.yaml
Normal file
20
e2e-test-app/versions/1/manifest.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
version: 1.0.0-1
|
||||
requires:
|
||||
- name: postgres
|
||||
defaultConfig:
|
||||
namespace: e2e-test-app
|
||||
domain: e2e-test-app.{{ .cloud.domain }}
|
||||
externalDnsDomain: '{{ .cloud.domain }}'
|
||||
tlsSecretName: wildcard-wild-cloud-tls
|
||||
storage: 1Gi
|
||||
db:
|
||||
host: '{{ .apps.postgres.host }}'
|
||||
port: '{{ .apps.postgres.port }}'
|
||||
name: e2e_test_app
|
||||
user: e2e_test_app
|
||||
defaultSecrets:
|
||||
- key: dbPassword
|
||||
- key: dbUrl
|
||||
default: "postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable"
|
||||
requiredSecrets:
|
||||
- postgres.password
|
||||
72
e2e-test-app/versions/2/db-init-job.yaml
Normal file
72
e2e-test-app/versions/2/db-init-job.yaml
Normal file
@@ -0,0 +1,72 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: e2e-test-app-db-init
|
||||
labels:
|
||||
component: db-init
|
||||
spec:
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
component: db-init
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 999
|
||||
runAsGroup: 999
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: postgres-init
|
||||
image: postgres:15
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: false
|
||||
env:
|
||||
- name: PGHOST
|
||||
value: {{ .db.host }}
|
||||
- name: PGUSER
|
||||
value: postgres
|
||||
- name: PGPASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: e2e-test-app-secrets
|
||||
key: postgres.password
|
||||
- name: DB_NAME
|
||||
value: {{ .db.name }}
|
||||
- name: DB_USER
|
||||
value: {{ .db.user }}
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: e2e-test-app-secrets
|
||||
key: dbPassword
|
||||
command:
|
||||
- /bin/bash
|
||||
- -c
|
||||
- |
|
||||
set -e
|
||||
echo "Waiting for PostgreSQL to be ready..."
|
||||
until pg_isready; do
|
||||
echo "PostgreSQL is not ready - sleeping"
|
||||
sleep 2
|
||||
done
|
||||
echo "PostgreSQL is ready"
|
||||
|
||||
echo "Creating database and user..."
|
||||
psql -c "CREATE DATABASE ${DB_NAME};" || echo "Database ${DB_NAME} already exists"
|
||||
psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}';" || echo "User ${DB_USER} already exists"
|
||||
psql -c "ALTER USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}';"
|
||||
psql -c "GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};"
|
||||
psql -d ${DB_NAME} -c "GRANT ALL ON SCHEMA public TO ${DB_USER};"
|
||||
|
||||
echo "Creating test data table..."
|
||||
psql -d ${DB_NAME} -c "CREATE TABLE IF NOT EXISTS e2e_test_data (id SERIAL PRIMARY KEY, key TEXT UNIQUE NOT NULL, value TEXT NOT NULL, created_at TIMESTAMP DEFAULT NOW());"
|
||||
psql -d ${DB_NAME} -c "GRANT ALL ON TABLE e2e_test_data TO ${DB_USER};"
|
||||
psql -d ${DB_NAME} -c "GRANT USAGE, SELECT ON SEQUENCE e2e_test_data_id_seq TO ${DB_USER};"
|
||||
|
||||
echo "Database initialization complete"
|
||||
55
e2e-test-app/versions/2/deployment.yaml
Normal file
55
e2e-test-app/versions/2/deployment.yaml
Normal file
@@ -0,0 +1,55 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: e2e-test-app
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
component: web
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
component: web
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 101
|
||||
runAsGroup: 101
|
||||
fsGroup: 101
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginxinc/nginx-unprivileged:alpine
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
volumeMounts:
|
||||
- name: app-data
|
||||
mountPath: /data
|
||||
resources:
|
||||
limits:
|
||||
cpu: 100m
|
||||
memory: 64Mi
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 32Mi
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8080
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 5
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: false
|
||||
volumes:
|
||||
- name: app-data
|
||||
persistentVolumeClaim:
|
||||
claimName: e2e-test-app-data
|
||||
15
e2e-test-app/versions/2/kustomization.yaml
Normal file
15
e2e-test-app/versions/2/kustomization.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: e2e-test-app
|
||||
labels:
|
||||
- includeSelectors: true
|
||||
pairs:
|
||||
app: e2e-test-app
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
- pvc.yaml
|
||||
- db-init-job.yaml
|
||||
20
e2e-test-app/versions/2/manifest.yaml
Normal file
20
e2e-test-app/versions/2/manifest.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
version: 2.0.0
|
||||
requires:
|
||||
- name: postgres
|
||||
defaultConfig:
|
||||
namespace: e2e-test-app
|
||||
domain: e2e-test-app.{{ .cloud.domain }}
|
||||
externalDnsDomain: '{{ .cloud.domain }}'
|
||||
tlsSecretName: wildcard-wild-cloud-tls
|
||||
storage: 1Gi
|
||||
db:
|
||||
host: '{{ .apps.postgres.host }}'
|
||||
port: '{{ .apps.postgres.port }}'
|
||||
name: e2e_test_app
|
||||
user: e2e_test_app
|
||||
defaultSecrets:
|
||||
- key: dbPassword
|
||||
- key: dbUrl
|
||||
default: "postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable"
|
||||
requiredSecrets:
|
||||
- postgres.password
|
||||
11
e2e-test-app/versions/2/pvc.yaml
Normal file
11
e2e-test-app/versions/2/pvc.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: e2e-test-app-data
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: longhorn
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .storage }}
|
||||
11
e2e-test-app/versions/2/service.yaml
Normal file
11
e2e-test-app/versions/2/service.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: e2e-test-app
|
||||
spec:
|
||||
selector:
|
||||
component: web
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
name: http
|
||||
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"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user