21 KiB
REA — Modul 1: Parser docker run → Compose (composerize)
Status: SPEC FINAL · Fase 3 · Target kode:
internal/stack/converter.goAnalisis: 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-love 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
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
services:
nginx:
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
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
services:
nginx:
container_name: foobar
environment:
- GREETING=hello world
image: nginx
V5 — -p80 nempel
docker run -p8080:80 nginx
services:
nginx:
ports:
- 8080:80
image: nginx
V6 — flag tak dikenal
docker run -d -z machin --unknown-long=truc nginx
# ignored options for 'nginx'
# -z=machin
# --unknown-long=truc
services:
nginx:
image: nginx
V7 — network bernama (external)
docker run --network appnet nginx
# 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
services:
nginx:
network_mode: host
image: nginx
V9 — named volume (perilaku tia; §8 perbedaan #3)
docker run -v vol:/data nginx
services:
nginx:
volumes:
- vol:/data
image: nginx
volumes:
vol: {}
V10 — --mount long syntax
docker run --mount type=bind,source=/host,target=/container,readonly nginx
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
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
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
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
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;
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
# 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
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
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
# 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 | Image tidak ditemukan → error jelas (docker run: image tidak ditemukan) |
Referensi menghasilkan service bernama !!!invalid!!! — error tegas lebih jujur untuk CLI & TUI |
| 11 | --health-retries ditulis sebagai integer |
Skema compose mengharapkan int; referensi menulisnya sebagai string |
| 12 | Multi-perintah (beberapa docker run dalam satu input) belum didukung — menyusul |
V16 ditahan sampai dukungan multi-command masuk |
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, npmcomposerize), 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; V16 menunggu multi-command) |
| Golden test di Go | ✅ 19 vektor hijau — internal/stack/converter_test.go |
| Differential vs composerize (Node) | ⬜ lanjutan Fase 3 — perlu node/npm di CI |
| Laporan paritas | ⬜ lanjutan Fase 3 |
Catatan implementasi (ditemukan saat porting, 2026-10-16):
- V2 di dokumen ini salah tulis (service
web:→ seharusnyanginx:, sebab nama service dari image) — sudah diperbaiki; perilaku referensi mengikutigetServiceName(image). --mount ...,readonly→readonly: true(diff #6) terbukti perlu di jalur MapArray.res.Output= teks siap cetak (komentar warning + YAML) — kontrak dipakai CLItia-love convert.