Files
tia-love/docs/rea/composerize.md
T

579 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# REA — Modul 1: Parser `docker run` → Compose (composerize)
> Status: SPEC FINAL · Fase 3 · Target kode: `internal/stack/converter.go`
> Analisis: 2026-10-15 · Metode: baca sumber + fixture test (bukan black-box)
## 1. Ringkasan & Tujuan
`composerize` mengubah perintah `docker run` menjadi file compose YAML. Di tia, logika ini menjadi
**konverter inti** yang dipakai dua-duanya: modal konverter TUI (F-008) dan `tia convert` CLI (F-028).
Yang diporting: **pipeline parsing, tabel pemetaan flag, dan aturan normalisasi**.
Yang tidak diport: fitur merge dengan file compose existing via stdin (ditangani layer stack tia
dengan cara berbeda), website, dan UI mereka.
## 2. Sumber
| Item | Nilai |
|---|---|
| Repo | https://github.com/composerize/composerize |
| Commit | `3e39eb7` (clone `--depth 1`, 2026-10-15) |
| Lisensi | **MIT** — Copyright (c) 2024 Composerize (diverifikasi dari file `LICENSE`) |
| File kunci | `packages/composerize/src/{index,logic,mappings}.js`, `cli.js`, `__tests__/*.js` |
| Dependency eksternal | `yargs-parser` (tokenizer), `composeverter` (yamlParse/yamlStringify/migrasi format), `deepmerge` |
Catatan: `composeverter` adalah paket npm terpisah (tidak ada di repo). Migrasi format
(`version: '3'` → common spec) terjadi di sana. Untuk tia kita langsung memproduksi output
common-spec, jadi bagian itu tidak perlu diport.
## 3. Alur Eksekusi
```
input (string perintah)
│
├─ 1. split multi-command: /^(?:\s*\$)?\s*(?:docker|podman)\s+/gm
│ → setiap segmen harus match ^(run|create|container run|service create),
│ kalau bukan → komentar "# ignored : docker/podman <cmd>"
│
├─ 2. normalisasi teks (urutan PENTING):
│ a. hapus komentar baris: /\n\s*#[^\n]+/g
│ b. lindungi $(...) : ganti spasi dalam $() dengan '¤'
│ c. hapus line continuation: /\\\n/g (backslash + newline)
│ d. collapse whitespace: /(\s)+/g → ' '
│ e. trim
│ f. perbaiki '-p80' → '-p 80': /\s-p(\d)/g
│ g. '\ \' (spasi-backslash-spasi) → ' ' [utf8: '\' literal]
│ h. hapus ';' di akhir: /\s*;$/g
│
├─ 3. buang prefix perintah:
│ /^(?:\s*\$\s+)?(?:docker|podman)\s+(run|create|container\s+run|service\s+create)/
│
├─ 4. tokenisasi via yargs-parser:
│ configuration: halt-at-non-option=true, camel-case-expansion=false,
│ boolean-negation=false
│ boolean flags: i interactive t tty d detach rm init help privileged
│ P publish-all no-healthcheck oom-kill-disable
│ read-only sig-proxy
│ → halt-at-non-option: semua token setelah IMAGE menjadi argumen command
│ → boolean-negation=false: '--no-healthcheck' tetap key 'no-healthcheck'
│
├─ 5. bersihkan nilai: stripQuotes() + kembalikan '¤' → spasi (dalam $())
│
├─ 6. loop setiap key → MAPPINGS[key] → ComposeEntry → deepmerge ke service
│ key tak dikenal → baris komentar "# --key=value"
│
├─ 7. image = token positional pertama (stripQuotes)
│ token positional sisanya → command: gabung dengan spasi
│
├─ 8. post-processing:
│ a. network_mode == host → hapus ports
│ b. volume source tanpa '/', '\', '$', '<', '>' → dianggap named volume
│ → top-level volumes + {external: {name}} + komentar
│ c. networks: nilai map kosong → null; semua null → jadikan array nama
│ → top-level networks {external: true, name} + komentar
│
├─ 9. bungkus: {version:'3', services:{<nama>: service}} — digabung (deepmerge)
│ dengan existing compose bila ada
│
└─ 10. stringify YAML (indent default library 4, CLI 2)
+ prepend komentar (ignored options, external network/volume)
+ migrasi format: latest → common spec (hapus version, tambah
"name: <your project name>"), v2x → "2.4", v3x → "3"
```
## 4. Struktur Data & Format I/O
**Input:** string (satu atau banyak perintah `docker/podman run/create/...`).
**Output:** string YAML, atau `SyntaxError` bila tidak ada satu pun perintah valid:
```
must have at least a valid docker/podman run/create/service create/container run command
```
**Tipe nilai flag** (`mappings.js`):
| Type | Arti |
|---|---|
| `Array` | list of string |
| `Value` | string apa adanya |
| `IntValue` / `FloatValue` | `parseInt` / `parseFloat` |
| `Switch` | boolean (`value === 'true'` \|\| `true`) |
| `Envs` | `["K=V", "K"]` — split di `=` pertama, sisanya digabung lagi |
| `Networks` | `host/bridge/none/container:*` → `network_mode`; selain itu → `networks.{nama}` |
| `Map` | nilai tunggal `k=v,k2=v2` → object (nilai numerik → int) |
| `MapArray` | seperti Map tapi tiap kemunculan flag jadi satu object (dipakai `--mount`) |
| `Ulimits` | `type=soft[:hard]` → `ulimits.<type>: int \| {soft,hard}` |
| `Gpus` | `all` \| angka → `deploy.resources.reservations.devices[{driver:nvidia,count,capabilities:[gpu]}]` |
| `DeviceBlockIOConfigWeight` | `path:weight` → `[{path, weight:int}]` |
| `DeviceBlockIOConfigRate` | `path:rate` → `[{path, rate:int\|string}]` |
**Nama service:** dari image → buang registry (`split('/')` terakhir) → buang tag
(`split(':')[0]`). Image kosong → `!!!invalid!!!`.
## 5. Aturan & Edge Case
| # | Kasus | Perilaku referensi | Catatan untuk tia |
|---|---|---|---|
| 1 | Komentar di input | Hanya dihapus jika didahului newline (`/\n\s*#/`) — komentar di baris pertama TIDAK terhapus | Tiru regex persis; uji kasus baris pertama |
| 2 | `$(pwd)`, `$(id -u)` | Spasi di dalam `$()` dilindungi `¤` agar tokenizer tidak memecah | Harus ditiru — krusial, banyak dipakai di compose nyata |
| 3 | Line continuation `\` + newline | Dihapus lalu whitespace di-collapse | Sama |
| 4 | `-p80:80` | Diperbaiki jadi `-p 80:80` (hanya untuk `-p` + digit!) | Sama. `-eFOO=bar` **tidak** diperbaiki — perlu uji differential |
| 5 | `;` di akhir | Dihapus | Sama |
| 6 | `-d` / `--detached` | Diabaikan **diam-diam** (mapping ke path `''`) | Sama |
| 7 | `--detach` (panjang) | Muncul sebagai komentar `# --detach` (inkonsisten dgn `-d`) | tia: ignore senyap keduanya (lihat §8) |
| 8 | `--rm` | Diabaikan diam-diam | Sama |
| 9 | Flag tak dikenal | Komentar `# ignored options for '<service>'` + daftar flag di atas YAML | Sama (tanpa gagal) |
| 10 | `--network host` | `network_mode: host` + ports dihapus | Sama |
| 11 | `--network appnet` | `networks: [appnet]` + top-level `networks.appnet.external: true, name` + komentar peringatan | Sama (parity dgn docker run: network harus sudah ada) |
| 12 | Named volume `-v vol:/data` | `volumes.vol.external: {name: vol}` + komentar | **Berbeda di tia** (lihat §8) |
| 13 | `--ip x` tanpa `--network` | Network fallback = `"default"` → `networks.default.ipv4_address` | Tiru; hasilnya agak aneh tapi konsisten |
| 14 | `--tmpfs` berulang | Digabung salah jadi `"/a,/b"` (String(array)) | tia: list (lihat §8) |
| 15 | `--mount ... ,readonly` | Key tanpa nilai menghasilkan value `undefined` → hilang dari YAML | tia: `readonly: true` (lihat §8) |
| 16 | Command setelah image | Token positional digabung dengan spasi (kutip hilang) | Tiru; tandai untuk uji differential |
| 17 | Kutip pada nilai | `stripQuotes` hanya membuang kutip terluar yang sepasang | Sama |
| 18 | Nilai `-` tunggal / angka | `Map`/`MapArray` mengubah string numerik jadi int | Sama |
| 19 | `--volumes-from` | Dipetakan ke `volume_from` | **BUG referensi** — compose spec = `volumes_from`. tia pakai yang benar |
| 20 | Tanpa perintah valid | `SyntaxError` dengan pesan spesifik | Pesan i18n di layer UI; inti error setara |
## 6. Perintah Docker/Compose yang Dipanggil
Tidak ada — murni transformasi string→YAML. (Verifikasi output: `docker compose config` opsional
untuk validasi di luar converter.)
## 7. Tabel Pemetaan Flag (docker run → compose)
Diambil dari `mappings.js` + short-flag alias. `[A]`=Array, `[V]`=Value, `[I]`=Int, `[F]`=Float,
`[S]`=Switch, `[E]`=Envs, `[N]`=Networks, `[M]`=Map, `[MA]`=MapArray, `[U]`=Ulimits, `[G]`=Gpus.
| docker run | compose | tipe |
|---|---|---|
| `--add-host` | `extra_hosts` | A |
| `--blkio-weight` | `blkio_config.weight` | I |
| `--blkio-weight-device` | `blkio_config.weight_device` | DeviceBlockIO |
| `--cap-add` / `--cap-drop` | `cap_add` / `cap_drop` | A |
| `--cgroup-parent` | `cgroup_parent` | V |
| `--cgroupns` | `cgroup` | V |
| `--cpu-period` `--cpu-quota` `--cpu-rt-period` `--cpu-rt-runtime` | `cpu_period` `cpu_quota` `cpu_rt_period` `cpu_rt_runtime` | V (string!) |
| `-c` `--cpu-shares` | `cpu_shares` | I |
| `--cpus` | `deploy.resources.limits.cpus` | F |
| `--device` | `devices` | A |
| `--device-cgroup-rule` | `device_cgroup_rules` | A |
| `--device-read-bps` `--device-read-iops` `--device-write-bps` `--device-write-iops` | `blkio_config.device_*` | DeviceBlockIO |
| `--dns` `--dns-opt` `--dns-search` | `dns` `dns_opt` `dns_search` | A |
| `--domainname` | `domainname` | V |
| `--entrypoint` | `entrypoint` | A (satu elemen) |
| `--env-file` | `env_file` | A |
| `-e` `--env` | `environment` | E |
| `--expose` | `expose` | A |
| `--gpus` | `deploy.resources.reservations.devices` | G |
| `--group-add` | `group_add` | A |
| `--health-cmd` `--health-interval` `--health-retries` `--health-start-period` `--health-timeout` | `healthcheck.{test,interval,retries,start_period,timeout}` | V |
| `--no-healthcheck` | `healthcheck.disable` | S |
| `-h` `--hostname` | `hostname` | V |
| `--init` | `init` | S |
| `-i` `--interactive` | `stdin_open` | S |
| `--ip` `--ip6` | `networks.<net>.ipv4_address` / `.ipv6_address` | V |
| `--ipc` | `ipc` | V |
| `--isolation` | `isolation` | V |
| `-l` `--label` | `labels` | A |
| `--link` `--link-local-ip` | `links` / `networks.<net>.link_local_ips` | A |
| `--log-driver` | `logging.driver` | V |
| `--log-opt` | `logging.options` | M |
| `--mac-address` | `mac_address` | V |
| `-m` `--memory` | `deploy.resources.limits.memory` | V |
| `--memory-reservation` | `deploy.resources.reservations.memory` | V |
| `--memory-swap` / `--memory-swappiness` | `memswap_limit` / `mem_swappiness` | V |
| `--mount` | `volumes` (long syntax object) | MA |
| `--name` | `container_name` | V |
| `--net` `--network` | `networks` / `network_mode` | N |
| `--network-alias` | `networks.<net>.aliases` | A |
| `--oom-kill-disable` | `oom_kill_disable` | S |
| `--oom-score-adj` | `oom_score_adj` | V |
| `--pid` | `pid` | V |
| `--pids-limit` | `deploy.resources.limits.pids` | I |
| `--platform` | `platform` | V |
| `--privileged` | `privileged` | S |
| `-p` `--publish` | `ports` | A |
| `--pull` | `pull_policy` | V |
| `--read-only` | `read_only` | S |
| `--restart` | `restart` | V |
| `--runtime` | `runtime` | V |
| `--security-opt` | `security_opt` | A |
| `--shm-size` | `shm_size` | V |
| `--stop-signal` | `stop_signal` | V |
| `--stop-timeout` | `stop_grace_period` | V |
| `--storage-opt` | `storage_opt` | M |
| `--sysctl` | `sysctls` | A |
| `--tmpfs` | `tmpfs` | V |
| `-t` `--tty` | `tty` | S |
| `--ulimit` | `ulimits` | U |
| `-u` `--user` | `user` | V |
| `--userns` | `userns_mode` | V |
| `--uts` | `uts` | V |
| `-v` `--volume` | `volumes` | A |
| `--volumes-from` | `volumes_from` *(ref: `volume_from` — bug)* | A |
| `-w` `--workdir` | `working_dir` | V |
| `--rm` `-d` `--detached` | *(diabaikan)* | — |
**Tidak ditangani referensi** (tia boleh menambah nanti, catat sebagai ekstensi):
`--health-start-interval`, `--annotation`, `--cidfile`, `--label-file`, `--attach`,
`-P/--publish-all`, `--sig-proxy`, `--stop-grace-period` (bukan flag docker run; yang benar `--stop-timeout`).
## 8. Test Vectors
Format target tia: **common spec** (tanpa `version:`, tanpa `name:` — nama project diurus layer
stack), indent 2, urutan key = urutan flag lalu `image` terakhir (mengikuti referensi).
### V1 — basic
```
docker run -p 80:80 foobar/baz:latest
```
```yaml
services:
baz:
ports:
- 80:80
image: foobar/baz:latest
```
### V2 — flags umum
```
docker run -d --name web -p 8080:80 -v ./html:/usr/share/nginx/html:ro -e TZ=Asia/Jakarta --restart unless-stopped nginx:latest
```
```yaml
services:
web:
container_name: web
ports:
- 8080:80
volumes:
- ./html:/usr/share/nginx/html:ro
environment:
- TZ=Asia/Jakarta
restart: unless-stopped
image: nginx:latest
```
*(nama service = `nginx` karena diambil dari image; `--name` hanya mengisi `container_name`)*
### V3 — multi-line + `$(...)`
```
docker run \
-d \
--name tor-relay \
-e TZ=Europe/London \
-e PUID=$(id -u) \
-v $(pwd)/tor/data:/data:Z \
-p 9001:9001 \
--restart always \
ilshidur/tor-relay
```
```yaml
services:
tor-relay:
container_name: tor-relay
environment:
- TZ=Europe/London
- PUID=$(id -u)
volumes:
- $(pwd)/tor/data:/data:Z
ports:
- 9001:9001
restart: always
image: ilshidur/tor-relay
```
### V4 — kutip pada nilai
```
docker run --name="foobar" -e GREETING="hello world" nginx
```
```yaml
services:
nginx:
container_name: foobar
environment:
- GREETING=hello world
image: nginx
```
### V5 — `-p80` nempel
```
docker run -p8080:80 nginx
```
```yaml
services:
nginx:
ports:
- 8080:80
image: nginx
```
### V6 — flag tak dikenal
```
docker run -d -z machin --unknown-long=truc nginx
```
```yaml
# ignored options for 'nginx'
# -z=machin
# --unknown-long=truc
services:
nginx:
image: nginx
```
### V7 — network bernama (external)
```
docker run --network appnet nginx
```
```yaml
# named network 'appnet' is marked as "external" (used by service 'nginx'), so either remove "external" from network definition or it needs to be created using: docker network create -d bridge appnet
services:
nginx:
networks:
- appnet
image: nginx
networks:
appnet:
external: true
name: appnet
```
### V8 — network mode host (ports dibuang)
```
docker run --network host -p 8080:80 nginx
```
```yaml
services:
nginx:
network_mode: host
image: nginx
```
### V9 — named volume (perilaku tia; §8 perbedaan #3)
```
docker run -v vol:/data nginx
```
```yaml
services:
nginx:
volumes:
- vol:/data
image: nginx
volumes:
vol: {}
```
### V10 — `--mount` long syntax
```
docker run --mount type=bind,source=/host,target=/container,readonly nginx
```
```yaml
services:
nginx:
volumes:
- type: bind
source: /host
target: /container
readonly: true
image: nginx
```
### V11 — healthcheck
```
docker run --health-cmd "curl -f http://localhost/" --health-interval 30s --health-retries 3 --health-timeout 5s --health-start-period 10s nginx
```
```yaml
services:
nginx:
healthcheck:
test: curl -f http://localhost/
interval: 30s
retries: 3
timeout: 5s
start_period: 10s
image: nginx
```
### V12 — log opt (berulang → satu object)
```
docker run --log-opt max-size=1g --log-opt max-file=3 --restart always nginx
```
```yaml
services:
nginx:
logging:
options:
max-size: 1g
max-file: 3
restart: always
image: nginx
```
### V13 — limits
```
docker run --memory 512m --cpus 1.5 --pids-limit 200 nginx
```
```yaml
services:
nginx:
deploy:
resources:
limits:
memory: 512m
cpus: 1.5
pids: 200
image: nginx
```
### V14 — gpus
```
docker run --gpus all nvidia/cuda:12.0-base
```
```yaml
services:
cuda:
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities:
- gpu
image: nvidia/cuda:12.0-base
```
### V15 — command setelah image
```
docker run nginx:alpine nginx -g daemon off;
```
```yaml
services:
nginx:
image: nginx:alpine
command: nginx -g daemon off
```
*(`;` akhir dihapus oleh normalisasi)*
### V16 — multi perintah + perintah diabaikan
```
docker run -p 80:80 foo/bar:latest
docker stop
docker run -p 80:80 foo/buzz:latest
```
```yaml
# ignored : docker/podman stop
services:
bar:
ports:
- 80:80
image: foo/bar:latest
buzz:
ports:
- 80:80
image: foo/buzz:latest
```
### V17 — error
```
foo bar
```
→ error (setara): `must have at least a valid docker/podman run/create/service create/container run command`
### V18 — env-file + env
```
docker run --env-file .env -e DEBUG=1 app/api:1.2
```
```yaml
services:
api:
env_file:
- .env
environment:
- DEBUG=1
image: app/api:1.2
```
### V19 — ulimit
```
docker run --ulimit nofile=1024:2048 --ulimit nproc=65535 nginx
```
```yaml
services:
nginx:
ulimits:
nofile:
soft: 1024
hard: 2048
nproc: 65535
image: nginx
```
### V20 — ip + network
```
docker run --network appnet --ip 10.0.0.5 nginx
```
```yaml
# named network 'appnet' is marked as "external" ...
services:
nginx:
networks:
appnet:
ipv4_address: 10.0.0.5
image: nginx
networks:
appnet:
external: true
name: appnet
```
> **Perlu verifikasi differential** (langsung lawan composerize asli, butuh Node):
> kutip di dalam `command`, `-eFOO=bar` (short flag nempel), `--` separator,
> `--privileged false`, path Windows (`C:\`), dan format kutip YAML-nya.
> Daftar ini masuk korpus uji paritas Fase 3.
## 9. Perbedaan yang Disengaja dari Referensi
| # | Perbedaan | Alasan |
|---|---|---|
| 1 | Output = **common spec** murni: tanpa `version:`, tanpa `name: <your project name>` | Tujuannya tia: file stack valid di Compose v2+; nama project ditentukan folder stack, bukan placeholder |
| 2 | Indent default **2** (referensi library 4, CLI 2) | Konvensi compose + samakan dengan perilaku CLI referensi |
| 3 | **Named volume tidak** di-mark `external` (`volumes: {vol: {}}`) | `docker run -v vol:/data` auto-create volume; `external` mengubah perilaku (harus sudah ada). Network tetap `external: true` karena `docker run --network X` memang menuntut X sudah ada |
| 4 | `--volumes-from` → `volumes_from` | `volume_from` adalah bug referensi (bukan field compose spec) |
| 5 | `--tmpfs` berulang → list `["/a", "/b"]` | Referensi menggabung jadi `"/a,/b"` (bug) |
| 6 | `--mount ...,readonly` → `readonly: true` | Referensi menjatuhkan key tanpa nilai |
| 7 | `-d` dan `--detach` sama-sama diabaikan senyap | Referensi inkonsisten (`-d` senyap, `--detach` komentar) |
| 8 | Tidak ada merge stdin; "tambah ke stack" ditangani layer stack (baca-tulis YAML compose) | Arsitektur tia: converter tetap murni string→string agar bisa dipakai TUI & CLI dengan hasil identik |
| 9 | Pesan error dibungkus i18n (F-023) | Prinsip tia; isi/arti error tetap sama |
## 10. Rencana Porting Go
File: `internal/stack/converter.go` (+ `converter_test.go`, `testdata/`).
```
Tokenizer (tulis sendiri, ~80 baris):
1. preprocess: komentar, $() → placeholder, line-continuation, collapse space
2. split token dgn dukungan kutip tunggal/ganda
3. klasifikasi: --opt=val | --opt val | -x val | -xval | -it (cluster) | positional
4. halt-at-non-option: setelah positional pertama → semua ke args command
Parser: map[string][]string (key → nilai; berulang jadi slice, urutan dipertahankan)
Mapper: tabel MAPPINGS di Go (key → type + path)
Builder: bangun pohon kompose secara TERURUT (yaml.Node / ordered map) → yaml.v3
```
Keputusan desain:
- **Urutan key deterministik** — map Go tidak berurutan; pakai `yaml.Node` (atau tipe ordered
map kecil) agar output stabil dan bisa dibandingkan di golden test.
- Tabel MAPPINGS ditulis **dari perilaku** (bukan copy kode JS); atribusi di `THIRD_PARTY_NOTICES.md`.
- Test vectors V1–V20 di atas dikonsumsi sebagai golden test. Differential test vs composerize
asli dijalankan di CI (`node`, npm `composerize`), YAML dinormalisasi (sort key) lalu dibandingkan;
target paritas ≥ 95% untuk flag umum — selisih yang tersisa wajib ada di tabel §9.
## 11. Verifikasi Paritas
| Item | Status |
|---|---|
| Test vectors tertulis | ✅ (V1–V20) |
| Golden test di Go | ⬜ Fase 3 |
| Differential vs composerize (Node) | ⬜ Fase 3 — perlu `node`/`npm` di CI |
| Laporan paritas | ⬜ Fase 3 |