Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion skills/mittwald-migrate/playbooks/discover-source.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ For each persistent path, capture: mount path, real size, file count, "hot" (con

**Watch out for "index" volumes** that are populated on demand (Solr, Elasticsearch, generated caches — Pitfall #11). If `du` shows them small or empty, do not waste time copying them; let the target rebuild on first start.

### Release-symlink layouts (Deployer / Capistrano)

PHP/Ruby sources deployed with **Deployer** or **Capistrano** use a release-rotation layout: `current/` is a symlink to the active `releases/<timestamp>/`, and persistent data (uploads, `.env`, logs) lives in `shared/`, symlinked into each release. Spot it during the file inventory — `ls -l` on the app root shows `current -> releases/<ts>`.

Migrate the **active state only**, not the machinery:

- **Don't** copy the whole tree — `releases/` holds N old deploys and multiplies the data.
- **Don't** ship the symlinks as-is — they'd land as broken links on the target.
- **Do** transfer from `current/` with `tar -h` (dereference): the active release *and* the `shared/` content it links to come across as one flat tree, into the target's single document root. You do **not** rebuild the `current`/`releases`/`shared` scaffolding on mStudio. Mechanics: [`migrate-files.md`](migrate-files.md).

(If the operator wants to keep deploying *to* mStudio with Deployer afterwards, that's a separate post-migration setup — out of scope for the data move.)

### External object storage / S3

If the app uses S3 / MinIO / Backblaze / equivalent: capture bucket names, region, total size, number of objects. Decision point: keep on the original provider (often the cheapest path) or move to Mittwald-attached storage / a project-mounted bucket. Often: **keep**.
Expand Down Expand Up @@ -145,7 +157,7 @@ Same discipline for the data plane. Full procedure in [`../references/database-e

For **each DB engine** in the inventory from §2:

1. If the engine is not MySQL and not Redis → **container in the stack**. Stop.
1. If the engine is not MySQL and not Redis → **container in the stack**. Stop. This includes **MariaDB**: mirror it to a `mariadb:<major>` container — don't route a MariaDB source to managed MySQL by default (engine swap = explicit operator opt-in; see [`../references/database-engines.md`](../references/database-engines.md) "Default: mirror the source engine").
2. If it is MySQL or Redis → live-query the supported versions:
- MySQL: `mw database mysql versions` / `mcp__mittwald__mittwald_database_mysql_versions` / `GET /v2/mysql-versions`
- Redis: `mw database redis versions -p <projectId>` (CLI quirk — projectId required) / `mcp__mittwald__mittwald_database_redis_versions` / `GET /v2/redis-versions`
Expand Down Expand Up @@ -192,6 +204,7 @@ Produce a markdown summary and present it to the operator. Template:
| Source engine | Source version | Target | Reason |
|---|---|---|---|
| MySQL | 5.7 | managed MySQL 5.7 | exact match, non-disabled |
| MariaDB | 10.11 | container `mariadb:10.11` | mirror source engine — MariaDB ≠ managed MySQL |
| Redis | 7.0 | container `redis:7.0` | 7.0 is `disabled: true`, source operator doesn't want to upgrade |
| Postgres | 15.6 | container `library/postgres:15.6` | no managed Postgres on mStudio |

Expand Down
2 changes: 2 additions & 0 deletions skills/mittwald-migrate/playbooks/migrate-mysql.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

Decision was made during Discovery's DB-engines self-check ([`discover-source.md`](discover-source.md) §7b; full rules in [`../references/database-engines.md`](../references/database-engines.md)). One of two paths:

> **Default = mirror the source engine.** A **MariaDB** source goes to a **`mariadb:<major>` container**, not managed MySQL — importing MariaDB into managed MySQL is an engine swap that only happens on explicit operator opt-in (see [`database-engines.md`](../references/database-engines.md) "Default: mirror the source engine"). The managed-vs-container choice below applies to genuine **MySQL** sources.

| Choice | Pros | Cons |
|---|---|---|
| Mittwald-managed MySQL | Mittwald owns backups, upgrades, monitoring | Limited version flexibility, not in the same stack network |
Expand Down
2 changes: 2 additions & 0 deletions skills/mittwald-migrate/playbooks/provision-target.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ The decision was already made in Discovery's DB-engines self-check ([`discover-s
### Managed MySQL

Only for a genuine **MySQL** source. Mirror a **MariaDB** source to a container instead (see "Container DBs" below) — don't import MariaDB into managed MySQL by default ([`../references/database-engines.md`](../references/database-engines.md) "Default: mirror the source engine").

```text
mcp__mittwald__mittwald_database_mysql_versions # confirm chosen version is offered and not disabled
mcp__mittwald__mittwald_database_mysql_create
Expand Down
20 changes: 20 additions & 0 deletions skills/mittwald-migrate/references/database-engines.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ curl -sS -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $MITTWALD_AP
https://api.mittwald.de/v2/postgres-versions # expect 404
```

## Default: mirror the source engine

For an autonomous migration the **default is to reproduce the source faithfully** — same engine, same major version. Switching engines is a behaviour change whose breakage often surfaces only in production, so it is an **explicit operator decision, never the agent's default**.

- **Source is MariaDB → target is a MariaDB *container*** (matching major, e.g. `mariadb:10.11`). Do **not** import a MariaDB dump into managed MySQL by default. MariaDB and MySQL have diverged — auth plugins, `utf8mb4` collations, system tables, JSON handling, sequences, virtual-column syntax — so schemas usually port but app-level expectations can break after cutover.
- **Source is MySQL → target is MySQL** (managed or container — decided below).
- A MariaDB→MySQL (or any cross-engine) swap is only appropriate when the operator explicitly asks for it and accepts the compatibility risk. Surface it as a question; don't bake it into the plan.

A managed or runtime app can still talk to a container DB by service name (apps and containers share the project network — see "App ↔ container DB networking" below), so **"managed app + container MariaDB" is a fully supported, first-class shape**, not a workaround.

## Live-query primitives

| Operation | MCP | CLI | API |
Expand Down Expand Up @@ -91,11 +101,21 @@ Result: upgrade-required from 5.5 to ≥5.6 — propose 5.7 for closest semantic

## When the managed engine is the right call

(Assumes the source engine is **MySQL** — for a **MariaDB** source, mirror to a container; see "Default: mirror the source engine".)

- App is a Managed App (e.g. WordPress) where Mittwald is already responsible for the runtime — pair it with a managed DB for consistency and to let Mittwald do the upgrades.
- Source's DB version is well-supported, with no exotic config.
- Operator explicitly wants Mittwald to handle backups, point-in-time recovery, and security patches.
- The DB is a small, well-bounded thing (a Redis cache, a single-app MySQL instance) — managed gives you less surface to operate.

## App ↔ container DB networking

Within a project, **apps and container stacks share the same network**. A managed or runtime app reaches a stack service by its **service name** as hostname (the service's map key in the compose — e.g. `mariadb`), the same way containers reach each other (Pitfall #16). Per the mittwald docs: *"Managed applications and containers are connected to the same network. […] you can access managed applications from your containers and vice versa"* ([containers platform docs](https://developer.mittwald.de/docs/v2/platform/workloads/containers/)).

Consequence: a PHP / Node.js / Python runtime app (or a Managed App) can use a **container database** with `DB_HOST=<service-name>` — no port-forward, no public exposure. This is the recommended shape when mirroring a container-only engine (MariaDB, Postgres, Mongo).

Contrast with **managed** MySQL/Redis, which live *outside* the stack network: reach them via the host from `database_mysql_get` / `database_redis_get`, not a service name (Pitfall #16 does not apply to managed DBs).

## Cross-references

- Provisioning details, including the managed-host vs service-name distinction: [`../playbooks/provision-target.md`](../playbooks/provision-target.md) §7.
Expand Down
13 changes: 8 additions & 5 deletions skills/mittwald-migrate/references/mittwald-surfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ Identifier convention below: `{projectId}` etc. are UUIDs. CLI accepts short-IDs
| Operation | MCP | CLI | API |
|---|---|---|---|
| List projects | `project_list` | `mw project list -o json` | `GET /v2/projects` |
| Get project | `project_get` | `mw project get -p {projectId} -o json` | `GET /v2/projects/{projectId}` |
| Get project | `project_get` | `mw project get {projectId} -o json` ⚠ project is a **positional** arg, not `-p` | `GET /v2/projects/{projectId}` |
| Create project | `project_create` | `mw project create ...` | `POST /v2/servers/{serverId}/projects` |
| Delete project (destructive) | `project_delete` | `mw project delete {projectId} -f` | `DELETE /v2/projects/{projectId}` |
| Assemble SSH connection info | `project_ssh` (returns ready-to-use ssh command) | `mw project ssh <projectId>` ⚠ takes the project as a **positional** arg, not `-p` — opens an interactive shell directly | composed from `clusterId` + `clusterDomain` + `shortId` on the project object, plus an app or container shortId. No single endpoint; see [`ssh-modes.md`](ssh-modes.md) §"Assembling the SSH command from API fields". |
Expand Down Expand Up @@ -167,6 +167,7 @@ Managed engines on mStudio: **MySQL and Redis only**. Everything else → contai
| List / get domain | `domain_list` / `_get` | `mw domain list -o json` / `mw domain get {id}` | `GET /v2/projects/{projectId}/domains[/{id}]` |
| List virtualhosts | `domain_virtualhost_list` | `mw domain virtualhost list -o json` | `GET /v2/projects/{projectId}/ingresses` |
| Create / delete virtualhost | `domain_virtualhost_create` / `_delete` | `mw domain virtualhost create/delete` | `POST` / `DELETE /v2/projects/{projectId}/ingresses[/{id}]` |
| Update virtualhost **paths** (re-route) | — | — (no CLI command yet; delete+recreate, or use API) | `PATCH /v2/ingresses/{ingressId}/paths` — **resource-scoped**, not under `…/projects/{id}/…` |
| Get DNS zone | `domain_dnszone_get` / `_list` | `mw domain dns get/list` | `GET /v2/projects/{projectId}/dns-zones[/{id}]` |
| Update DNS zone | `domain_dnszone_update` | `mw domain dns update {zoneId}` | `PUT /v2/projects/{projectId}/dns-zones/{id}/records` |

Expand All @@ -181,10 +182,12 @@ Managed engines on mStudio: **MySQL and Redis only**. Everything else → contai

| Operation | MCP | CLI | API |
|---|---|---|---|
| One-shot backup | `backup_create` | `mw project backup create --expires 30d` | `POST /v2/projects/{projectId}/backups` |
| List / get / delete | `backup_list` / `_get` / `_delete` | `mw project backup list/get/delete` | `GET` / `DELETE /v2/projects/{projectId}/backups[/{id}]` |
| Download | — | `mw project backup download {id}` | dedicated endpoint via signed URL |
| Schedules | `backup_schedule_*` | `mw project backupschedule list` (+ create/update/delete) | `…/backup-schedules…` |
| One-shot backup | `backup_create` | `mw backup create --expires 30d` | `POST /v2/projects/{projectId}/backups` |
| List / get / delete | `backup_list` / `_get` / `_delete` | `mw backup list/get/delete` | `GET` / `DELETE /v2/projects/{projectId}/backups[/{id}]` |
| Download | — | `mw backup download {id}` | dedicated endpoint via signed URL |
| Schedules | `backup_schedule_*` | `mw backup schedule list` (+ create/update/delete) | `…/backup-schedules…` |

> ⚠ The `mw project backup …` / `mw project backupschedule …` forms are **deprecated** — the CLI now nests these under `mw backup …` / `mw backup schedule …`.

### Registry, SSH users, volumes

Expand Down
2 changes: 2 additions & 0 deletions skills/mittwald-migrate/references/pitfalls.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ Update the compose to reference the new path.

**Fix.** Rewrite source-side DB hostnames to bare service names: `DB_HOST=postgresql`, `REDIS_HOST=redis`. Capture this in the env-var rewrite map during Discovery.

This reach extends **across the app↔stack boundary**: a managed or runtime app in the same project resolves a container service by the same service name (apps and containers share the project network), so "runtime app + container DB" works with `DB_HOST=<service-name>`. See [`database-engines.md`](database-engines.md) § "App ↔ container DB networking". (Managed MySQL/Redis are the exception — they live outside the stack network and are reached by host, not service name.)

---

## #17 — Default `<shortId>.project.space` is your smoke-test surface
Expand Down
8 changes: 8 additions & 0 deletions skills/mittwald-migrate/references/ssh-modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ Where:

Both modes go through the same SSH gateway (`ssh.<cluster>.<clusterDomain>`).

> **Local SSH multiplexing breaks mStudio connections.** If your `~/.ssh/config` enables connection sharing (`ControlMaster auto` with a `ControlPath`), an mStudio connection can fail with a misleading `could not connect to project` even though the key is registered and the app is `ready` — the shared control socket is keyed on the gateway host and collides with the multi-`@` routing identity. Disable it per connection:
>
> ```
> ssh -o ControlMaster=no -o ControlPath=none <ssh-identity>@<short-id>@ssh.<cluster>.project.host
> ```
>
> Apply the same two flags to any `scp` / `rsync -e ssh` / `tar … | ssh …` pipeline against mStudio.

## Two SSH user models — which one are you using?

This decides where the SSH key lives and what the `<ssh-identity>` looks like in the URL.
Expand Down
Loading