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

20 KiB
Raw Blame History

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-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:
  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
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. 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