579 lines
20 KiB
Markdown
579 lines
20 KiB
Markdown
# 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 |
|