First version of app upgrade.
This commit is contained in:
112
ADDING-APPS.md
112
ADDING-APPS.md
@@ -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:
|
||||||
|
|||||||
72
e2e-test-app/.versions/1.0.0-1/db-init-job.yaml
Normal file
72
e2e-test-app/.versions/1.0.0-1/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/1.0.0-1/deployment.yaml
Normal file
55
e2e-test-app/.versions/1.0.0-1/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/1.0.0-1/kustomization.yaml
Normal file
15
e2e-test-app/.versions/1.0.0-1/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
|
||||||
23
e2e-test-app/.versions/1.0.0-1/manifest.yaml
Normal file
23
e2e-test-app/.versions/1.0.0-1/manifest.yaml
Normal 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
|
||||||
4
e2e-test-app/.versions/1.0.0-1/namespace.yaml
Normal file
4
e2e-test-app/.versions/1.0.0-1/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: {{ .namespace }}
|
||||||
11
e2e-test-app/.versions/1.0.0-1/pvc.yaml
Normal file
11
e2e-test-app/.versions/1.0.0-1/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/1.0.0-1/service.yaml
Normal file
11
e2e-test-app/.versions/1.0.0-1/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
|
||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user