Compare commits

..

2 Commits

Author SHA1 Message Date
Paul Payne
945d2225a2 First version of app upgrade. 2026-05-24 04:00:07 +00:00
Paul Payne
6e1c676c09 feat: update versioning in longhorn and snapshot-controller manifests 2026-05-23 20:42:45 +00:00
11 changed files with 316 additions and 3 deletions

View File

@@ -85,6 +85,118 @@ 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. 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.
### 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
```yaml
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
- version: "<3.4.0"
blocked: true
notes: "Requires sequential major upgrades. See upstream docs."
preUpgrade:
backup: required # "none", "recommended", or "required"
migrations:
pre:
- migrations/pre-deploy.yaml # K8s Job YAML paths relative to app dir
post:
- migrations/post-deploy.yaml
configMigrations:
oldKeyName: newKeyName # Renames config keys automatically
```
**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[].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 |
| `configMigrations` | Map of old config key → new config key for automatic renaming |
#### Waypoint versions (`.versions/` directory)
When an upgrade requires passing through an intermediate version, store that version's files in a `.versions/` subdirectory:
```
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)
├── 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`.
**Creating a waypoint:**
```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
```
#### 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:
```yaml
# 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 3.6.0 manifest (or its waypoint), not on 3.5.x.
#### Example: simple app with waypoint
```yaml
# myapp/manifest.yaml (version 2.0.0)
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"
preUpgrade:
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.
### Dependency Configuration ### Dependency Configuration
- Each dependency in `requires` can have: - Each dependency in `requires` can have:

View 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"

View 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

View 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

View File

@@ -0,0 +1,23 @@
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
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

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: {{ .namespace }}

View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: e2e-test-app-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: {{ .storage }}

View 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

View File

@@ -1,7 +1,16 @@
name: e2e-test-app name: e2e-test-app
is: 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. 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 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: requires:
- name: postgres - name: postgres
defaultConfig: defaultConfig:

View File

@@ -1,12 +1,13 @@
name: longhorn name: longhorn
is: longhorn is: longhorn
description: Cloud-native distributed block storage for Kubernetes description: Cloud-native distributed block storage for Kubernetes
version: v1.8.1 version: v1.8.1-2
deploymentName: longhorn-ui deploymentName: longhorn-ui
category: infrastructure category: infrastructure
requires: requires:
- name: traefik - name: traefik
- name: nfs - name: nfs
- name: snapshot-controller
defaultConfig: defaultConfig:
namespace: longhorn-system namespace: longhorn-system
internalDomain: "{{ .cloud.internalDomain }}" internalDomain: "{{ .cloud.internalDomain }}"

View File

@@ -1,7 +1,7 @@
name: snapshot-controller name: snapshot-controller
is: snapshot-controller is: snapshot-controller
description: Kubernetes CSI Snapshot Controller for managing VolumeSnapshots description: Kubernetes CSI Snapshot Controller for managing VolumeSnapshots
version: v8.1.0 version: v8.1.0_1
deploymentName: snapshot-controller deploymentName: snapshot-controller
category: infrastructure category: infrastructure
defaultConfig: defaultConfig: