Bu saytdagi lab tekshiruvchisi qanday ishlaydi
E'lon qilinmagan volume lab'ida boshlang'ich faylni o'zgartirmasdan yuboring. Bir zumda hisobot keladi: "Compose havolalari to'g'ri" tekshiruvi o'tmagan, yonida — db → pgdata volume'i e'lon qilinmagan. Terminal panelida esa tanish qator:
service "db" refers to undefined volume pgdata: invalid compose projectTabiiy savol: sayt faylimni haqiqatan docker compose up qildimi? Yo'q. Hech bir konteyner ishga tushmadi. Bu saytning eng qat'iy qoidasi — ishga tushmagan narsani ishga tushgandek ko'rsatmaslik. Fayl o'qildi, tekshirildi, terminal chiqishi esa shu fayldan hisoblandi. Shunga qaramay, yuqoridagi qator haqiqiy Compose chiqishi bilan so'zma-so'z bir xil: xuddi shu fayl Compose v2.40 da docker compose config -q bilan tekshirilganda aynan shu qator chiqadi.
Quyida fayl serverga yetib borgandan hisobot qaytgunicha bosib o'tadigan yo'lni backend/app/labs/ dagi haqiqiy kod bilan ko'rib chiqamiz.
Yo'l: fayldan hisobotgacha#
- Brauzer faqat fayl matnini yuboradi:
POST /api/labs/<slug>/submit, tanasi{"config": "..."}. Buning uchun akkaunt kerak, va bitta akkaunt daqiqasiga 30 tadan ortiq yubora olmaydi: ishonchsiz YAML'ni tahlil qilish arzon, lekin tekin emas. parse_compose()— cheklovlar vayaml.safe_load(parsing.py).evaluate()— uch qatlam tekshiruv: xavfsizlik, Compose faylni qabul qiladimi, lab vazifasi bajarilganmi (checks.py).simulate()— shu natijalardan hisoblangandocker compose up -d(simulate.py).- Urinish bazaga yoziladi: fayl, o'tdi-o'tmadi va har bir tekshiruv natijasi. Simulyatsiya yozilmaydi — kerak bo'lsa, u saqlangan fayldan qayta hisoblanadi.
Parser: ishonchsiz matnning birinchi eshigi#
Foydalanuvchi yozgan matn birinchi marta shu yerda talqin qilinadi. Shuning uchun cheklovlarning bir qismi YAML parser ishga tushishidan oldin qo'yiladi:
# A compose file for a teaching scenario is small. The cap exists to bound the
# work an alias-expansion bomb can do, not because anyone needs the space.
MAX_CONFIG_BYTES = 64 * 1024
MAX_ANCHORS = 20
MAX_ALIASES = 20
MAX_DEPTH = 12safe_load, load emas#
PyYAML'ning xavfsiz bo'lmagan loader'i (UnsafeLoader) YAML teglaridan istalgan Python obyektini quradi — funksiya chaqiruvini ham:
doc = "!!python/object/apply:os.getpid []"
yaml.load(doc, Loader=yaml.UnsafeLoader) # os.getpid() chaqiriladi, natijasi qaytadi
yaml.safe_load(doc) # ConstructorError: could not determine a constructor ...os.getpid o'rnida istalgan funksiya turishi mumkin — bu lab endpoint'ini masofadan kod bajarishga aylantirardi. PyYAML 6 da yaml.load loader'siz chaqirilmaydi, ya'ni tanlov har doim ochiq qilinadi. safe_load faqat standart YAML turlarini quradi: mapping, ro'yxat, matn, son, sana va shu kabilar.
Hajm, anchor'lar va chuqurlik#
Hajm baytlarda, parserdan oldin tekshiriladi. O'quv compose fayli bir necha kilobayt bo'ladi; 64 KB chegara joy uchun emas, parser bajaradigan ish hajmini cheklash uchun.
Anchor'lar bilan qiziqroq. YAML'da &nom qiymatga nom beradi, *nom esa uni boshqa joyda qayta ishlatadi. Compose'da bu foydali: x- bilan boshlanadigan umumiy blok bir marta yoziladi va servislarga <<: *default bilan qo'shiladi. Lekin xuddi shu mexanizm klassik hujumning ham asosi:
a: &a ["lol", "lol", "lol", "lol", "lol", "lol", "lol", "lol", "lol"]
b: &b [*a, *a, *a, *a, *a, *a, *a, *a, *a]
c: &c [*b, *b, *b, *b, *b, *b, *b, *b, *b]Har qator oldingisini to'qqiz marta takrorlaydi: uchinchi qatorda 729 ta "lol", to'qqizinchisida qariyb 390 million. Hujumning klassik XML variantida har qatorda o'nta nusxa bor va o'ninchi qator milliardga yetadi — "billion laughs" nomi shundan. Fayl bir necha yuz bayt, uni to'liq yoyib chiqish esa gigabaytlar. PyYAML alias'ni nusxalamaydi — hamma havola bitta obyektga ishora qiladi, shuning uchun o'qishning o'zi arzon. Qimmati keyin keladi: strukturani to'liq aylanib chiqadigan yoki matnga aylantiradigan kod har bir havolani alohida ko'radi.
Shuning uchun fayl obyektga aylantirilishidan oldin hodisalar oqimi sifatida o'qiladi (yaml.parse) — bu chiziqli va hech narsa qurmaydi. Shu oqimda anchor'lar (&) ham, alias'lar (*) ham sanaladi, har biriga 20 ta chegara bor. Muhimi — alias'lar: ishni ko'paytiradigan narsa havolalar, anchor'lar emas. Yuqoridagi misolda anchor atigi uchta. Tekshiruvning birinchi versiyasi faqat anchor'larni oddiy regex bilan sanardi va aynan shu teshikni o'tkazib yuborardi; hozirgi versiya hodisalar oqimiga tayanadi. Qo'shimcha himoya sifatida chuqurlikni tekshiradigan kod bitta obyektga ikkinchi marta kirmaydi.
Chuqurlik ham shu oqimda sanaladi: 12 darajadan chuqur fayl rad etiladi. Tartib bu yerda muhim: PyYAML obyekt qurayotganda har bir daraja uchun rekursiya qiladi, va bir necha kilobayt [[[[... qurish paytidayoq RecursionError beradi. Tekshiruv qurishdan keyin kelganida, foydalanuvchi tushunarli xabar o'rniga server xatosini (500) ko'rardi. Odatiy fayldagi eng chuqur yo'llardan biri — services.api.deploy.resources.limits.memory — bundan ancha qisqa.
Bu bosqichda rad etilgan fayl tekshiruvlargacha yetib bormaydi, hisobotda esa o'zbekcha xabar chiqadi:
Konfiguratsiya bo'sh.
Fayl juda katta (70000 bayt, ruxsat 65536).
YAML sintaksis xatosi (3-qator).
Konfiguratsiya ildizi obyekt (mapping) bo'lishi kerak.Bitta maydon — bir necha shakl#
Compose deyarli har bir maydonning qisqa va uzun yozuvini qabul qiladi. Tekshiruv ulardan faqat bittasini tushunsa, u shunchaki mo'rt emas, balki noto'g'ri bo'lardi: to'g'ri yechim uzun yozuvda bo'lgani uchun yiqilardi. normalize.py har bir maydonni bitta turga keltiradi. Masalan, parse_port() bu uch yozuvning har birini bitta turga — PortMapping ga (konteyner porti, host porti, host IP, protokol) aylantiradi, tekshiruvlar esa faqat shu turni solishtiradi:
ports:
- "8080:80"
- "127.0.0.1:8080:80/tcp"
- target: 80
published: 8080
host_ip: 127.0.0.1Eng nozik joyi — YAML versiyalari. PyYAML YAML 1.1 ga amal qiladi va qo'shtirnoqsiz no ni False deb o'qiydi. Compose'ning Go'dagi parseri esa uni matn sifatida qoldiradi: restart: no Compose uchun to'g'ri siyosat. O'quvchi siyosatni nazarda tutgan, shuning uchun normalizator ham shunday o'qiydi:
def restart_of(service: dict) -> str | None:
raw = service.get("restart")
if raw is None:
return None
if raw is False:
return "no"
return str(raw).lower() if isinstance(raw, bool) else str(raw)Xuddi shunday, environment dagi DEBUG: true konteynerga Python'ning True si emas, true matni bo'lib yetadi; qisqa depends_on: [db] esa condition: service_started ga teng.
Muharrir yonidagi xarita ham faylni shu qoidalar bilan o'qiydi — site/src/lib/compose-analyze.ts bu modulni aks ettiradi. Lekin u hech narsani baholamaydi: faqat yozayotganingizda Compose faylni qanday tushunishini ko'rsatadi.
Uch qatlam tekshiruv#
Har bir natija bir xil shaklda qaytadi: id, title, status, expected, actual, hint va severity. Faqat "xato" degan so'z bilan hech narsa qilib bo'lmaydi — nima kutilgani, nima topilgani va nima qilish kerakligi kerak.
Xavfsizlik: hamma lab uchun bir xil#
To'rtta tekshiruv har bir faylga qo'llanadi: faqat ruxsat etilgan rasmiy image'lar (build va nomlar fazosidagi image'lar yo'q), privileged, cap_add, devices yo'q, host namespace'lari (network_mode: host, pid: host va boshqalar) yo'q, host fayl tizimi ulanmagan. Bu qatlam "bu faylni umuman ishga tushirsa bo'ladimi?" degan savolga javob beradi. Hozir hech narsa ishga tushmaydi, lekin siyosat o'sha kun uchun yozilgan. Kod ichidagi izoh ham shuni aytadi: fayllar haqiqatan ishga tushiriladigan kuni ruxsat etilgan image'lar Docker Hub'dan emas, o'zimiz saqlaydigan joydan olinishi kerak.
Shu qatlamdan o'tmagan fayl uchun simulyatsiya Docker'ga taqlid qilmaydi — rad etayotgan tomon o'zini ochiq nomlaydi:
lab-sandbox: fayl xavfsizlik siyosatidan o'tmadi — hech bir konteyner ishga tushirilmadi.
lab-sandbox: Kengaytirilgan huquqlar yo'q: api.privileged
lab-sandbox: bu Docker xatosi emas — lab muhitimiz bunday konfiguratsiyani ishga tushirmaydi.Compose faylni qabul qiladimi#
Bu qatlam yaqinda qo'shildi. Avval baho faqat lab mavzusiga qarardi, va fayl lab'dan o'tib ketishi mumkin edi — holbuki docker compose up uni o'sha zahoti rad etardi, masalan, e'lon qilinmagan volume bilan. Endi "o'tdi" degani "Compose uni ishga tushirgan bo'lardi" degani.
| Tekshiruv | Nimani ushlaydi |
|---|---|
compose-schema | noma'lum kalit, noto'g'ri tur, condition: healthy, image yo'qligi |
compose-refs | e'lon qilinmagan volume, tarmoq yoki secret; yo'q servisga depends_on; bog'liqlik aylanasi |
compose-runtime | host port to'qnashuvi, takroriy container_name, healthcheck'siz servisni service_healthy bilan kutish, noto'g'ri restart |
image-env | Postgres, MySQL yoki MariaDB parolsiz |
Xato matnlari Compose va Engine'ning o'z matnlariga moslab yozilgan, chunki haqiqiy serverda siz aynan o'shalarni ko'rasiz va o'shalarni qidirasiz. Noma'lum kalitda hisobot yana bir qadam qo'shadi — difflib bilan eng yaqin haqiqiy kalitni topadi. enviroment xatosida maslahat shunday: enviroment → environment.
Ba'zi xatolar docker compose config dan o'tib ketadi va faqat up paytida, Engine konteyner yaratayotganda chiqadi. Masalan, restart: unless_stopped — pastki chiziq bilan. Haqiqiy Docker Engine 29 va simulyatsiya bu yerda bir xil qatorni chiqaradi:
Error response from daemon: invalid restart policy: unknown policy 'unless_stopped'; use one of 'no', 'always', 'on-failure', or 'unless-stopped'Lab vazifasi: kod emas, ma'lumot#
Uchinchi qatlam — lab'ning o'z talablari, va ular kod emas, ma'lumot. Zaxira nusxa servisi lab'ining tekshiruvlaridan bir qismi:
"checks": [
{"id": "backup-once", "type": "restart_policy", "service": "backup", "policy": "no"},
{"id": "backup-waits", "type": "depends_on_condition", "service": "backup",
"depends_on": "db", "condition": "service_healthy"},
{"id": "db-health", "type": "healthcheck_present", "service": "db"},
{"id": "backup-volume", "type": "named_volume", "service": "backup", "path": "/backups"},
# ...
],type ishlov beruvchilar ro'yxatidan (hozir 37 ta) birini tanlaydi, qolgan maydonlar uning argumentlari. Yangi lab yozish — Python emas, shunday ro'yxat yozish demak. Noma'lum tur esa jim o'tkazib yuborilmaydi:
handler = HANDLERS.get(spec.get("type", ""))
if handler is None:
# An unknown check type is an authoring bug; surfacing it beats
# silently passing a lab that was never really tested.
results.append(_bad(...)) # "Bu labning ta'rifida xatolik bor — administratorga xabar bering."
continueHar bir lab scripts/labs/verify.py dan o'tadi: boshlang'ich fayl yiqilishi, yechim o'tishi, simulyatsiya ikki marta bir xil chiqishi va uning natijasi baho bilan mos kelishi shart. Skriptning chiqish kodi — buzilgan lab'lar soni, shuning uchun u lab'larni bazaga yozishdan oldingi darvoza bo'lib ishlaydi.
Nega tekshiruvlar brauzerga yuborilmaydi#
Lab sahifasi oladigan javobda checks maydoni yo'q, va bu ataylab qilingan:
class LabDetail(LabSummary):
"""Note the absence of `checks`: the grading definitions stay server-side."""
brief: str
starter_config: strBuning uchta sababi bor:
- Tekshiruv — javoblar kaliti. Yuqoridagi
{"type": "restart_policy", "service": "backup", "policy": "no"}javobni to'g'ridan-to'g'ri aytib turibdi. Uni brauzerga yuborish lab'ni ko'chirib yozish mashqiga aylantiradi. - Baho — yozuv. Yechilgan lab'lar ro'yxati server yozgan urinishlardan olinadi. Baho brauzerda hisoblansa, "o'tdi" degani brauzer nima desa shu bo'lardi — devtools'dagi bitta so'rov bilan.
- Haqiqat manbai bitta. Hamma bir xil
evaluate()dan o'tadi. Tekshiruvdagi xato serverda bir marta tuzatiladi va hamma uchun birdaniga tuzaladi.
Yuborganingizdan keyin har bir tekshiruv natijasi expected, actual va hint bilan ko'rinadi — bu fikr-mulohaza. Lekin tekshiruv ta'rifi va uni bajaradigan kod serverdan chiqmaydi.
Simulyatsiya: raqamli egizak, ishga tushirish emas#
Terminal chiqishi, servislar holati va loglar simulate.py dan keladi. Modul hujjati birinchi qatordayoq o'zini docker compose up -d ning deterministik "raqamli egizagi" deb ataydi va darhol qo'shadi: bu yerda hech narsa konteyner ishga tushirmaydi. Simulyator Compose va Engine shu fayl bilan nima qilishini qayta o'ynaydi: ishga tushish tartibi, healthcheck, loglar — hammasi tahlil qilingan fayl va baholash natijasidan hisoblanadi. Uchta va'da bor:
- natija
upfaqat hamma tekshiruv o'tganda chiqadi; - chiqishda ko'ringan har bir nosozlik yiqilgan tekshiruvga borib taqaladi, o'tgan faylda esa hech narsa buzilgan ko'rinmaydi;
- simulyatsiya taxmin qilishga majbur bo'lgan hamma narsa izohlarda yoziladi.
Volume lab'idagi fayl uchun izohlar:
Bu simulyatsiya: hech narsa ishga tushirilmadi — natija faylingiz va tekshiruv natijalaridan hisoblandi.
`${POSTGRES_PASSWORD}` qiymatlari `.env` fayldan olingan deb hisoblandi.
Image'lar serverda oldindan yuklab olingan deb hisoblandi; log'lardagi versiya raqamlari teg bo'yicha taxminiy.Simulyator deterministik: unda soat ham, tasodif ham yo'q. Vaqt belgilangan nuqtadan qat'iy qadamlar bilan yuradi, konteyner ID'lari servis nomidan xesh orqali olinadi, server manzili esa hujjatlar uchun ajratilgan tarmoqdan:
BASE = dt.datetime(2026, 9, 21, 9, 0, 0, tzinfo=dt.timezone.utc)
# TEST-NET-3 (RFC 5737): stands in for the server's public address.
PUBLIC_IP = "203.0.113.10"
def _container_id(service: str, replica: int = 1) -> str:
return _hex(PROJECT, "container", service, replica)Bir xil fayl har doim bir xil chiqish beradi: "jonli" ko'rinsin deb qo'shilgan tasodifiy raqamlar yo'q. Shu tufayli verify.py har bir lab'ni ikki marta simulyatsiya qilib, natijalarni solishtira oladi. Interfeysda hisobot paneli ham, terminal ham "simulyatsiya" deb belgilangan.
Baho va simulyatsiyaning og'irligi har xil, va router buni kodda aytadi:
try:
simulation = simulate(report.compose, report.services, lab.checks, report, payload.config)
except Exception:
# The simulation is an explanation, the grade is the record. A bug in
# the former must not cost the learner the latter.
logger.exception("lab simulation failed lab=%s", lab.slug)
simulation = NoneSimulyatsiya nimani bilmaydi#
Ilovangiz kodini, .env dagi haqiqiy qiymatlarni va env_file ichini — ular faylda yo'q. Masalan, servisda env_file bo'lsa, baza paroli o'sha faylda bor deb hisoblanadi, chunki uni tekshirishning iloji yo'q. Shuning uchun lab'dan o'tish "compose fayli to'g'ri va xavfsiz" degani, "ilova ishlaydi" degani emas. Fayllarni izolyatsiya qilingan muhitda haqiqatan ishga tushirish — keyingi bosqich. Xavfsizlik qatlami o'sha bosqichni ko'zlab yozilgan, lekin u kelmaguncha sayt hech narsani ishga tushirgandek ko'rsatmaydi.
Amaliyot#
Har bir qatlamni o'zingiz ko'ring. Birinchi lab Compose sxemasiga, ikkinchisi havolalarga, uchinchisi Engine'ga, to'rtinchisi xavfsizlik siyosatiga urilib yiqiladi:
docker compose config, ps va logs — haqiqiy serverda nosozlikni qidirishning birinchi uch qadami: