config_test.py 177 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421
  1. # encoding: utf-8
  2. from __future__ import absolute_import
  3. from __future__ import print_function
  4. from __future__ import unicode_literals
  5. import codecs
  6. import os
  7. import shutil
  8. import tempfile
  9. from operator import itemgetter
  10. from random import shuffle
  11. import py
  12. import pytest
  13. import yaml
  14. from ...helpers import build_config_details
  15. from compose.config import config
  16. from compose.config import types
  17. from compose.config.config import resolve_build_args
  18. from compose.config.config import resolve_environment
  19. from compose.config.environment import Environment
  20. from compose.config.errors import ConfigurationError
  21. from compose.config.errors import VERSION_EXPLANATION
  22. from compose.config.serialize import denormalize_service_dict
  23. from compose.config.serialize import serialize_config
  24. from compose.config.serialize import serialize_ns_time_value
  25. from compose.config.types import VolumeSpec
  26. from compose.const import COMPOSEFILE_V1 as V1
  27. from compose.const import COMPOSEFILE_V2_0 as V2_0
  28. from compose.const import COMPOSEFILE_V2_1 as V2_1
  29. from compose.const import COMPOSEFILE_V2_2 as V2_2
  30. from compose.const import COMPOSEFILE_V2_3 as V2_3
  31. from compose.const import COMPOSEFILE_V3_0 as V3_0
  32. from compose.const import COMPOSEFILE_V3_1 as V3_1
  33. from compose.const import COMPOSEFILE_V3_2 as V3_2
  34. from compose.const import COMPOSEFILE_V3_3 as V3_3
  35. from compose.const import COMPOSEFILE_V3_5 as V3_5
  36. from compose.const import IS_WINDOWS_PLATFORM
  37. from tests import mock
  38. from tests import unittest
  39. DEFAULT_VERSION = V2_0
  40. def make_service_dict(name, service_dict, working_dir='.', filename=None):
  41. """Test helper function to construct a ServiceExtendsResolver
  42. """
  43. resolver = config.ServiceExtendsResolver(
  44. config.ServiceConfig(
  45. working_dir=working_dir,
  46. filename=filename,
  47. name=name,
  48. config=service_dict),
  49. config.ConfigFile(filename=filename, config={}),
  50. environment=Environment.from_env_file(working_dir)
  51. )
  52. return config.process_service(resolver.run())
  53. def service_sort(services):
  54. return sorted(services, key=itemgetter('name'))
  55. def secret_sort(secrets):
  56. return sorted(secrets, key=itemgetter('source'))
  57. class ConfigTest(unittest.TestCase):
  58. def test_load(self):
  59. service_dicts = config.load(
  60. build_config_details(
  61. {
  62. 'foo': {'image': 'busybox'},
  63. 'bar': {'image': 'busybox', 'environment': ['FOO=1']},
  64. },
  65. 'tests/fixtures/extends',
  66. 'common.yml'
  67. )
  68. ).services
  69. assert service_sort(service_dicts) == service_sort([
  70. {
  71. 'name': 'bar',
  72. 'image': 'busybox',
  73. 'environment': {'FOO': '1'},
  74. },
  75. {
  76. 'name': 'foo',
  77. 'image': 'busybox',
  78. }
  79. ])
  80. def test_load_v2(self):
  81. config_data = config.load(
  82. build_config_details({
  83. 'version': '2',
  84. 'services': {
  85. 'foo': {'image': 'busybox'},
  86. 'bar': {'image': 'busybox', 'environment': ['FOO=1']},
  87. },
  88. 'volumes': {
  89. 'hello': {
  90. 'driver': 'default',
  91. 'driver_opts': {'beep': 'boop'}
  92. }
  93. },
  94. 'networks': {
  95. 'default': {
  96. 'driver': 'bridge',
  97. 'driver_opts': {'beep': 'boop'}
  98. },
  99. 'with_ipam': {
  100. 'ipam': {
  101. 'driver': 'default',
  102. 'config': [
  103. {'subnet': '172.28.0.0/16'}
  104. ]
  105. }
  106. },
  107. 'internal': {
  108. 'driver': 'bridge',
  109. 'internal': True
  110. }
  111. }
  112. }, 'working_dir', 'filename.yml')
  113. )
  114. service_dicts = config_data.services
  115. volume_dict = config_data.volumes
  116. networks_dict = config_data.networks
  117. assert service_sort(service_dicts) == service_sort([
  118. {
  119. 'name': 'bar',
  120. 'image': 'busybox',
  121. 'environment': {'FOO': '1'},
  122. },
  123. {
  124. 'name': 'foo',
  125. 'image': 'busybox',
  126. }
  127. ])
  128. assert volume_dict == {
  129. 'hello': {
  130. 'driver': 'default',
  131. 'driver_opts': {'beep': 'boop'}
  132. }
  133. }
  134. assert networks_dict == {
  135. 'default': {
  136. 'driver': 'bridge',
  137. 'driver_opts': {'beep': 'boop'}
  138. },
  139. 'with_ipam': {
  140. 'ipam': {
  141. 'driver': 'default',
  142. 'config': [
  143. {'subnet': '172.28.0.0/16'}
  144. ]
  145. }
  146. },
  147. 'internal': {
  148. 'driver': 'bridge',
  149. 'internal': True
  150. }
  151. }
  152. def test_valid_versions(self):
  153. for version in ['2', '2.0']:
  154. cfg = config.load(build_config_details({'version': version}))
  155. assert cfg.version == V2_0
  156. cfg = config.load(build_config_details({'version': '2.1'}))
  157. assert cfg.version == V2_1
  158. cfg = config.load(build_config_details({'version': '2.2'}))
  159. assert cfg.version == V2_2
  160. cfg = config.load(build_config_details({'version': '2.3'}))
  161. assert cfg.version == V2_3
  162. for version in ['3', '3.0']:
  163. cfg = config.load(build_config_details({'version': version}))
  164. assert cfg.version == V3_0
  165. cfg = config.load(build_config_details({'version': '3.1'}))
  166. assert cfg.version == V3_1
  167. def test_v1_file_version(self):
  168. cfg = config.load(build_config_details({'web': {'image': 'busybox'}}))
  169. assert cfg.version == V1
  170. assert list(s['name'] for s in cfg.services) == ['web']
  171. cfg = config.load(build_config_details({'version': {'image': 'busybox'}}))
  172. assert cfg.version == V1
  173. assert list(s['name'] for s in cfg.services) == ['version']
  174. def test_wrong_version_type(self):
  175. for version in [None, 1, 2, 2.0]:
  176. with pytest.raises(ConfigurationError) as excinfo:
  177. config.load(
  178. build_config_details(
  179. {'version': version},
  180. filename='filename.yml',
  181. )
  182. )
  183. assert 'Version in "filename.yml" is invalid - it should be a string.' \
  184. in excinfo.exconly()
  185. def test_unsupported_version(self):
  186. with pytest.raises(ConfigurationError) as excinfo:
  187. config.load(
  188. build_config_details(
  189. {'version': '2.18'},
  190. filename='filename.yml',
  191. )
  192. )
  193. assert 'Version in "filename.yml" is unsupported' in excinfo.exconly()
  194. assert VERSION_EXPLANATION in excinfo.exconly()
  195. def test_version_1_is_invalid(self):
  196. with pytest.raises(ConfigurationError) as excinfo:
  197. config.load(
  198. build_config_details(
  199. {
  200. 'version': '1',
  201. 'web': {'image': 'busybox'},
  202. },
  203. filename='filename.yml',
  204. )
  205. )
  206. assert 'Version in "filename.yml" is invalid' in excinfo.exconly()
  207. assert VERSION_EXPLANATION in excinfo.exconly()
  208. def test_v1_file_with_version_is_invalid(self):
  209. with pytest.raises(ConfigurationError) as excinfo:
  210. config.load(
  211. build_config_details(
  212. {
  213. 'version': '2',
  214. 'web': {'image': 'busybox'},
  215. },
  216. filename='filename.yml',
  217. )
  218. )
  219. assert 'Invalid top-level property "web"' in excinfo.exconly()
  220. assert VERSION_EXPLANATION in excinfo.exconly()
  221. def test_named_volume_config_empty(self):
  222. config_details = build_config_details({
  223. 'version': '2',
  224. 'services': {
  225. 'simple': {'image': 'busybox'}
  226. },
  227. 'volumes': {
  228. 'simple': None,
  229. 'other': {},
  230. }
  231. })
  232. config_result = config.load(config_details)
  233. volumes = config_result.volumes
  234. assert 'simple' in volumes
  235. assert volumes['simple'] == {}
  236. assert volumes['other'] == {}
  237. def test_named_volume_numeric_driver_opt(self):
  238. config_details = build_config_details({
  239. 'version': '2',
  240. 'services': {
  241. 'simple': {'image': 'busybox'}
  242. },
  243. 'volumes': {
  244. 'simple': {'driver_opts': {'size': 42}},
  245. }
  246. })
  247. cfg = config.load(config_details)
  248. assert cfg.volumes['simple']['driver_opts']['size'] == '42'
  249. def test_volume_invalid_driver_opt(self):
  250. config_details = build_config_details({
  251. 'version': '2',
  252. 'services': {
  253. 'simple': {'image': 'busybox'}
  254. },
  255. 'volumes': {
  256. 'simple': {'driver_opts': {'size': True}},
  257. }
  258. })
  259. with pytest.raises(ConfigurationError) as exc:
  260. config.load(config_details)
  261. assert 'driver_opts.size contains an invalid type' in exc.exconly()
  262. def test_named_volume_invalid_type_list(self):
  263. config_details = build_config_details({
  264. 'version': '2',
  265. 'services': {
  266. 'simple': {'image': 'busybox'}
  267. },
  268. 'volumes': []
  269. })
  270. with pytest.raises(ConfigurationError) as exc:
  271. config.load(config_details)
  272. assert "volume must be a mapping, not an array" in exc.exconly()
  273. def test_networks_invalid_type_list(self):
  274. config_details = build_config_details({
  275. 'version': '2',
  276. 'services': {
  277. 'simple': {'image': 'busybox'}
  278. },
  279. 'networks': []
  280. })
  281. with pytest.raises(ConfigurationError) as exc:
  282. config.load(config_details)
  283. assert "network must be a mapping, not an array" in exc.exconly()
  284. def test_load_service_with_name_version(self):
  285. with mock.patch('compose.config.config.log') as mock_logging:
  286. config_data = config.load(
  287. build_config_details({
  288. 'version': {
  289. 'image': 'busybox'
  290. }
  291. }, 'working_dir', 'filename.yml')
  292. )
  293. assert 'Unexpected type for "version" key in "filename.yml"' \
  294. in mock_logging.warning.call_args[0][0]
  295. service_dicts = config_data.services
  296. assert service_sort(service_dicts) == service_sort([
  297. {
  298. 'name': 'version',
  299. 'image': 'busybox',
  300. }
  301. ])
  302. def test_load_throws_error_when_not_dict(self):
  303. with pytest.raises(ConfigurationError):
  304. config.load(
  305. build_config_details(
  306. {'web': 'busybox:latest'},
  307. 'working_dir',
  308. 'filename.yml'
  309. )
  310. )
  311. def test_load_throws_error_when_not_dict_v2(self):
  312. with pytest.raises(ConfigurationError):
  313. config.load(
  314. build_config_details(
  315. {'version': '2', 'services': {'web': 'busybox:latest'}},
  316. 'working_dir',
  317. 'filename.yml'
  318. )
  319. )
  320. def test_load_throws_error_with_invalid_network_fields(self):
  321. with pytest.raises(ConfigurationError):
  322. config.load(
  323. build_config_details({
  324. 'version': '2',
  325. 'services': {'web': 'busybox:latest'},
  326. 'networks': {
  327. 'invalid': {'foo', 'bar'}
  328. }
  329. }, 'working_dir', 'filename.yml')
  330. )
  331. def test_load_config_link_local_ips_network(self):
  332. base_file = config.ConfigFile(
  333. 'base.yaml',
  334. {
  335. 'version': str(V2_1),
  336. 'services': {
  337. 'web': {
  338. 'image': 'example/web',
  339. 'networks': {
  340. 'foobar': {
  341. 'aliases': ['foo', 'bar'],
  342. 'link_local_ips': ['169.254.8.8']
  343. }
  344. }
  345. }
  346. },
  347. 'networks': {'foobar': {}}
  348. }
  349. )
  350. details = config.ConfigDetails('.', [base_file])
  351. web_service = config.load(details).services[0]
  352. assert web_service['networks'] == {
  353. 'foobar': {
  354. 'aliases': ['foo', 'bar'],
  355. 'link_local_ips': ['169.254.8.8']
  356. }
  357. }
  358. def test_load_config_service_labels(self):
  359. base_file = config.ConfigFile(
  360. 'base.yaml',
  361. {
  362. 'version': '2.1',
  363. 'services': {
  364. 'web': {
  365. 'image': 'example/web',
  366. 'labels': ['label_key=label_val']
  367. },
  368. 'db': {
  369. 'image': 'example/db',
  370. 'labels': {
  371. 'label_key': 'label_val'
  372. }
  373. }
  374. },
  375. }
  376. )
  377. details = config.ConfigDetails('.', [base_file])
  378. service_dicts = config.load(details).services
  379. for service in service_dicts:
  380. assert service['labels'] == {
  381. 'label_key': 'label_val'
  382. }
  383. def test_load_config_custom_resource_names(self):
  384. base_file = config.ConfigFile(
  385. 'base.yaml', {
  386. 'version': '3.5',
  387. 'volumes': {
  388. 'abc': {
  389. 'name': 'xyz'
  390. }
  391. },
  392. 'networks': {
  393. 'abc': {
  394. 'name': 'xyz'
  395. }
  396. },
  397. 'secrets': {
  398. 'abc': {
  399. 'name': 'xyz'
  400. }
  401. },
  402. 'configs': {
  403. 'abc': {
  404. 'name': 'xyz'
  405. }
  406. }
  407. }
  408. )
  409. details = config.ConfigDetails('.', [base_file])
  410. loaded_config = config.load(details)
  411. assert loaded_config.networks['abc'] == {'name': 'xyz'}
  412. assert loaded_config.volumes['abc'] == {'name': 'xyz'}
  413. assert loaded_config.secrets['abc']['name'] == 'xyz'
  414. assert loaded_config.configs['abc']['name'] == 'xyz'
  415. def test_load_config_volume_and_network_labels(self):
  416. base_file = config.ConfigFile(
  417. 'base.yaml',
  418. {
  419. 'version': '2.1',
  420. 'services': {
  421. 'web': {
  422. 'image': 'example/web',
  423. },
  424. },
  425. 'networks': {
  426. 'with_label': {
  427. 'labels': {
  428. 'label_key': 'label_val'
  429. }
  430. }
  431. },
  432. 'volumes': {
  433. 'with_label': {
  434. 'labels': {
  435. 'label_key': 'label_val'
  436. }
  437. }
  438. }
  439. }
  440. )
  441. details = config.ConfigDetails('.', [base_file])
  442. loaded_config = config.load(details)
  443. assert loaded_config.networks == {
  444. 'with_label': {
  445. 'labels': {
  446. 'label_key': 'label_val'
  447. }
  448. }
  449. }
  450. assert loaded_config.volumes == {
  451. 'with_label': {
  452. 'labels': {
  453. 'label_key': 'label_val'
  454. }
  455. }
  456. }
  457. def test_load_config_invalid_service_names(self):
  458. for invalid_name in ['?not?allowed', ' ', '', '!', '/', '\xe2']:
  459. with pytest.raises(ConfigurationError) as exc:
  460. config.load(build_config_details(
  461. {invalid_name: {'image': 'busybox'}}))
  462. assert 'Invalid service name \'%s\'' % invalid_name in exc.exconly()
  463. def test_load_config_invalid_service_names_v2(self):
  464. for invalid_name in ['?not?allowed', ' ', '', '!', '/', '\xe2']:
  465. with pytest.raises(ConfigurationError) as exc:
  466. config.load(build_config_details(
  467. {
  468. 'version': '2',
  469. 'services': {invalid_name: {'image': 'busybox'}},
  470. }))
  471. assert 'Invalid service name \'%s\'' % invalid_name in exc.exconly()
  472. def test_load_with_invalid_field_name(self):
  473. with pytest.raises(ConfigurationError) as exc:
  474. config.load(build_config_details(
  475. {
  476. 'version': '2',
  477. 'services': {
  478. 'web': {'image': 'busybox', 'name': 'bogus'},
  479. }
  480. },
  481. 'working_dir',
  482. 'filename.yml',
  483. ))
  484. assert "Unsupported config option for services.web: 'name'" in exc.exconly()
  485. def test_load_with_invalid_field_name_v1(self):
  486. with pytest.raises(ConfigurationError) as exc:
  487. config.load(build_config_details(
  488. {
  489. 'web': {'image': 'busybox', 'name': 'bogus'},
  490. },
  491. 'working_dir',
  492. 'filename.yml',
  493. ))
  494. assert "Unsupported config option for web: 'name'" in exc.exconly()
  495. def test_load_invalid_service_definition(self):
  496. config_details = build_config_details(
  497. {'web': 'wrong'},
  498. 'working_dir',
  499. 'filename.yml')
  500. with pytest.raises(ConfigurationError) as exc:
  501. config.load(config_details)
  502. assert "service 'web' must be a mapping not a string." in exc.exconly()
  503. def test_load_with_empty_build_args(self):
  504. config_details = build_config_details(
  505. {
  506. 'version': '2',
  507. 'services': {
  508. 'web': {
  509. 'build': {
  510. 'context': os.getcwd(),
  511. 'args': None,
  512. },
  513. },
  514. },
  515. }
  516. )
  517. with pytest.raises(ConfigurationError) as exc:
  518. config.load(config_details)
  519. assert (
  520. "services.web.build.args contains an invalid type, it should be an "
  521. "object, or an array" in exc.exconly()
  522. )
  523. def test_config_integer_service_name_raise_validation_error(self):
  524. with pytest.raises(ConfigurationError) as excinfo:
  525. config.load(
  526. build_config_details(
  527. {1: {'image': 'busybox'}},
  528. 'working_dir',
  529. 'filename.yml'
  530. )
  531. )
  532. assert (
  533. "In file 'filename.yml', the service name 1 must be a quoted string, i.e. '1'" in
  534. excinfo.exconly()
  535. )
  536. def test_config_integer_service_name_raise_validation_error_v2(self):
  537. with pytest.raises(ConfigurationError) as excinfo:
  538. config.load(
  539. build_config_details(
  540. {
  541. 'version': '2',
  542. 'services': {1: {'image': 'busybox'}}
  543. },
  544. 'working_dir',
  545. 'filename.yml'
  546. )
  547. )
  548. assert (
  549. "In file 'filename.yml', the service name 1 must be a quoted string, i.e. '1'." in
  550. excinfo.exconly()
  551. )
  552. def test_config_integer_service_name_raise_validation_error_v2_when_no_interpolate(self):
  553. with pytest.raises(ConfigurationError) as excinfo:
  554. config.load(
  555. build_config_details(
  556. {
  557. 'version': '2',
  558. 'services': {1: {'image': 'busybox'}}
  559. },
  560. 'working_dir',
  561. 'filename.yml'
  562. ),
  563. interpolate=False
  564. )
  565. assert (
  566. "In file 'filename.yml', the service name 1 must be a quoted string, i.e. '1'." in
  567. excinfo.exconly()
  568. )
  569. def test_config_integer_service_property_raise_validation_error(self):
  570. with pytest.raises(ConfigurationError) as excinfo:
  571. config.load(
  572. build_config_details({
  573. 'version': '2.1',
  574. 'services': {'foobar': {'image': 'busybox', 1234: 'hah'}}
  575. }, 'working_dir', 'filename.yml')
  576. )
  577. assert (
  578. "Unsupported config option for services.foobar: '1234'" in excinfo.exconly()
  579. )
  580. def test_config_invalid_service_name_raise_validation_error(self):
  581. with pytest.raises(ConfigurationError) as excinfo:
  582. config.load(
  583. build_config_details({
  584. 'version': '2',
  585. 'services': {
  586. 'test_app': {'build': '.'},
  587. 'mong\\o': {'image': 'mongo'},
  588. }
  589. })
  590. )
  591. assert 'Invalid service name \'mong\\o\'' in excinfo.exconly()
  592. def test_config_duplicate_cache_from_values_validation_error(self):
  593. with pytest.raises(ConfigurationError) as exc:
  594. config.load(
  595. build_config_details({
  596. 'version': '2.3',
  597. 'services': {
  598. 'test': {'build': {'context': '.', 'cache_from': ['a', 'b', 'a']}}
  599. }
  600. })
  601. )
  602. assert 'build.cache_from contains non-unique items' in exc.exconly()
  603. def test_load_with_multiple_files_v1(self):
  604. base_file = config.ConfigFile(
  605. 'base.yaml',
  606. {
  607. 'web': {
  608. 'image': 'example/web',
  609. 'links': ['db'],
  610. },
  611. 'db': {
  612. 'image': 'example/db',
  613. },
  614. })
  615. override_file = config.ConfigFile(
  616. 'override.yaml',
  617. {
  618. 'web': {
  619. 'build': '/',
  620. 'volumes': ['/home/user/project:/code'],
  621. },
  622. })
  623. details = config.ConfigDetails('.', [base_file, override_file])
  624. service_dicts = config.load(details).services
  625. expected = [
  626. {
  627. 'name': 'web',
  628. 'build': {'context': os.path.abspath('/')},
  629. 'volumes': [VolumeSpec.parse('/home/user/project:/code')],
  630. 'links': ['db'],
  631. },
  632. {
  633. 'name': 'db',
  634. 'image': 'example/db',
  635. },
  636. ]
  637. assert service_sort(service_dicts) == service_sort(expected)
  638. def test_load_with_multiple_files_and_empty_override(self):
  639. base_file = config.ConfigFile(
  640. 'base.yml',
  641. {'web': {'image': 'example/web'}})
  642. override_file = config.ConfigFile('override.yml', None)
  643. details = config.ConfigDetails('.', [base_file, override_file])
  644. with pytest.raises(ConfigurationError) as exc:
  645. config.load(details)
  646. error_msg = "Top level object in 'override.yml' needs to be an object"
  647. assert error_msg in exc.exconly()
  648. def test_load_with_multiple_files_and_empty_override_v2(self):
  649. base_file = config.ConfigFile(
  650. 'base.yml',
  651. {'version': '2', 'services': {'web': {'image': 'example/web'}}})
  652. override_file = config.ConfigFile('override.yml', None)
  653. details = config.ConfigDetails('.', [base_file, override_file])
  654. with pytest.raises(ConfigurationError) as exc:
  655. config.load(details)
  656. error_msg = "Top level object in 'override.yml' needs to be an object"
  657. assert error_msg in exc.exconly()
  658. def test_load_with_multiple_files_and_empty_base(self):
  659. base_file = config.ConfigFile('base.yml', None)
  660. override_file = config.ConfigFile(
  661. 'override.yml',
  662. {'web': {'image': 'example/web'}})
  663. details = config.ConfigDetails('.', [base_file, override_file])
  664. with pytest.raises(ConfigurationError) as exc:
  665. config.load(details)
  666. assert "Top level object in 'base.yml' needs to be an object" in exc.exconly()
  667. def test_load_with_multiple_files_and_empty_base_v2(self):
  668. base_file = config.ConfigFile('base.yml', None)
  669. override_file = config.ConfigFile(
  670. 'override.tml',
  671. {'version': '2', 'services': {'web': {'image': 'example/web'}}}
  672. )
  673. details = config.ConfigDetails('.', [base_file, override_file])
  674. with pytest.raises(ConfigurationError) as exc:
  675. config.load(details)
  676. assert "Top level object in 'base.yml' needs to be an object" in exc.exconly()
  677. def test_load_with_multiple_files_and_extends_in_override_file(self):
  678. base_file = config.ConfigFile(
  679. 'base.yaml',
  680. {
  681. 'web': {'image': 'example/web'},
  682. })
  683. override_file = config.ConfigFile(
  684. 'override.yaml',
  685. {
  686. 'web': {
  687. 'extends': {
  688. 'file': 'common.yml',
  689. 'service': 'base',
  690. },
  691. 'volumes': ['/home/user/project:/code'],
  692. },
  693. })
  694. details = config.ConfigDetails('.', [base_file, override_file])
  695. tmpdir = py.test.ensuretemp('config_test')
  696. self.addCleanup(tmpdir.remove)
  697. tmpdir.join('common.yml').write("""
  698. base:
  699. labels: ['label=one']
  700. """)
  701. with tmpdir.as_cwd():
  702. service_dicts = config.load(details).services
  703. expected = [
  704. {
  705. 'name': 'web',
  706. 'image': 'example/web',
  707. 'volumes': [VolumeSpec.parse('/home/user/project:/code')],
  708. 'labels': {'label': 'one'},
  709. },
  710. ]
  711. assert service_sort(service_dicts) == service_sort(expected)
  712. def test_load_mixed_extends_resolution(self):
  713. main_file = config.ConfigFile(
  714. 'main.yml', {
  715. 'version': '2.2',
  716. 'services': {
  717. 'prodweb': {
  718. 'extends': {
  719. 'service': 'web',
  720. 'file': 'base.yml'
  721. },
  722. 'environment': {'PROD': 'true'},
  723. },
  724. },
  725. }
  726. )
  727. tmpdir = pytest.ensuretemp('config_test')
  728. self.addCleanup(tmpdir.remove)
  729. tmpdir.join('base.yml').write("""
  730. version: '2.2'
  731. services:
  732. base:
  733. image: base
  734. web:
  735. extends: base
  736. """)
  737. details = config.ConfigDetails('.', [main_file])
  738. with tmpdir.as_cwd():
  739. service_dicts = config.load(details).services
  740. assert service_dicts[0] == {
  741. 'name': 'prodweb',
  742. 'image': 'base',
  743. 'environment': {'PROD': 'true'},
  744. }
  745. def test_load_with_multiple_files_and_invalid_override(self):
  746. base_file = config.ConfigFile(
  747. 'base.yaml',
  748. {'web': {'image': 'example/web'}})
  749. override_file = config.ConfigFile(
  750. 'override.yaml',
  751. {'bogus': 'thing'})
  752. details = config.ConfigDetails('.', [base_file, override_file])
  753. with pytest.raises(ConfigurationError) as exc:
  754. config.load(details)
  755. assert "service 'bogus' must be a mapping not a string." in exc.exconly()
  756. assert "In file 'override.yaml'" in exc.exconly()
  757. def test_load_sorts_in_dependency_order(self):
  758. config_details = build_config_details({
  759. 'web': {
  760. 'image': 'busybox:latest',
  761. 'links': ['db'],
  762. },
  763. 'db': {
  764. 'image': 'busybox:latest',
  765. 'volumes_from': ['volume:ro']
  766. },
  767. 'volume': {
  768. 'image': 'busybox:latest',
  769. 'volumes': ['/tmp'],
  770. }
  771. })
  772. services = config.load(config_details).services
  773. assert services[0]['name'] == 'volume'
  774. assert services[1]['name'] == 'db'
  775. assert services[2]['name'] == 'web'
  776. def test_load_with_extensions(self):
  777. config_details = build_config_details({
  778. 'version': '2.3',
  779. 'x-data': {
  780. 'lambda': 3,
  781. 'excess': [True, {}]
  782. }
  783. })
  784. config_data = config.load(config_details)
  785. assert config_data.services == []
  786. def test_config_build_configuration(self):
  787. service = config.load(
  788. build_config_details(
  789. {'web': {
  790. 'build': '.',
  791. 'dockerfile': 'Dockerfile-alt'
  792. }},
  793. 'tests/fixtures/extends',
  794. 'filename.yml'
  795. )
  796. ).services
  797. assert 'context' in service[0]['build']
  798. assert service[0]['build']['dockerfile'] == 'Dockerfile-alt'
  799. def test_config_build_configuration_v2(self):
  800. # service.dockerfile is invalid in v2
  801. with pytest.raises(ConfigurationError):
  802. config.load(
  803. build_config_details(
  804. {
  805. 'version': '2',
  806. 'services': {
  807. 'web': {
  808. 'build': '.',
  809. 'dockerfile': 'Dockerfile-alt'
  810. }
  811. }
  812. },
  813. 'tests/fixtures/extends',
  814. 'filename.yml'
  815. )
  816. )
  817. service = config.load(
  818. build_config_details({
  819. 'version': '2',
  820. 'services': {
  821. 'web': {
  822. 'build': '.'
  823. }
  824. }
  825. }, 'tests/fixtures/extends', 'filename.yml')
  826. ).services[0]
  827. assert 'context' in service['build']
  828. service = config.load(
  829. build_config_details(
  830. {
  831. 'version': '2',
  832. 'services': {
  833. 'web': {
  834. 'build': {
  835. 'context': '.',
  836. 'dockerfile': 'Dockerfile-alt'
  837. }
  838. }
  839. }
  840. },
  841. 'tests/fixtures/extends',
  842. 'filename.yml'
  843. )
  844. ).services
  845. assert 'context' in service[0]['build']
  846. assert service[0]['build']['dockerfile'] == 'Dockerfile-alt'
  847. def test_load_with_buildargs(self):
  848. service = config.load(
  849. build_config_details(
  850. {
  851. 'version': '2',
  852. 'services': {
  853. 'web': {
  854. 'build': {
  855. 'context': '.',
  856. 'dockerfile': 'Dockerfile-alt',
  857. 'args': {
  858. 'opt1': 42,
  859. 'opt2': 'foobar'
  860. }
  861. }
  862. }
  863. }
  864. },
  865. 'tests/fixtures/extends',
  866. 'filename.yml'
  867. )
  868. ).services[0]
  869. assert 'args' in service['build']
  870. assert 'opt1' in service['build']['args']
  871. assert isinstance(service['build']['args']['opt1'], str)
  872. assert service['build']['args']['opt1'] == '42'
  873. assert service['build']['args']['opt2'] == 'foobar'
  874. def test_load_build_labels_dict(self):
  875. service = config.load(
  876. build_config_details(
  877. {
  878. 'version': str(V3_3),
  879. 'services': {
  880. 'web': {
  881. 'build': {
  882. 'context': '.',
  883. 'dockerfile': 'Dockerfile-alt',
  884. 'labels': {
  885. 'label1': 42,
  886. 'label2': 'foobar'
  887. }
  888. }
  889. }
  890. }
  891. },
  892. 'tests/fixtures/extends',
  893. 'filename.yml'
  894. )
  895. ).services[0]
  896. assert 'labels' in service['build']
  897. assert 'label1' in service['build']['labels']
  898. assert service['build']['labels']['label1'] == '42'
  899. assert service['build']['labels']['label2'] == 'foobar'
  900. def test_load_build_labels_list(self):
  901. base_file = config.ConfigFile(
  902. 'base.yml',
  903. {
  904. 'version': '2.3',
  905. 'services': {
  906. 'web': {
  907. 'build': {
  908. 'context': '.',
  909. 'labels': ['foo=bar', 'baz=true', 'foobar=1']
  910. },
  911. },
  912. },
  913. }
  914. )
  915. details = config.ConfigDetails('.', [base_file])
  916. service = config.load(details).services[0]
  917. assert service['build']['labels'] == {
  918. 'foo': 'bar', 'baz': 'true', 'foobar': '1'
  919. }
  920. def test_build_args_allow_empty_properties(self):
  921. service = config.load(
  922. build_config_details(
  923. {
  924. 'version': '2',
  925. 'services': {
  926. 'web': {
  927. 'build': {
  928. 'context': '.',
  929. 'dockerfile': 'Dockerfile-alt',
  930. 'args': {
  931. 'foo': None
  932. }
  933. }
  934. }
  935. }
  936. },
  937. 'tests/fixtures/extends',
  938. 'filename.yml'
  939. )
  940. ).services[0]
  941. assert 'args' in service['build']
  942. assert 'foo' in service['build']['args']
  943. assert service['build']['args']['foo'] == ''
  944. # If build argument is None then it will be converted to the empty
  945. # string. Make sure that int zero kept as it is, i.e. not converted to
  946. # the empty string
  947. def test_build_args_check_zero_preserved(self):
  948. service = config.load(
  949. build_config_details(
  950. {
  951. 'version': '2',
  952. 'services': {
  953. 'web': {
  954. 'build': {
  955. 'context': '.',
  956. 'dockerfile': 'Dockerfile-alt',
  957. 'args': {
  958. 'foo': 0
  959. }
  960. }
  961. }
  962. }
  963. },
  964. 'tests/fixtures/extends',
  965. 'filename.yml'
  966. )
  967. ).services[0]
  968. assert 'args' in service['build']
  969. assert 'foo' in service['build']['args']
  970. assert service['build']['args']['foo'] == '0'
  971. def test_load_with_multiple_files_mismatched_networks_format(self):
  972. base_file = config.ConfigFile(
  973. 'base.yaml',
  974. {
  975. 'version': '2',
  976. 'services': {
  977. 'web': {
  978. 'image': 'example/web',
  979. 'networks': {
  980. 'foobar': {'aliases': ['foo', 'bar']}
  981. }
  982. }
  983. },
  984. 'networks': {'foobar': {}, 'baz': {}}
  985. }
  986. )
  987. override_file = config.ConfigFile(
  988. 'override.yaml',
  989. {
  990. 'version': '2',
  991. 'services': {
  992. 'web': {
  993. 'networks': ['baz']
  994. }
  995. }
  996. }
  997. )
  998. details = config.ConfigDetails('.', [base_file, override_file])
  999. web_service = config.load(details).services[0]
  1000. assert web_service['networks'] == {
  1001. 'foobar': {'aliases': ['bar', 'foo']},
  1002. 'baz': {}
  1003. }
  1004. def test_load_with_multiple_files_mismatched_networks_format_inverse_order(self):
  1005. base_file = config.ConfigFile(
  1006. 'override.yaml',
  1007. {
  1008. 'version': '2',
  1009. 'services': {
  1010. 'web': {
  1011. 'networks': ['baz']
  1012. }
  1013. }
  1014. }
  1015. )
  1016. override_file = config.ConfigFile(
  1017. 'base.yaml',
  1018. {
  1019. 'version': '2',
  1020. 'services': {
  1021. 'web': {
  1022. 'image': 'example/web',
  1023. 'networks': {
  1024. 'foobar': {'aliases': ['foo', 'bar']}
  1025. }
  1026. }
  1027. },
  1028. 'networks': {'foobar': {}, 'baz': {}}
  1029. }
  1030. )
  1031. details = config.ConfigDetails('.', [base_file, override_file])
  1032. web_service = config.load(details).services[0]
  1033. assert web_service['networks'] == {
  1034. 'foobar': {'aliases': ['bar', 'foo']},
  1035. 'baz': {}
  1036. }
  1037. def test_load_with_multiple_files_v2(self):
  1038. base_file = config.ConfigFile(
  1039. 'base.yaml',
  1040. {
  1041. 'version': '2',
  1042. 'services': {
  1043. 'web': {
  1044. 'image': 'example/web',
  1045. 'depends_on': ['db'],
  1046. },
  1047. 'db': {
  1048. 'image': 'example/db',
  1049. }
  1050. },
  1051. })
  1052. override_file = config.ConfigFile(
  1053. 'override.yaml',
  1054. {
  1055. 'version': '2',
  1056. 'services': {
  1057. 'web': {
  1058. 'build': '/',
  1059. 'volumes': ['/home/user/project:/code'],
  1060. 'depends_on': ['other'],
  1061. },
  1062. 'other': {
  1063. 'image': 'example/other',
  1064. }
  1065. }
  1066. })
  1067. details = config.ConfigDetails('.', [base_file, override_file])
  1068. service_dicts = config.load(details).services
  1069. expected = [
  1070. {
  1071. 'name': 'web',
  1072. 'build': {'context': os.path.abspath('/')},
  1073. 'image': 'example/web',
  1074. 'volumes': [VolumeSpec.parse('/home/user/project:/code')],
  1075. 'depends_on': {
  1076. 'db': {'condition': 'service_started'},
  1077. 'other': {'condition': 'service_started'},
  1078. },
  1079. },
  1080. {
  1081. 'name': 'db',
  1082. 'image': 'example/db',
  1083. },
  1084. {
  1085. 'name': 'other',
  1086. 'image': 'example/other',
  1087. },
  1088. ]
  1089. assert service_sort(service_dicts) == service_sort(expected)
  1090. @mock.patch.dict(os.environ)
  1091. def test_load_with_multiple_files_v3_2(self):
  1092. os.environ['COMPOSE_CONVERT_WINDOWS_PATHS'] = 'true'
  1093. base_file = config.ConfigFile(
  1094. 'base.yaml',
  1095. {
  1096. 'version': '3.2',
  1097. 'services': {
  1098. 'web': {
  1099. 'image': 'example/web',
  1100. 'volumes': [
  1101. {'source': '/a', 'target': '/b', 'type': 'bind'},
  1102. {'source': 'vol', 'target': '/x', 'type': 'volume', 'read_only': True}
  1103. ],
  1104. 'stop_grace_period': '30s',
  1105. }
  1106. },
  1107. 'volumes': {'vol': {}}
  1108. }
  1109. )
  1110. override_file = config.ConfigFile(
  1111. 'override.yaml',
  1112. {
  1113. 'version': '3.2',
  1114. 'services': {
  1115. 'web': {
  1116. 'volumes': ['/c:/b', '/anonymous']
  1117. }
  1118. }
  1119. }
  1120. )
  1121. details = config.ConfigDetails('.', [base_file, override_file])
  1122. service_dicts = config.load(details).services
  1123. svc_volumes = map(lambda v: v.repr(), service_dicts[0]['volumes'])
  1124. for vol in svc_volumes:
  1125. assert vol in [
  1126. '/anonymous',
  1127. '/c:/b:rw',
  1128. {'source': 'vol', 'target': '/x', 'type': 'volume', 'read_only': True}
  1129. ]
  1130. assert service_dicts[0]['stop_grace_period'] == '30s'
  1131. @mock.patch.dict(os.environ)
  1132. def test_volume_mode_override(self):
  1133. os.environ['COMPOSE_CONVERT_WINDOWS_PATHS'] = 'true'
  1134. base_file = config.ConfigFile(
  1135. 'base.yaml',
  1136. {
  1137. 'version': '2.3',
  1138. 'services': {
  1139. 'web': {
  1140. 'image': 'example/web',
  1141. 'volumes': ['/c:/b:rw']
  1142. }
  1143. },
  1144. }
  1145. )
  1146. override_file = config.ConfigFile(
  1147. 'override.yaml',
  1148. {
  1149. 'version': '2.3',
  1150. 'services': {
  1151. 'web': {
  1152. 'volumes': ['/c:/b:ro']
  1153. }
  1154. }
  1155. }
  1156. )
  1157. details = config.ConfigDetails('.', [base_file, override_file])
  1158. service_dicts = config.load(details).services
  1159. svc_volumes = list(map(lambda v: v.repr(), service_dicts[0]['volumes']))
  1160. assert svc_volumes == ['/c:/b:ro']
  1161. def test_undeclared_volume_v2(self):
  1162. base_file = config.ConfigFile(
  1163. 'base.yaml',
  1164. {
  1165. 'version': '2',
  1166. 'services': {
  1167. 'web': {
  1168. 'image': 'busybox:latest',
  1169. 'volumes': ['data0028:/data:ro'],
  1170. },
  1171. },
  1172. }
  1173. )
  1174. details = config.ConfigDetails('.', [base_file])
  1175. with pytest.raises(ConfigurationError):
  1176. config.load(details)
  1177. base_file = config.ConfigFile(
  1178. 'base.yaml',
  1179. {
  1180. 'version': '2',
  1181. 'services': {
  1182. 'web': {
  1183. 'image': 'busybox:latest',
  1184. 'volumes': ['./data0028:/data:ro'],
  1185. },
  1186. },
  1187. }
  1188. )
  1189. details = config.ConfigDetails('.', [base_file])
  1190. config_data = config.load(details)
  1191. volume = config_data.services[0].get('volumes')[0]
  1192. assert not volume.is_named_volume
  1193. def test_undeclared_volume_v1(self):
  1194. base_file = config.ConfigFile(
  1195. 'base.yaml',
  1196. {
  1197. 'web': {
  1198. 'image': 'busybox:latest',
  1199. 'volumes': ['data0028:/data:ro'],
  1200. },
  1201. }
  1202. )
  1203. details = config.ConfigDetails('.', [base_file])
  1204. config_data = config.load(details)
  1205. volume = config_data.services[0].get('volumes')[0]
  1206. assert volume.external == 'data0028'
  1207. assert volume.is_named_volume
  1208. def test_volumes_long_syntax(self):
  1209. base_file = config.ConfigFile(
  1210. 'base.yaml', {
  1211. 'version': '2.3',
  1212. 'services': {
  1213. 'web': {
  1214. 'image': 'busybox:latest',
  1215. 'volumes': [
  1216. {
  1217. 'target': '/anonymous', 'type': 'volume'
  1218. }, {
  1219. 'source': '/abc', 'target': '/xyz', 'type': 'bind'
  1220. }, {
  1221. 'source': '\\\\.\\pipe\\abcd', 'target': '/named_pipe', 'type': 'npipe'
  1222. }, {
  1223. 'type': 'tmpfs', 'target': '/tmpfs'
  1224. }
  1225. ]
  1226. },
  1227. },
  1228. },
  1229. )
  1230. details = config.ConfigDetails('.', [base_file])
  1231. config_data = config.load(details)
  1232. volumes = config_data.services[0].get('volumes')
  1233. anon_volume = [v for v in volumes if v.target == '/anonymous'][0]
  1234. tmpfs_mount = [v for v in volumes if v.type == 'tmpfs'][0]
  1235. host_mount = [v for v in volumes if v.type == 'bind'][0]
  1236. npipe_mount = [v for v in volumes if v.type == 'npipe'][0]
  1237. assert anon_volume.type == 'volume'
  1238. assert not anon_volume.is_named_volume
  1239. assert tmpfs_mount.target == '/tmpfs'
  1240. assert not tmpfs_mount.is_named_volume
  1241. assert host_mount.source == '/abc'
  1242. assert host_mount.target == '/xyz'
  1243. assert not host_mount.is_named_volume
  1244. assert npipe_mount.source == '\\\\.\\pipe\\abcd'
  1245. assert npipe_mount.target == '/named_pipe'
  1246. assert not npipe_mount.is_named_volume
  1247. def test_load_bind_mount_relative_path(self):
  1248. expected_source = 'C:\\tmp\\web' if IS_WINDOWS_PLATFORM else '/tmp/web'
  1249. base_file = config.ConfigFile(
  1250. 'base.yaml', {
  1251. 'version': '3.4',
  1252. 'services': {
  1253. 'web': {
  1254. 'image': 'busybox:latest',
  1255. 'volumes': [
  1256. {'type': 'bind', 'source': './web', 'target': '/web'},
  1257. ],
  1258. },
  1259. },
  1260. },
  1261. )
  1262. details = config.ConfigDetails('/tmp', [base_file])
  1263. config_data = config.load(details)
  1264. mount = config_data.services[0].get('volumes')[0]
  1265. assert mount.target == '/web'
  1266. assert mount.type == 'bind'
  1267. assert mount.source == expected_source
  1268. def test_load_bind_mount_relative_path_with_tilde(self):
  1269. base_file = config.ConfigFile(
  1270. 'base.yaml', {
  1271. 'version': '3.4',
  1272. 'services': {
  1273. 'web': {
  1274. 'image': 'busybox:latest',
  1275. 'volumes': [
  1276. {'type': 'bind', 'source': '~/web', 'target': '/web'},
  1277. ],
  1278. },
  1279. },
  1280. },
  1281. )
  1282. details = config.ConfigDetails('.', [base_file])
  1283. config_data = config.load(details)
  1284. mount = config_data.services[0].get('volumes')[0]
  1285. assert mount.target == '/web'
  1286. assert mount.type == 'bind'
  1287. assert (
  1288. not mount.source.startswith('~') and mount.source.endswith(
  1289. '{}web'.format(os.path.sep)
  1290. )
  1291. )
  1292. def test_config_invalid_ipam_config(self):
  1293. with pytest.raises(ConfigurationError) as excinfo:
  1294. config.load(
  1295. build_config_details(
  1296. {
  1297. 'version': str(V2_1),
  1298. 'networks': {
  1299. 'foo': {
  1300. 'driver': 'default',
  1301. 'ipam': {
  1302. 'driver': 'default',
  1303. 'config': ['172.18.0.0/16'],
  1304. }
  1305. }
  1306. }
  1307. },
  1308. filename='filename.yml',
  1309. )
  1310. )
  1311. assert ('networks.foo.ipam.config contains an invalid type,'
  1312. ' it should be an object') in excinfo.exconly()
  1313. def test_config_valid_ipam_config(self):
  1314. ipam_config = {
  1315. 'subnet': '172.28.0.0/16',
  1316. 'ip_range': '172.28.5.0/24',
  1317. 'gateway': '172.28.5.254',
  1318. 'aux_addresses': {
  1319. 'host1': '172.28.1.5',
  1320. 'host2': '172.28.1.6',
  1321. 'host3': '172.28.1.7',
  1322. },
  1323. }
  1324. networks = config.load(
  1325. build_config_details(
  1326. {
  1327. 'version': str(V2_1),
  1328. 'networks': {
  1329. 'foo': {
  1330. 'driver': 'default',
  1331. 'ipam': {
  1332. 'driver': 'default',
  1333. 'config': [ipam_config],
  1334. }
  1335. }
  1336. }
  1337. },
  1338. filename='filename.yml',
  1339. )
  1340. ).networks
  1341. assert 'foo' in networks
  1342. assert networks['foo']['ipam']['config'] == [ipam_config]
  1343. def test_config_valid_service_names(self):
  1344. for valid_name in ['_', '-', '.__.', '_what-up.', 'what_.up----', 'whatup']:
  1345. services = config.load(
  1346. build_config_details(
  1347. {valid_name: {'image': 'busybox'}},
  1348. 'tests/fixtures/extends',
  1349. 'common.yml')).services
  1350. assert services[0]['name'] == valid_name
  1351. def test_config_hint(self):
  1352. with pytest.raises(ConfigurationError) as excinfo:
  1353. config.load(
  1354. build_config_details(
  1355. {
  1356. 'foo': {'image': 'busybox', 'privilige': 'something'},
  1357. },
  1358. 'tests/fixtures/extends',
  1359. 'filename.yml'
  1360. )
  1361. )
  1362. assert "(did you mean 'privileged'?)" in excinfo.exconly()
  1363. def test_load_errors_on_uppercase_with_no_image(self):
  1364. with pytest.raises(ConfigurationError) as exc:
  1365. config.load(build_config_details({
  1366. 'Foo': {'build': '.'},
  1367. }, 'tests/fixtures/build-ctx'))
  1368. assert "Service 'Foo' contains uppercase characters" in exc.exconly()
  1369. def test_invalid_config_v1(self):
  1370. with pytest.raises(ConfigurationError) as excinfo:
  1371. config.load(
  1372. build_config_details(
  1373. {
  1374. 'foo': {'image': 1},
  1375. },
  1376. 'tests/fixtures/extends',
  1377. 'filename.yml'
  1378. )
  1379. )
  1380. assert "foo.image contains an invalid type, it should be a string" \
  1381. in excinfo.exconly()
  1382. def test_invalid_config_v2(self):
  1383. with pytest.raises(ConfigurationError) as excinfo:
  1384. config.load(
  1385. build_config_details(
  1386. {
  1387. 'version': '2',
  1388. 'services': {
  1389. 'foo': {'image': 1},
  1390. },
  1391. },
  1392. 'tests/fixtures/extends',
  1393. 'filename.yml'
  1394. )
  1395. )
  1396. assert "services.foo.image contains an invalid type, it should be a string" \
  1397. in excinfo.exconly()
  1398. def test_invalid_config_build_and_image_specified_v1(self):
  1399. with pytest.raises(ConfigurationError) as excinfo:
  1400. config.load(
  1401. build_config_details(
  1402. {
  1403. 'foo': {'image': 'busybox', 'build': '.'},
  1404. },
  1405. 'tests/fixtures/extends',
  1406. 'filename.yml'
  1407. )
  1408. )
  1409. assert "foo has both an image and build path specified." in excinfo.exconly()
  1410. def test_invalid_config_type_should_be_an_array(self):
  1411. with pytest.raises(ConfigurationError) as excinfo:
  1412. config.load(
  1413. build_config_details(
  1414. {
  1415. 'foo': {'image': 'busybox', 'links': 'an_link'},
  1416. },
  1417. 'tests/fixtures/extends',
  1418. 'filename.yml'
  1419. )
  1420. )
  1421. assert "foo.links contains an invalid type, it should be an array" \
  1422. in excinfo.exconly()
  1423. def test_invalid_config_not_a_dictionary(self):
  1424. with pytest.raises(ConfigurationError) as excinfo:
  1425. config.load(
  1426. build_config_details(
  1427. ['foo', 'lol'],
  1428. 'tests/fixtures/extends',
  1429. 'filename.yml'
  1430. )
  1431. )
  1432. assert "Top level object in 'filename.yml' needs to be an object" \
  1433. in excinfo.exconly()
  1434. def test_invalid_config_not_unique_items(self):
  1435. with pytest.raises(ConfigurationError) as excinfo:
  1436. config.load(
  1437. build_config_details(
  1438. {
  1439. 'web': {'build': '.', 'devices': ['/dev/foo:/dev/foo', '/dev/foo:/dev/foo']}
  1440. },
  1441. 'tests/fixtures/extends',
  1442. 'filename.yml'
  1443. )
  1444. )
  1445. assert "has non-unique elements" in excinfo.exconly()
  1446. def test_invalid_list_of_strings_format(self):
  1447. with pytest.raises(ConfigurationError) as excinfo:
  1448. config.load(
  1449. build_config_details(
  1450. {
  1451. 'web': {'build': '.', 'command': [1]}
  1452. },
  1453. 'tests/fixtures/extends',
  1454. 'filename.yml'
  1455. )
  1456. )
  1457. assert "web.command contains 1, which is an invalid type, it should be a string" \
  1458. in excinfo.exconly()
  1459. def test_load_config_dockerfile_without_build_raises_error_v1(self):
  1460. with pytest.raises(ConfigurationError) as exc:
  1461. config.load(build_config_details({
  1462. 'web': {
  1463. 'image': 'busybox',
  1464. 'dockerfile': 'Dockerfile.alt'
  1465. }
  1466. }))
  1467. assert "web has both an image and alternate Dockerfile." in exc.exconly()
  1468. def test_config_extra_hosts_string_raises_validation_error(self):
  1469. with pytest.raises(ConfigurationError) as excinfo:
  1470. config.load(
  1471. build_config_details(
  1472. {'web': {
  1473. 'image': 'busybox',
  1474. 'extra_hosts': 'somehost:162.242.195.82'
  1475. }},
  1476. 'working_dir',
  1477. 'filename.yml'
  1478. )
  1479. )
  1480. assert "web.extra_hosts contains an invalid type" \
  1481. in excinfo.exconly()
  1482. def test_config_extra_hosts_list_of_dicts_validation_error(self):
  1483. with pytest.raises(ConfigurationError) as excinfo:
  1484. config.load(
  1485. build_config_details(
  1486. {'web': {
  1487. 'image': 'busybox',
  1488. 'extra_hosts': [
  1489. {'somehost': '162.242.195.82'},
  1490. {'otherhost': '50.31.209.229'}
  1491. ]
  1492. }},
  1493. 'working_dir',
  1494. 'filename.yml'
  1495. )
  1496. )
  1497. assert "web.extra_hosts contains {\"somehost\": \"162.242.195.82\"}, " \
  1498. "which is an invalid type, it should be a string" \
  1499. in excinfo.exconly()
  1500. def test_config_ulimits_invalid_keys_validation_error(self):
  1501. with pytest.raises(ConfigurationError) as exc:
  1502. config.load(build_config_details(
  1503. {
  1504. 'web': {
  1505. 'image': 'busybox',
  1506. 'ulimits': {
  1507. 'nofile': {
  1508. "not_soft_or_hard": 100,
  1509. "soft": 10000,
  1510. "hard": 20000,
  1511. }
  1512. }
  1513. }
  1514. },
  1515. 'working_dir',
  1516. 'filename.yml'))
  1517. assert "web.ulimits.nofile contains unsupported option: 'not_soft_or_hard'" \
  1518. in exc.exconly()
  1519. def test_config_ulimits_required_keys_validation_error(self):
  1520. with pytest.raises(ConfigurationError) as exc:
  1521. config.load(build_config_details(
  1522. {
  1523. 'web': {
  1524. 'image': 'busybox',
  1525. 'ulimits': {'nofile': {"soft": 10000}}
  1526. }
  1527. },
  1528. 'working_dir',
  1529. 'filename.yml'))
  1530. assert "web.ulimits.nofile" in exc.exconly()
  1531. assert "'hard' is a required property" in exc.exconly()
  1532. def test_config_ulimits_soft_greater_than_hard_error(self):
  1533. expected = "'soft' value can not be greater than 'hard' value"
  1534. with pytest.raises(ConfigurationError) as exc:
  1535. config.load(build_config_details(
  1536. {
  1537. 'web': {
  1538. 'image': 'busybox',
  1539. 'ulimits': {
  1540. 'nofile': {"soft": 10000, "hard": 1000}
  1541. }
  1542. }
  1543. },
  1544. 'working_dir',
  1545. 'filename.yml'))
  1546. assert expected in exc.exconly()
  1547. def test_valid_config_which_allows_two_type_definitions(self):
  1548. expose_values = [["8000"], [8000]]
  1549. for expose in expose_values:
  1550. service = config.load(
  1551. build_config_details(
  1552. {'web': {
  1553. 'image': 'busybox',
  1554. 'expose': expose
  1555. }},
  1556. 'working_dir',
  1557. 'filename.yml'
  1558. )
  1559. ).services
  1560. assert service[0]['expose'] == expose
  1561. def test_valid_config_oneof_string_or_list(self):
  1562. entrypoint_values = [["sh"], "sh"]
  1563. for entrypoint in entrypoint_values:
  1564. service = config.load(
  1565. build_config_details(
  1566. {'web': {
  1567. 'image': 'busybox',
  1568. 'entrypoint': entrypoint
  1569. }},
  1570. 'working_dir',
  1571. 'filename.yml'
  1572. )
  1573. ).services
  1574. assert service[0]['entrypoint'] == entrypoint
  1575. def test_logs_warning_for_boolean_in_environment(self):
  1576. config_details = build_config_details({
  1577. 'web': {
  1578. 'image': 'busybox',
  1579. 'environment': {'SHOW_STUFF': True}
  1580. }
  1581. })
  1582. with pytest.raises(ConfigurationError) as exc:
  1583. config.load(config_details)
  1584. assert "contains true, which is an invalid type" in exc.exconly()
  1585. def test_config_valid_environment_dict_key_contains_dashes(self):
  1586. services = config.load(
  1587. build_config_details(
  1588. {'web': {
  1589. 'image': 'busybox',
  1590. 'environment': {'SPRING_JPA_HIBERNATE_DDL-AUTO': 'none'}
  1591. }},
  1592. 'working_dir',
  1593. 'filename.yml'
  1594. )
  1595. ).services
  1596. assert services[0]['environment']['SPRING_JPA_HIBERNATE_DDL-AUTO'] == 'none'
  1597. def test_load_yaml_with_yaml_error(self):
  1598. tmpdir = py.test.ensuretemp('invalid_yaml_test')
  1599. self.addCleanup(tmpdir.remove)
  1600. invalid_yaml_file = tmpdir.join('docker-compose.yml')
  1601. invalid_yaml_file.write("""
  1602. web:
  1603. this is bogus: ok: what
  1604. """)
  1605. with pytest.raises(ConfigurationError) as exc:
  1606. config.load_yaml(str(invalid_yaml_file))
  1607. assert 'line 3, column 32' in exc.exconly()
  1608. def test_load_yaml_with_bom(self):
  1609. tmpdir = py.test.ensuretemp('bom_yaml')
  1610. self.addCleanup(tmpdir.remove)
  1611. bom_yaml = tmpdir.join('docker-compose.yml')
  1612. with codecs.open(str(bom_yaml), 'w', encoding='utf-8') as f:
  1613. f.write('''\ufeff
  1614. version: '2.3'
  1615. volumes:
  1616. park_bom:
  1617. ''')
  1618. assert config.load_yaml(str(bom_yaml)) == {
  1619. 'version': '2.3',
  1620. 'volumes': {'park_bom': None}
  1621. }
  1622. def test_validate_extra_hosts_invalid(self):
  1623. with pytest.raises(ConfigurationError) as exc:
  1624. config.load(build_config_details({
  1625. 'web': {
  1626. 'image': 'alpine',
  1627. 'extra_hosts': "www.example.com: 192.168.0.17",
  1628. }
  1629. }))
  1630. assert "web.extra_hosts contains an invalid type" in exc.exconly()
  1631. def test_validate_extra_hosts_invalid_list(self):
  1632. with pytest.raises(ConfigurationError) as exc:
  1633. config.load(build_config_details({
  1634. 'web': {
  1635. 'image': 'alpine',
  1636. 'extra_hosts': [
  1637. {'www.example.com': '192.168.0.17'},
  1638. {'api.example.com': '192.168.0.18'}
  1639. ],
  1640. }
  1641. }))
  1642. assert "which is an invalid type" in exc.exconly()
  1643. def test_normalize_dns_options(self):
  1644. actual = config.load(build_config_details({
  1645. 'web': {
  1646. 'image': 'alpine',
  1647. 'dns': '8.8.8.8',
  1648. 'dns_search': 'domain.local',
  1649. }
  1650. }))
  1651. assert actual.services == [
  1652. {
  1653. 'name': 'web',
  1654. 'image': 'alpine',
  1655. 'dns': ['8.8.8.8'],
  1656. 'dns_search': ['domain.local'],
  1657. }
  1658. ]
  1659. def test_tmpfs_option(self):
  1660. actual = config.load(build_config_details({
  1661. 'version': '2',
  1662. 'services': {
  1663. 'web': {
  1664. 'image': 'alpine',
  1665. 'tmpfs': '/run',
  1666. }
  1667. }
  1668. }))
  1669. assert actual.services == [
  1670. {
  1671. 'name': 'web',
  1672. 'image': 'alpine',
  1673. 'tmpfs': ['/run'],
  1674. }
  1675. ]
  1676. def test_oom_score_adj_option(self):
  1677. actual = config.load(build_config_details({
  1678. 'version': '2',
  1679. 'services': {
  1680. 'web': {
  1681. 'image': 'alpine',
  1682. 'oom_score_adj': 500
  1683. }
  1684. }
  1685. }))
  1686. assert actual.services == [
  1687. {
  1688. 'name': 'web',
  1689. 'image': 'alpine',
  1690. 'oom_score_adj': 500
  1691. }
  1692. ]
  1693. def test_swappiness_option(self):
  1694. actual = config.load(build_config_details({
  1695. 'version': '2',
  1696. 'services': {
  1697. 'web': {
  1698. 'image': 'alpine',
  1699. 'mem_swappiness': 10,
  1700. }
  1701. }
  1702. }))
  1703. assert actual.services == [
  1704. {
  1705. 'name': 'web',
  1706. 'image': 'alpine',
  1707. 'mem_swappiness': 10,
  1708. }
  1709. ]
  1710. def test_group_add_option(self):
  1711. actual = config.load(build_config_details({
  1712. 'version': '2',
  1713. 'services': {
  1714. 'web': {
  1715. 'image': 'alpine',
  1716. 'group_add': ["docker", 777]
  1717. }
  1718. }
  1719. }))
  1720. assert actual.services == [
  1721. {
  1722. 'name': 'web',
  1723. 'image': 'alpine',
  1724. 'group_add': ["docker", 777]
  1725. }
  1726. ]
  1727. def test_dns_opt_option(self):
  1728. actual = config.load(build_config_details({
  1729. 'version': '2',
  1730. 'services': {
  1731. 'web': {
  1732. 'image': 'alpine',
  1733. 'dns_opt': ["use-vc", "no-tld-query"]
  1734. }
  1735. }
  1736. }))
  1737. assert actual.services == [
  1738. {
  1739. 'name': 'web',
  1740. 'image': 'alpine',
  1741. 'dns_opt': ["use-vc", "no-tld-query"]
  1742. }
  1743. ]
  1744. def test_isolation_option(self):
  1745. actual = config.load(build_config_details({
  1746. 'version': str(V2_1),
  1747. 'services': {
  1748. 'web': {
  1749. 'image': 'win10',
  1750. 'isolation': 'hyperv'
  1751. }
  1752. }
  1753. }))
  1754. assert actual.services == [
  1755. {
  1756. 'name': 'web',
  1757. 'image': 'win10',
  1758. 'isolation': 'hyperv',
  1759. }
  1760. ]
  1761. def test_runtime_option(self):
  1762. actual = config.load(build_config_details({
  1763. 'version': str(V2_3),
  1764. 'services': {
  1765. 'web': {
  1766. 'image': 'nvidia/cuda',
  1767. 'runtime': 'nvidia'
  1768. }
  1769. }
  1770. }))
  1771. assert actual.services == [
  1772. {
  1773. 'name': 'web',
  1774. 'image': 'nvidia/cuda',
  1775. 'runtime': 'nvidia',
  1776. }
  1777. ]
  1778. def test_merge_service_dicts_from_files_with_extends_in_base(self):
  1779. base = {
  1780. 'volumes': ['.:/app'],
  1781. 'extends': {'service': 'app'}
  1782. }
  1783. override = {
  1784. 'image': 'alpine:edge',
  1785. }
  1786. actual = config.merge_service_dicts_from_files(
  1787. base,
  1788. override,
  1789. DEFAULT_VERSION)
  1790. assert actual == {
  1791. 'image': 'alpine:edge',
  1792. 'volumes': ['.:/app'],
  1793. 'extends': {'service': 'app'}
  1794. }
  1795. def test_merge_service_dicts_from_files_with_extends_in_override(self):
  1796. base = {
  1797. 'volumes': ['.:/app'],
  1798. 'extends': {'service': 'app'}
  1799. }
  1800. override = {
  1801. 'image': 'alpine:edge',
  1802. 'extends': {'service': 'foo'}
  1803. }
  1804. actual = config.merge_service_dicts_from_files(
  1805. base,
  1806. override,
  1807. DEFAULT_VERSION)
  1808. assert actual == {
  1809. 'image': 'alpine:edge',
  1810. 'volumes': ['.:/app'],
  1811. 'extends': {'service': 'foo'}
  1812. }
  1813. def test_merge_service_dicts_heterogeneous(self):
  1814. base = {
  1815. 'volumes': ['.:/app'],
  1816. 'ports': ['5432']
  1817. }
  1818. override = {
  1819. 'image': 'alpine:edge',
  1820. 'ports': [5432]
  1821. }
  1822. actual = config.merge_service_dicts_from_files(
  1823. base,
  1824. override,
  1825. DEFAULT_VERSION)
  1826. assert actual == {
  1827. 'image': 'alpine:edge',
  1828. 'volumes': ['.:/app'],
  1829. 'ports': types.ServicePort.parse('5432')
  1830. }
  1831. def test_merge_service_dicts_heterogeneous_2(self):
  1832. base = {
  1833. 'volumes': ['.:/app'],
  1834. 'ports': [5432]
  1835. }
  1836. override = {
  1837. 'image': 'alpine:edge',
  1838. 'ports': ['5432']
  1839. }
  1840. actual = config.merge_service_dicts_from_files(
  1841. base,
  1842. override,
  1843. DEFAULT_VERSION)
  1844. assert actual == {
  1845. 'image': 'alpine:edge',
  1846. 'volumes': ['.:/app'],
  1847. 'ports': types.ServicePort.parse('5432')
  1848. }
  1849. def test_merge_service_dicts_ports_sorting(self):
  1850. base = {
  1851. 'ports': [5432]
  1852. }
  1853. override = {
  1854. 'image': 'alpine:edge',
  1855. 'ports': ['5432/udp']
  1856. }
  1857. actual = config.merge_service_dicts_from_files(
  1858. base,
  1859. override,
  1860. DEFAULT_VERSION)
  1861. assert len(actual['ports']) == 2
  1862. assert types.ServicePort.parse('5432')[0] in actual['ports']
  1863. assert types.ServicePort.parse('5432/udp')[0] in actual['ports']
  1864. def test_merge_service_dicts_heterogeneous_volumes(self):
  1865. base = {
  1866. 'volumes': ['/a:/b', '/x:/z'],
  1867. }
  1868. override = {
  1869. 'image': 'alpine:edge',
  1870. 'volumes': [
  1871. {'source': '/e', 'target': '/b', 'type': 'bind'},
  1872. {'source': '/c', 'target': '/d', 'type': 'bind'}
  1873. ]
  1874. }
  1875. actual = config.merge_service_dicts_from_files(
  1876. base, override, V3_2
  1877. )
  1878. assert actual['volumes'] == [
  1879. {'source': '/e', 'target': '/b', 'type': 'bind'},
  1880. {'source': '/c', 'target': '/d', 'type': 'bind'},
  1881. '/x:/z'
  1882. ]
  1883. def test_merge_logging_v1(self):
  1884. base = {
  1885. 'image': 'alpine:edge',
  1886. 'log_driver': 'something',
  1887. 'log_opt': {'foo': 'three'},
  1888. }
  1889. override = {
  1890. 'image': 'alpine:edge',
  1891. 'command': 'true',
  1892. }
  1893. actual = config.merge_service_dicts(base, override, V1)
  1894. assert actual == {
  1895. 'image': 'alpine:edge',
  1896. 'log_driver': 'something',
  1897. 'log_opt': {'foo': 'three'},
  1898. 'command': 'true',
  1899. }
  1900. def test_merge_logging_v2(self):
  1901. base = {
  1902. 'image': 'alpine:edge',
  1903. 'logging': {
  1904. 'driver': 'json-file',
  1905. 'options': {
  1906. 'frequency': '2000',
  1907. 'timeout': '23'
  1908. }
  1909. }
  1910. }
  1911. override = {
  1912. 'logging': {
  1913. 'options': {
  1914. 'timeout': '360',
  1915. 'pretty-print': 'on'
  1916. }
  1917. }
  1918. }
  1919. actual = config.merge_service_dicts(base, override, V2_0)
  1920. assert actual == {
  1921. 'image': 'alpine:edge',
  1922. 'logging': {
  1923. 'driver': 'json-file',
  1924. 'options': {
  1925. 'frequency': '2000',
  1926. 'timeout': '360',
  1927. 'pretty-print': 'on'
  1928. }
  1929. }
  1930. }
  1931. def test_merge_logging_v2_override_driver(self):
  1932. base = {
  1933. 'image': 'alpine:edge',
  1934. 'logging': {
  1935. 'driver': 'json-file',
  1936. 'options': {
  1937. 'frequency': '2000',
  1938. 'timeout': '23'
  1939. }
  1940. }
  1941. }
  1942. override = {
  1943. 'logging': {
  1944. 'driver': 'syslog',
  1945. 'options': {
  1946. 'timeout': '360',
  1947. 'pretty-print': 'on'
  1948. }
  1949. }
  1950. }
  1951. actual = config.merge_service_dicts(base, override, V2_0)
  1952. assert actual == {
  1953. 'image': 'alpine:edge',
  1954. 'logging': {
  1955. 'driver': 'syslog',
  1956. 'options': {
  1957. 'timeout': '360',
  1958. 'pretty-print': 'on'
  1959. }
  1960. }
  1961. }
  1962. def test_merge_logging_v2_no_base_driver(self):
  1963. base = {
  1964. 'image': 'alpine:edge',
  1965. 'logging': {
  1966. 'options': {
  1967. 'frequency': '2000',
  1968. 'timeout': '23'
  1969. }
  1970. }
  1971. }
  1972. override = {
  1973. 'logging': {
  1974. 'driver': 'json-file',
  1975. 'options': {
  1976. 'timeout': '360',
  1977. 'pretty-print': 'on'
  1978. }
  1979. }
  1980. }
  1981. actual = config.merge_service_dicts(base, override, V2_0)
  1982. assert actual == {
  1983. 'image': 'alpine:edge',
  1984. 'logging': {
  1985. 'driver': 'json-file',
  1986. 'options': {
  1987. 'frequency': '2000',
  1988. 'timeout': '360',
  1989. 'pretty-print': 'on'
  1990. }
  1991. }
  1992. }
  1993. def test_merge_logging_v2_no_drivers(self):
  1994. base = {
  1995. 'image': 'alpine:edge',
  1996. 'logging': {
  1997. 'options': {
  1998. 'frequency': '2000',
  1999. 'timeout': '23'
  2000. }
  2001. }
  2002. }
  2003. override = {
  2004. 'logging': {
  2005. 'options': {
  2006. 'timeout': '360',
  2007. 'pretty-print': 'on'
  2008. }
  2009. }
  2010. }
  2011. actual = config.merge_service_dicts(base, override, V2_0)
  2012. assert actual == {
  2013. 'image': 'alpine:edge',
  2014. 'logging': {
  2015. 'options': {
  2016. 'frequency': '2000',
  2017. 'timeout': '360',
  2018. 'pretty-print': 'on'
  2019. }
  2020. }
  2021. }
  2022. def test_merge_logging_v2_no_override_options(self):
  2023. base = {
  2024. 'image': 'alpine:edge',
  2025. 'logging': {
  2026. 'driver': 'json-file',
  2027. 'options': {
  2028. 'frequency': '2000',
  2029. 'timeout': '23'
  2030. }
  2031. }
  2032. }
  2033. override = {
  2034. 'logging': {
  2035. 'driver': 'syslog'
  2036. }
  2037. }
  2038. actual = config.merge_service_dicts(base, override, V2_0)
  2039. assert actual == {
  2040. 'image': 'alpine:edge',
  2041. 'logging': {
  2042. 'driver': 'syslog',
  2043. }
  2044. }
  2045. def test_merge_logging_v2_no_base(self):
  2046. base = {
  2047. 'image': 'alpine:edge'
  2048. }
  2049. override = {
  2050. 'logging': {
  2051. 'driver': 'json-file',
  2052. 'options': {
  2053. 'frequency': '2000'
  2054. }
  2055. }
  2056. }
  2057. actual = config.merge_service_dicts(base, override, V2_0)
  2058. assert actual == {
  2059. 'image': 'alpine:edge',
  2060. 'logging': {
  2061. 'driver': 'json-file',
  2062. 'options': {
  2063. 'frequency': '2000'
  2064. }
  2065. }
  2066. }
  2067. def test_merge_logging_v2_no_override(self):
  2068. base = {
  2069. 'image': 'alpine:edge',
  2070. 'logging': {
  2071. 'driver': 'syslog',
  2072. 'options': {
  2073. 'frequency': '2000'
  2074. }
  2075. }
  2076. }
  2077. override = {}
  2078. actual = config.merge_service_dicts(base, override, V2_0)
  2079. assert actual == {
  2080. 'image': 'alpine:edge',
  2081. 'logging': {
  2082. 'driver': 'syslog',
  2083. 'options': {
  2084. 'frequency': '2000'
  2085. }
  2086. }
  2087. }
  2088. def test_merge_mixed_ports(self):
  2089. base = {
  2090. 'image': 'busybox:latest',
  2091. 'command': 'top',
  2092. 'ports': [
  2093. {
  2094. 'target': '1245',
  2095. 'published': '1245',
  2096. 'protocol': 'udp',
  2097. }
  2098. ]
  2099. }
  2100. override = {
  2101. 'ports': ['1245:1245/udp']
  2102. }
  2103. actual = config.merge_service_dicts(base, override, V3_1)
  2104. assert actual == {
  2105. 'image': 'busybox:latest',
  2106. 'command': 'top',
  2107. 'ports': [types.ServicePort('1245', '1245', 'udp', None, None)]
  2108. }
  2109. def test_merge_depends_on_no_override(self):
  2110. base = {
  2111. 'image': 'busybox',
  2112. 'depends_on': {
  2113. 'app1': {'condition': 'service_started'},
  2114. 'app2': {'condition': 'service_healthy'}
  2115. }
  2116. }
  2117. override = {}
  2118. actual = config.merge_service_dicts(base, override, V2_1)
  2119. assert actual == base
  2120. def test_merge_depends_on_mixed_syntax(self):
  2121. base = {
  2122. 'image': 'busybox',
  2123. 'depends_on': {
  2124. 'app1': {'condition': 'service_started'},
  2125. 'app2': {'condition': 'service_healthy'}
  2126. }
  2127. }
  2128. override = {
  2129. 'depends_on': ['app3']
  2130. }
  2131. actual = config.merge_service_dicts(base, override, V2_1)
  2132. assert actual == {
  2133. 'image': 'busybox',
  2134. 'depends_on': {
  2135. 'app1': {'condition': 'service_started'},
  2136. 'app2': {'condition': 'service_healthy'},
  2137. 'app3': {'condition': 'service_started'}
  2138. }
  2139. }
  2140. def test_empty_environment_key_allowed(self):
  2141. service_dict = config.load(
  2142. build_config_details(
  2143. {
  2144. 'web': {
  2145. 'build': '.',
  2146. 'environment': {
  2147. 'POSTGRES_PASSWORD': ''
  2148. },
  2149. },
  2150. },
  2151. '.',
  2152. None,
  2153. )
  2154. ).services[0]
  2155. assert service_dict['environment']['POSTGRES_PASSWORD'] == ''
  2156. def test_merge_pid(self):
  2157. # Regression: https://github.com/docker/compose/issues/4184
  2158. base = {
  2159. 'image': 'busybox',
  2160. 'pid': 'host'
  2161. }
  2162. override = {
  2163. 'labels': {'com.docker.compose.test': 'yes'}
  2164. }
  2165. actual = config.merge_service_dicts(base, override, V2_0)
  2166. assert actual == {
  2167. 'image': 'busybox',
  2168. 'pid': 'host',
  2169. 'labels': {'com.docker.compose.test': 'yes'}
  2170. }
  2171. def test_merge_different_secrets(self):
  2172. base = {
  2173. 'image': 'busybox',
  2174. 'secrets': [
  2175. {'source': 'src.txt'}
  2176. ]
  2177. }
  2178. override = {'secrets': ['other-src.txt']}
  2179. actual = config.merge_service_dicts(base, override, V3_1)
  2180. assert secret_sort(actual['secrets']) == secret_sort([
  2181. {'source': 'src.txt'},
  2182. {'source': 'other-src.txt'}
  2183. ])
  2184. def test_merge_secrets_override(self):
  2185. base = {
  2186. 'image': 'busybox',
  2187. 'secrets': ['src.txt'],
  2188. }
  2189. override = {
  2190. 'secrets': [
  2191. {
  2192. 'source': 'src.txt',
  2193. 'target': 'data.txt',
  2194. 'mode': 0o400
  2195. }
  2196. ]
  2197. }
  2198. actual = config.merge_service_dicts(base, override, V3_1)
  2199. assert actual['secrets'] == override['secrets']
  2200. def test_merge_different_configs(self):
  2201. base = {
  2202. 'image': 'busybox',
  2203. 'configs': [
  2204. {'source': 'src.txt'}
  2205. ]
  2206. }
  2207. override = {'configs': ['other-src.txt']}
  2208. actual = config.merge_service_dicts(base, override, V3_3)
  2209. assert secret_sort(actual['configs']) == secret_sort([
  2210. {'source': 'src.txt'},
  2211. {'source': 'other-src.txt'}
  2212. ])
  2213. def test_merge_configs_override(self):
  2214. base = {
  2215. 'image': 'busybox',
  2216. 'configs': ['src.txt'],
  2217. }
  2218. override = {
  2219. 'configs': [
  2220. {
  2221. 'source': 'src.txt',
  2222. 'target': 'data.txt',
  2223. 'mode': 0o400
  2224. }
  2225. ]
  2226. }
  2227. actual = config.merge_service_dicts(base, override, V3_3)
  2228. assert actual['configs'] == override['configs']
  2229. def test_merge_deploy(self):
  2230. base = {
  2231. 'image': 'busybox',
  2232. }
  2233. override = {
  2234. 'deploy': {
  2235. 'mode': 'global',
  2236. 'restart_policy': {
  2237. 'condition': 'on-failure'
  2238. }
  2239. }
  2240. }
  2241. actual = config.merge_service_dicts(base, override, V3_0)
  2242. assert actual['deploy'] == override['deploy']
  2243. def test_merge_deploy_override(self):
  2244. base = {
  2245. 'deploy': {
  2246. 'endpoint_mode': 'vip',
  2247. 'labels': ['com.docker.compose.a=1', 'com.docker.compose.b=2'],
  2248. 'mode': 'replicated',
  2249. 'placement': {
  2250. 'constraints': [
  2251. 'node.role == manager', 'engine.labels.aws == true'
  2252. ],
  2253. 'preferences': [
  2254. {'spread': 'node.labels.zone'}, {'spread': 'x.d.z'}
  2255. ]
  2256. },
  2257. 'replicas': 3,
  2258. 'resources': {
  2259. 'limits': {'cpus': '0.50', 'memory': '50m'},
  2260. 'reservations': {
  2261. 'cpus': '0.1',
  2262. 'generic_resources': [
  2263. {'discrete_resource_spec': {'kind': 'abc', 'value': 123}}
  2264. ],
  2265. 'memory': '15m'
  2266. }
  2267. },
  2268. 'restart_policy': {'condition': 'any', 'delay': '10s'},
  2269. 'update_config': {'delay': '10s', 'max_failure_ratio': 0.3}
  2270. },
  2271. 'image': 'hello-world'
  2272. }
  2273. override = {
  2274. 'deploy': {
  2275. 'labels': {
  2276. 'com.docker.compose.b': '21', 'com.docker.compose.c': '3'
  2277. },
  2278. 'placement': {
  2279. 'constraints': ['node.role == worker', 'engine.labels.dev == true'],
  2280. 'preferences': [{'spread': 'node.labels.zone'}, {'spread': 'x.d.s'}]
  2281. },
  2282. 'resources': {
  2283. 'limits': {'memory': '200m'},
  2284. 'reservations': {
  2285. 'cpus': '0.78',
  2286. 'generic_resources': [
  2287. {'discrete_resource_spec': {'kind': 'abc', 'value': 134}},
  2288. {'discrete_resource_spec': {'kind': 'xyz', 'value': 0.1}}
  2289. ]
  2290. }
  2291. },
  2292. 'restart_policy': {'condition': 'on-failure', 'max_attempts': 42},
  2293. 'update_config': {'max_failure_ratio': 0.712, 'parallelism': 4}
  2294. }
  2295. }
  2296. actual = config.merge_service_dicts(base, override, V3_5)
  2297. assert actual['deploy'] == {
  2298. 'mode': 'replicated',
  2299. 'endpoint_mode': 'vip',
  2300. 'labels': {
  2301. 'com.docker.compose.a': '1',
  2302. 'com.docker.compose.b': '21',
  2303. 'com.docker.compose.c': '3'
  2304. },
  2305. 'placement': {
  2306. 'constraints': [
  2307. 'engine.labels.aws == true', 'engine.labels.dev == true',
  2308. 'node.role == manager', 'node.role == worker'
  2309. ],
  2310. 'preferences': [
  2311. {'spread': 'node.labels.zone'}, {'spread': 'x.d.s'}, {'spread': 'x.d.z'}
  2312. ]
  2313. },
  2314. 'replicas': 3,
  2315. 'resources': {
  2316. 'limits': {'cpus': '0.50', 'memory': '200m'},
  2317. 'reservations': {
  2318. 'cpus': '0.78',
  2319. 'memory': '15m',
  2320. 'generic_resources': [
  2321. {'discrete_resource_spec': {'kind': 'abc', 'value': 134}},
  2322. {'discrete_resource_spec': {'kind': 'xyz', 'value': 0.1}},
  2323. ]
  2324. }
  2325. },
  2326. 'restart_policy': {
  2327. 'condition': 'on-failure',
  2328. 'delay': '10s',
  2329. 'max_attempts': 42,
  2330. },
  2331. 'update_config': {
  2332. 'max_failure_ratio': 0.712,
  2333. 'delay': '10s',
  2334. 'parallelism': 4
  2335. }
  2336. }
  2337. def test_merge_credential_spec(self):
  2338. base = {
  2339. 'image': 'bb',
  2340. 'credential_spec': {
  2341. 'file': '/hello-world',
  2342. }
  2343. }
  2344. override = {
  2345. 'credential_spec': {
  2346. 'registry': 'revolution.com',
  2347. }
  2348. }
  2349. actual = config.merge_service_dicts(base, override, V3_3)
  2350. assert actual['credential_spec'] == override['credential_spec']
  2351. def test_merge_scale(self):
  2352. base = {
  2353. 'image': 'bar',
  2354. 'scale': 2,
  2355. }
  2356. override = {
  2357. 'scale': 4,
  2358. }
  2359. actual = config.merge_service_dicts(base, override, V2_2)
  2360. assert actual == {'image': 'bar', 'scale': 4}
  2361. def test_merge_blkio_config(self):
  2362. base = {
  2363. 'image': 'bar',
  2364. 'blkio_config': {
  2365. 'weight': 300,
  2366. 'weight_device': [
  2367. {'path': '/dev/sda1', 'weight': 200}
  2368. ],
  2369. 'device_read_iops': [
  2370. {'path': '/dev/sda1', 'rate': 300}
  2371. ],
  2372. 'device_write_iops': [
  2373. {'path': '/dev/sda1', 'rate': 1000}
  2374. ]
  2375. }
  2376. }
  2377. override = {
  2378. 'blkio_config': {
  2379. 'weight': 450,
  2380. 'weight_device': [
  2381. {'path': '/dev/sda2', 'weight': 400}
  2382. ],
  2383. 'device_read_iops': [
  2384. {'path': '/dev/sda1', 'rate': 2000}
  2385. ],
  2386. 'device_read_bps': [
  2387. {'path': '/dev/sda1', 'rate': 1024}
  2388. ]
  2389. }
  2390. }
  2391. actual = config.merge_service_dicts(base, override, V2_2)
  2392. assert actual == {
  2393. 'image': 'bar',
  2394. 'blkio_config': {
  2395. 'weight': override['blkio_config']['weight'],
  2396. 'weight_device': (
  2397. base['blkio_config']['weight_device'] +
  2398. override['blkio_config']['weight_device']
  2399. ),
  2400. 'device_read_iops': override['blkio_config']['device_read_iops'],
  2401. 'device_read_bps': override['blkio_config']['device_read_bps'],
  2402. 'device_write_iops': base['blkio_config']['device_write_iops']
  2403. }
  2404. }
  2405. def test_merge_extra_hosts(self):
  2406. base = {
  2407. 'image': 'bar',
  2408. 'extra_hosts': {
  2409. 'foo': '1.2.3.4',
  2410. }
  2411. }
  2412. override = {
  2413. 'extra_hosts': ['bar:5.6.7.8', 'foo:127.0.0.1']
  2414. }
  2415. actual = config.merge_service_dicts(base, override, V2_0)
  2416. assert actual['extra_hosts'] == {
  2417. 'foo': '127.0.0.1',
  2418. 'bar': '5.6.7.8',
  2419. }
  2420. def test_merge_healthcheck_config(self):
  2421. base = {
  2422. 'image': 'bar',
  2423. 'healthcheck': {
  2424. 'start_period': 1000,
  2425. 'interval': 3000,
  2426. 'test': ['true']
  2427. }
  2428. }
  2429. override = {
  2430. 'healthcheck': {
  2431. 'interval': 5000,
  2432. 'timeout': 10000,
  2433. 'test': ['echo', 'OK'],
  2434. }
  2435. }
  2436. actual = config.merge_service_dicts(base, override, V2_3)
  2437. assert actual['healthcheck'] == {
  2438. 'start_period': base['healthcheck']['start_period'],
  2439. 'test': override['healthcheck']['test'],
  2440. 'interval': override['healthcheck']['interval'],
  2441. 'timeout': override['healthcheck']['timeout'],
  2442. }
  2443. def test_merge_healthcheck_override_disables(self):
  2444. base = {
  2445. 'image': 'bar',
  2446. 'healthcheck': {
  2447. 'start_period': 1000,
  2448. 'interval': 3000,
  2449. 'timeout': 2000,
  2450. 'retries': 3,
  2451. 'test': ['true']
  2452. }
  2453. }
  2454. override = {
  2455. 'healthcheck': {
  2456. 'disabled': True
  2457. }
  2458. }
  2459. actual = config.merge_service_dicts(base, override, V2_3)
  2460. assert actual['healthcheck'] == {'disabled': True}
  2461. def test_merge_healthcheck_override_enables(self):
  2462. base = {
  2463. 'image': 'bar',
  2464. 'healthcheck': {
  2465. 'disabled': True
  2466. }
  2467. }
  2468. override = {
  2469. 'healthcheck': {
  2470. 'disabled': False,
  2471. 'start_period': 1000,
  2472. 'interval': 3000,
  2473. 'timeout': 2000,
  2474. 'retries': 3,
  2475. 'test': ['true']
  2476. }
  2477. }
  2478. actual = config.merge_service_dicts(base, override, V2_3)
  2479. assert actual['healthcheck'] == override['healthcheck']
  2480. def test_merge_device_cgroup_rules(self):
  2481. base = {
  2482. 'image': 'bar',
  2483. 'device_cgroup_rules': ['c 7:128 rwm', 'x 3:244 rw']
  2484. }
  2485. override = {
  2486. 'device_cgroup_rules': ['c 7:128 rwm', 'f 0:128 n']
  2487. }
  2488. actual = config.merge_service_dicts(base, override, V2_3)
  2489. assert sorted(actual['device_cgroup_rules']) == sorted(
  2490. ['c 7:128 rwm', 'x 3:244 rw', 'f 0:128 n']
  2491. )
  2492. def test_merge_isolation(self):
  2493. base = {
  2494. 'image': 'bar',
  2495. 'isolation': 'default',
  2496. }
  2497. override = {
  2498. 'isolation': 'hyperv',
  2499. }
  2500. actual = config.merge_service_dicts(base, override, V2_3)
  2501. assert actual == {
  2502. 'image': 'bar',
  2503. 'isolation': 'hyperv',
  2504. }
  2505. def test_merge_storage_opt(self):
  2506. base = {
  2507. 'image': 'bar',
  2508. 'storage_opt': {
  2509. 'size': '1G',
  2510. 'readonly': 'false',
  2511. }
  2512. }
  2513. override = {
  2514. 'storage_opt': {
  2515. 'size': '2G',
  2516. 'encryption': 'aes',
  2517. }
  2518. }
  2519. actual = config.merge_service_dicts(base, override, V2_3)
  2520. assert actual['storage_opt'] == {
  2521. 'size': '2G',
  2522. 'readonly': 'false',
  2523. 'encryption': 'aes',
  2524. }
  2525. def test_external_volume_config(self):
  2526. config_details = build_config_details({
  2527. 'version': '2',
  2528. 'services': {
  2529. 'bogus': {'image': 'busybox'}
  2530. },
  2531. 'volumes': {
  2532. 'ext': {'external': True},
  2533. 'ext2': {'external': {'name': 'aliased'}}
  2534. }
  2535. })
  2536. config_result = config.load(config_details)
  2537. volumes = config_result.volumes
  2538. assert 'ext' in volumes
  2539. assert volumes['ext']['external'] is True
  2540. assert 'ext2' in volumes
  2541. assert volumes['ext2']['external']['name'] == 'aliased'
  2542. def test_external_volume_invalid_config(self):
  2543. config_details = build_config_details({
  2544. 'version': '2',
  2545. 'services': {
  2546. 'bogus': {'image': 'busybox'}
  2547. },
  2548. 'volumes': {
  2549. 'ext': {'external': True, 'driver': 'foo'}
  2550. }
  2551. })
  2552. with pytest.raises(ConfigurationError):
  2553. config.load(config_details)
  2554. def test_depends_on_orders_services(self):
  2555. config_details = build_config_details({
  2556. 'version': '2',
  2557. 'services': {
  2558. 'one': {'image': 'busybox', 'depends_on': ['three', 'two']},
  2559. 'two': {'image': 'busybox', 'depends_on': ['three']},
  2560. 'three': {'image': 'busybox'},
  2561. },
  2562. })
  2563. actual = config.load(config_details)
  2564. assert (
  2565. [service['name'] for service in actual.services] ==
  2566. ['three', 'two', 'one']
  2567. )
  2568. def test_depends_on_unknown_service_errors(self):
  2569. config_details = build_config_details({
  2570. 'version': '2',
  2571. 'services': {
  2572. 'one': {'image': 'busybox', 'depends_on': ['three']},
  2573. },
  2574. })
  2575. with pytest.raises(ConfigurationError) as exc:
  2576. config.load(config_details)
  2577. assert "Service 'one' depends on service 'three'" in exc.exconly()
  2578. def test_linked_service_is_undefined(self):
  2579. with pytest.raises(ConfigurationError):
  2580. config.load(
  2581. build_config_details({
  2582. 'version': '2',
  2583. 'services': {
  2584. 'web': {'image': 'busybox', 'links': ['db:db']},
  2585. },
  2586. })
  2587. )
  2588. def test_load_dockerfile_without_context(self):
  2589. config_details = build_config_details({
  2590. 'version': '2',
  2591. 'services': {
  2592. 'one': {'build': {'dockerfile': 'Dockerfile.foo'}},
  2593. },
  2594. })
  2595. with pytest.raises(ConfigurationError) as exc:
  2596. config.load(config_details)
  2597. assert 'has neither an image nor a build context' in exc.exconly()
  2598. def test_load_secrets(self):
  2599. base_file = config.ConfigFile(
  2600. 'base.yaml',
  2601. {
  2602. 'version': '3.1',
  2603. 'services': {
  2604. 'web': {
  2605. 'image': 'example/web',
  2606. 'secrets': [
  2607. 'one',
  2608. {
  2609. 'source': 'source',
  2610. 'target': 'target',
  2611. 'uid': '100',
  2612. 'gid': '200',
  2613. 'mode': 0o777,
  2614. },
  2615. ],
  2616. },
  2617. },
  2618. 'secrets': {
  2619. 'one': {'file': 'secret.txt'},
  2620. },
  2621. })
  2622. details = config.ConfigDetails('.', [base_file])
  2623. service_dicts = config.load(details).services
  2624. expected = [
  2625. {
  2626. 'name': 'web',
  2627. 'image': 'example/web',
  2628. 'secrets': [
  2629. types.ServiceSecret('one', None, None, None, None, None),
  2630. types.ServiceSecret('source', 'target', '100', '200', 0o777, None),
  2631. ],
  2632. },
  2633. ]
  2634. assert service_sort(service_dicts) == service_sort(expected)
  2635. def test_load_secrets_multi_file(self):
  2636. base_file = config.ConfigFile(
  2637. 'base.yaml',
  2638. {
  2639. 'version': '3.1',
  2640. 'services': {
  2641. 'web': {
  2642. 'image': 'example/web',
  2643. 'secrets': ['one'],
  2644. },
  2645. },
  2646. 'secrets': {
  2647. 'one': {'file': 'secret.txt'},
  2648. },
  2649. })
  2650. override_file = config.ConfigFile(
  2651. 'base.yaml',
  2652. {
  2653. 'version': '3.1',
  2654. 'services': {
  2655. 'web': {
  2656. 'secrets': [
  2657. {
  2658. 'source': 'source',
  2659. 'target': 'target',
  2660. 'uid': '100',
  2661. 'gid': '200',
  2662. 'mode': 0o777,
  2663. },
  2664. ],
  2665. },
  2666. },
  2667. })
  2668. details = config.ConfigDetails('.', [base_file, override_file])
  2669. service_dicts = config.load(details).services
  2670. expected = [
  2671. {
  2672. 'name': 'web',
  2673. 'image': 'example/web',
  2674. 'secrets': [
  2675. types.ServiceSecret('one', None, None, None, None, None),
  2676. types.ServiceSecret('source', 'target', '100', '200', 0o777, None),
  2677. ],
  2678. },
  2679. ]
  2680. assert service_sort(service_dicts) == service_sort(expected)
  2681. def test_load_configs(self):
  2682. base_file = config.ConfigFile(
  2683. 'base.yaml',
  2684. {
  2685. 'version': '3.3',
  2686. 'services': {
  2687. 'web': {
  2688. 'image': 'example/web',
  2689. 'configs': [
  2690. 'one',
  2691. {
  2692. 'source': 'source',
  2693. 'target': 'target',
  2694. 'uid': '100',
  2695. 'gid': '200',
  2696. 'mode': 0o777,
  2697. },
  2698. ],
  2699. },
  2700. },
  2701. 'configs': {
  2702. 'one': {'file': 'secret.txt'},
  2703. },
  2704. })
  2705. details = config.ConfigDetails('.', [base_file])
  2706. service_dicts = config.load(details).services
  2707. expected = [
  2708. {
  2709. 'name': 'web',
  2710. 'image': 'example/web',
  2711. 'configs': [
  2712. types.ServiceConfig('one', None, None, None, None, None),
  2713. types.ServiceConfig('source', 'target', '100', '200', 0o777, None),
  2714. ],
  2715. },
  2716. ]
  2717. assert service_sort(service_dicts) == service_sort(expected)
  2718. def test_load_configs_multi_file(self):
  2719. base_file = config.ConfigFile(
  2720. 'base.yaml',
  2721. {
  2722. 'version': '3.3',
  2723. 'services': {
  2724. 'web': {
  2725. 'image': 'example/web',
  2726. 'configs': ['one'],
  2727. },
  2728. },
  2729. 'configs': {
  2730. 'one': {'file': 'secret.txt'},
  2731. },
  2732. })
  2733. override_file = config.ConfigFile(
  2734. 'base.yaml',
  2735. {
  2736. 'version': '3.3',
  2737. 'services': {
  2738. 'web': {
  2739. 'configs': [
  2740. {
  2741. 'source': 'source',
  2742. 'target': 'target',
  2743. 'uid': '100',
  2744. 'gid': '200',
  2745. 'mode': 0o777,
  2746. },
  2747. ],
  2748. },
  2749. },
  2750. })
  2751. details = config.ConfigDetails('.', [base_file, override_file])
  2752. service_dicts = config.load(details).services
  2753. expected = [
  2754. {
  2755. 'name': 'web',
  2756. 'image': 'example/web',
  2757. 'configs': [
  2758. types.ServiceConfig('one', None, None, None, None, None),
  2759. types.ServiceConfig('source', 'target', '100', '200', 0o777, None),
  2760. ],
  2761. },
  2762. ]
  2763. assert service_sort(service_dicts) == service_sort(expected)
  2764. def test_config_convertible_label_types(self):
  2765. config_details = build_config_details(
  2766. {
  2767. 'version': '3.5',
  2768. 'services': {
  2769. 'web': {
  2770. 'build': {
  2771. 'labels': {'testbuild': True},
  2772. 'context': os.getcwd()
  2773. },
  2774. 'labels': {
  2775. "key": 12345
  2776. }
  2777. },
  2778. },
  2779. 'networks': {
  2780. 'foo': {
  2781. 'labels': {'network.ips.max': 1023}
  2782. }
  2783. },
  2784. 'volumes': {
  2785. 'foo': {
  2786. 'labels': {'volume.is_readonly': False}
  2787. }
  2788. },
  2789. 'secrets': {
  2790. 'foo': {
  2791. 'labels': {'secret.data.expires': 1546282120}
  2792. }
  2793. },
  2794. 'configs': {
  2795. 'foo': {
  2796. 'labels': {'config.data.correction.value': -0.1412}
  2797. }
  2798. }
  2799. }
  2800. )
  2801. loaded_config = config.load(config_details)
  2802. assert loaded_config.services[0]['build']['labels'] == {'testbuild': 'True'}
  2803. assert loaded_config.services[0]['labels'] == {'key': '12345'}
  2804. assert loaded_config.networks['foo']['labels']['network.ips.max'] == '1023'
  2805. assert loaded_config.volumes['foo']['labels']['volume.is_readonly'] == 'False'
  2806. assert loaded_config.secrets['foo']['labels']['secret.data.expires'] == '1546282120'
  2807. assert loaded_config.configs['foo']['labels']['config.data.correction.value'] == '-0.1412'
  2808. def test_config_invalid_label_types(self):
  2809. config_details = build_config_details({
  2810. 'version': '2.3',
  2811. 'volumes': {
  2812. 'foo': {'labels': [1, 2, 3]}
  2813. }
  2814. })
  2815. with pytest.raises(ConfigurationError):
  2816. config.load(config_details)
  2817. def test_service_volume_invalid_config(self):
  2818. config_details = build_config_details(
  2819. {
  2820. 'version': '3.2',
  2821. 'services': {
  2822. 'web': {
  2823. 'build': {
  2824. 'context': '.',
  2825. 'args': None,
  2826. },
  2827. 'volumes': [
  2828. {
  2829. "type": "volume",
  2830. "source": "/data",
  2831. "garbage": {
  2832. "and": "error"
  2833. }
  2834. }
  2835. ]
  2836. }
  2837. }
  2838. }
  2839. )
  2840. with pytest.raises(ConfigurationError) as exc:
  2841. config.load(config_details)
  2842. assert "services.web.volumes contains unsupported option: 'garbage'" in exc.exconly()
  2843. def test_config_valid_service_label_validation(self):
  2844. config_details = build_config_details(
  2845. {
  2846. 'version': '3.5',
  2847. 'services': {
  2848. 'web': {
  2849. 'image': 'busybox',
  2850. 'labels': {
  2851. "key": "string"
  2852. }
  2853. },
  2854. },
  2855. }
  2856. )
  2857. config.load(config_details)
  2858. def test_config_duplicate_mount_points(self):
  2859. config1 = build_config_details(
  2860. {
  2861. 'version': '3.5',
  2862. 'services': {
  2863. 'web': {
  2864. 'image': 'busybox',
  2865. 'volumes': ['/tmp/foo:/tmp/foo', '/tmp/foo:/tmp/foo:rw']
  2866. }
  2867. }
  2868. }
  2869. )
  2870. config2 = build_config_details(
  2871. {
  2872. 'version': '3.5',
  2873. 'services': {
  2874. 'web': {
  2875. 'image': 'busybox',
  2876. 'volumes': ['/x:/y', '/z:/y']
  2877. }
  2878. }
  2879. }
  2880. )
  2881. with self.assertRaises(ConfigurationError) as e:
  2882. config.load(config1)
  2883. self.assertEquals(str(e.exception), 'Duplicate mount points: [%s]' % (
  2884. ', '.join(['/tmp/foo:/tmp/foo:rw']*2)))
  2885. with self.assertRaises(ConfigurationError) as e:
  2886. config.load(config2)
  2887. self.assertEquals(str(e.exception), 'Duplicate mount points: [%s]' % (
  2888. ', '.join(['/x:/y:rw', '/z:/y:rw'])))
  2889. class NetworkModeTest(unittest.TestCase):
  2890. def test_network_mode_standard(self):
  2891. config_data = config.load(build_config_details({
  2892. 'version': '2',
  2893. 'services': {
  2894. 'web': {
  2895. 'image': 'busybox',
  2896. 'command': "top",
  2897. 'network_mode': 'bridge',
  2898. },
  2899. },
  2900. }))
  2901. assert config_data.services[0]['network_mode'] == 'bridge'
  2902. def test_network_mode_standard_v1(self):
  2903. config_data = config.load(build_config_details({
  2904. 'web': {
  2905. 'image': 'busybox',
  2906. 'command': "top",
  2907. 'net': 'bridge',
  2908. },
  2909. }))
  2910. assert config_data.services[0]['network_mode'] == 'bridge'
  2911. assert 'net' not in config_data.services[0]
  2912. def test_network_mode_container(self):
  2913. config_data = config.load(build_config_details({
  2914. 'version': '2',
  2915. 'services': {
  2916. 'web': {
  2917. 'image': 'busybox',
  2918. 'command': "top",
  2919. 'network_mode': 'container:foo',
  2920. },
  2921. },
  2922. }))
  2923. assert config_data.services[0]['network_mode'] == 'container:foo'
  2924. def test_network_mode_container_v1(self):
  2925. config_data = config.load(build_config_details({
  2926. 'web': {
  2927. 'image': 'busybox',
  2928. 'command': "top",
  2929. 'net': 'container:foo',
  2930. },
  2931. }))
  2932. assert config_data.services[0]['network_mode'] == 'container:foo'
  2933. def test_network_mode_service(self):
  2934. config_data = config.load(build_config_details({
  2935. 'version': '2',
  2936. 'services': {
  2937. 'web': {
  2938. 'image': 'busybox',
  2939. 'command': "top",
  2940. 'network_mode': 'service:foo',
  2941. },
  2942. 'foo': {
  2943. 'image': 'busybox',
  2944. 'command': "top",
  2945. },
  2946. },
  2947. }))
  2948. assert config_data.services[1]['network_mode'] == 'service:foo'
  2949. def test_network_mode_service_v1(self):
  2950. config_data = config.load(build_config_details({
  2951. 'web': {
  2952. 'image': 'busybox',
  2953. 'command': "top",
  2954. 'net': 'container:foo',
  2955. },
  2956. 'foo': {
  2957. 'image': 'busybox',
  2958. 'command': "top",
  2959. },
  2960. }))
  2961. assert config_data.services[1]['network_mode'] == 'service:foo'
  2962. def test_network_mode_service_nonexistent(self):
  2963. with pytest.raises(ConfigurationError) as excinfo:
  2964. config.load(build_config_details({
  2965. 'version': '2',
  2966. 'services': {
  2967. 'web': {
  2968. 'image': 'busybox',
  2969. 'command': "top",
  2970. 'network_mode': 'service:foo',
  2971. },
  2972. },
  2973. }))
  2974. assert "service 'foo' which is undefined" in excinfo.exconly()
  2975. def test_network_mode_plus_networks_is_invalid(self):
  2976. with pytest.raises(ConfigurationError) as excinfo:
  2977. config.load(build_config_details({
  2978. 'version': '2',
  2979. 'services': {
  2980. 'web': {
  2981. 'image': 'busybox',
  2982. 'command': "top",
  2983. 'network_mode': 'bridge',
  2984. 'networks': ['front'],
  2985. },
  2986. },
  2987. 'networks': {
  2988. 'front': None,
  2989. }
  2990. }))
  2991. assert "'network_mode' and 'networks' cannot be combined" in excinfo.exconly()
  2992. class PortsTest(unittest.TestCase):
  2993. INVALID_PORTS_TYPES = [
  2994. {"1": "8000"},
  2995. False,
  2996. "8000",
  2997. 8000,
  2998. ]
  2999. NON_UNIQUE_SINGLE_PORTS = [
  3000. ["8000", "8000"],
  3001. ]
  3002. INVALID_PORT_MAPPINGS = [
  3003. ["8000-8004:8000-8002"],
  3004. ["4242:4242-4244"],
  3005. ]
  3006. VALID_SINGLE_PORTS = [
  3007. ["8000"],
  3008. ["8000/tcp"],
  3009. ["8000", "9000"],
  3010. [8000],
  3011. [8000, 9000],
  3012. ]
  3013. VALID_PORT_MAPPINGS = [
  3014. ["8000:8050"],
  3015. ["49153-49154:3002-3003"],
  3016. ]
  3017. def test_config_invalid_ports_type_validation(self):
  3018. for invalid_ports in self.INVALID_PORTS_TYPES:
  3019. with pytest.raises(ConfigurationError) as exc:
  3020. self.check_config({'ports': invalid_ports})
  3021. assert "contains an invalid type" in exc.value.msg
  3022. def test_config_non_unique_ports_validation(self):
  3023. for invalid_ports in self.NON_UNIQUE_SINGLE_PORTS:
  3024. with pytest.raises(ConfigurationError) as exc:
  3025. self.check_config({'ports': invalid_ports})
  3026. assert "non-unique" in exc.value.msg
  3027. def test_config_invalid_ports_format_validation(self):
  3028. for invalid_ports in self.INVALID_PORT_MAPPINGS:
  3029. with pytest.raises(ConfigurationError) as exc:
  3030. self.check_config({'ports': invalid_ports})
  3031. assert "Port ranges don't match in length" in exc.value.msg
  3032. def test_config_valid_ports_format_validation(self):
  3033. for valid_ports in self.VALID_SINGLE_PORTS + self.VALID_PORT_MAPPINGS:
  3034. self.check_config({'ports': valid_ports})
  3035. def test_config_invalid_expose_type_validation(self):
  3036. for invalid_expose in self.INVALID_PORTS_TYPES:
  3037. with pytest.raises(ConfigurationError) as exc:
  3038. self.check_config({'expose': invalid_expose})
  3039. assert "contains an invalid type" in exc.value.msg
  3040. def test_config_non_unique_expose_validation(self):
  3041. for invalid_expose in self.NON_UNIQUE_SINGLE_PORTS:
  3042. with pytest.raises(ConfigurationError) as exc:
  3043. self.check_config({'expose': invalid_expose})
  3044. assert "non-unique" in exc.value.msg
  3045. def test_config_invalid_expose_format_validation(self):
  3046. # Valid port mappings ARE NOT valid 'expose' entries
  3047. for invalid_expose in self.INVALID_PORT_MAPPINGS + self.VALID_PORT_MAPPINGS:
  3048. with pytest.raises(ConfigurationError) as exc:
  3049. self.check_config({'expose': invalid_expose})
  3050. assert "should be of the format" in exc.value.msg
  3051. def test_config_valid_expose_format_validation(self):
  3052. # Valid single ports ARE valid 'expose' entries
  3053. for valid_expose in self.VALID_SINGLE_PORTS:
  3054. self.check_config({'expose': valid_expose})
  3055. def check_config(self, cfg):
  3056. config.load(
  3057. build_config_details({
  3058. 'version': '2.3',
  3059. 'services': {
  3060. 'web': dict(image='busybox', **cfg)
  3061. },
  3062. }, 'working_dir', 'filename.yml')
  3063. )
  3064. class SubnetTest(unittest.TestCase):
  3065. INVALID_SUBNET_TYPES = [
  3066. None,
  3067. False,
  3068. 10,
  3069. ]
  3070. INVALID_SUBNET_MAPPINGS = [
  3071. "",
  3072. "192.168.0.1/sdfsdfs",
  3073. "192.168.0.1/",
  3074. "192.168.0.1/33",
  3075. "192.168.0.1/01",
  3076. "192.168.0.1",
  3077. "fe80:0000:0000:0000:0204:61ff:fe9d:f156/sdfsdfs",
  3078. "fe80:0000:0000:0000:0204:61ff:fe9d:f156/",
  3079. "fe80:0000:0000:0000:0204:61ff:fe9d:f156/129",
  3080. "fe80:0000:0000:0000:0204:61ff:fe9d:f156/01",
  3081. "fe80:0000:0000:0000:0204:61ff:fe9d:f156",
  3082. "ge80:0000:0000:0000:0204:61ff:fe9d:f156/128",
  3083. "192.168.0.1/31/31",
  3084. ]
  3085. VALID_SUBNET_MAPPINGS = [
  3086. "192.168.0.1/0",
  3087. "192.168.0.1/32",
  3088. "fe80:0000:0000:0000:0204:61ff:fe9d:f156/0",
  3089. "fe80:0000:0000:0000:0204:61ff:fe9d:f156/128",
  3090. "1:2:3:4:5:6:7:8/0",
  3091. "1::/0",
  3092. "1:2:3:4:5:6:7::/0",
  3093. "1::8/0",
  3094. "1:2:3:4:5:6::8/0",
  3095. "::/0",
  3096. "::8/0",
  3097. "::2:3:4:5:6:7:8/0",
  3098. "fe80::7:8%eth0/0",
  3099. "fe80::7:8%1/0",
  3100. "::255.255.255.255/0",
  3101. "::ffff:255.255.255.255/0",
  3102. "::ffff:0:255.255.255.255/0",
  3103. "2001:db8:3:4::192.0.2.33/0",
  3104. "64:ff9b::192.0.2.33/0",
  3105. ]
  3106. def test_config_invalid_subnet_type_validation(self):
  3107. for invalid_subnet in self.INVALID_SUBNET_TYPES:
  3108. with pytest.raises(ConfigurationError) as exc:
  3109. self.check_config(invalid_subnet)
  3110. assert "contains an invalid type" in exc.value.msg
  3111. def test_config_invalid_subnet_format_validation(self):
  3112. for invalid_subnet in self.INVALID_SUBNET_MAPPINGS:
  3113. with pytest.raises(ConfigurationError) as exc:
  3114. self.check_config(invalid_subnet)
  3115. assert "should use the CIDR format" in exc.value.msg
  3116. def test_config_valid_subnet_format_validation(self):
  3117. for valid_subnet in self.VALID_SUBNET_MAPPINGS:
  3118. self.check_config(valid_subnet)
  3119. def check_config(self, subnet):
  3120. config.load(
  3121. build_config_details({
  3122. 'version': '3.5',
  3123. 'services': {
  3124. 'web': {
  3125. 'image': 'busybox'
  3126. }
  3127. },
  3128. 'networks': {
  3129. 'default': {
  3130. 'ipam': {
  3131. 'config': [
  3132. {
  3133. 'subnet': subnet
  3134. }
  3135. ],
  3136. 'driver': 'default'
  3137. }
  3138. }
  3139. }
  3140. })
  3141. )
  3142. class InterpolationTest(unittest.TestCase):
  3143. @mock.patch.dict(os.environ)
  3144. def test_config_file_with_environment_file(self):
  3145. project_dir = 'tests/fixtures/default-env-file'
  3146. service_dicts = config.load(
  3147. config.find(
  3148. project_dir, None, Environment.from_env_file(project_dir)
  3149. )
  3150. ).services
  3151. assert service_dicts[0] == {
  3152. 'name': 'web',
  3153. 'image': 'alpine:latest',
  3154. 'ports': [
  3155. types.ServicePort.parse('5643')[0],
  3156. types.ServicePort.parse('9999')[0]
  3157. ],
  3158. 'command': 'true'
  3159. }
  3160. @mock.patch.dict(os.environ)
  3161. def test_config_file_with_options_environment_file(self):
  3162. project_dir = 'tests/fixtures/default-env-file'
  3163. service_dicts = config.load(
  3164. config.find(
  3165. project_dir, None, Environment.from_env_file(project_dir, '.env2')
  3166. )
  3167. ).services
  3168. assert service_dicts[0] == {
  3169. 'name': 'web',
  3170. 'image': 'alpine:latest',
  3171. 'ports': [
  3172. types.ServicePort.parse('5644')[0],
  3173. types.ServicePort.parse('9998')[0]
  3174. ],
  3175. 'command': 'false'
  3176. }
  3177. @mock.patch.dict(os.environ)
  3178. def test_config_file_with_environment_variable(self):
  3179. project_dir = 'tests/fixtures/environment-interpolation'
  3180. os.environ.update(
  3181. IMAGE="busybox",
  3182. HOST_PORT="80",
  3183. LABEL_VALUE="myvalue",
  3184. )
  3185. service_dicts = config.load(
  3186. config.find(
  3187. project_dir, None, Environment.from_env_file(project_dir)
  3188. )
  3189. ).services
  3190. assert service_dicts == [
  3191. {
  3192. 'name': 'web',
  3193. 'image': 'busybox',
  3194. 'ports': types.ServicePort.parse('80:8000'),
  3195. 'labels': {'mylabel': 'myvalue'},
  3196. 'hostname': 'host-',
  3197. 'command': '${ESCAPED}',
  3198. }
  3199. ]
  3200. @mock.patch.dict(os.environ)
  3201. def test_config_file_with_environment_variable_with_defaults(self):
  3202. project_dir = 'tests/fixtures/environment-interpolation-with-defaults'
  3203. os.environ.update(
  3204. IMAGE="busybox",
  3205. )
  3206. service_dicts = config.load(
  3207. config.find(
  3208. project_dir, None, Environment.from_env_file(project_dir)
  3209. )
  3210. ).services
  3211. assert service_dicts == [
  3212. {
  3213. 'name': 'web',
  3214. 'image': 'busybox',
  3215. 'ports': types.ServicePort.parse('80:8000'),
  3216. 'hostname': 'host-',
  3217. }
  3218. ]
  3219. @mock.patch.dict(os.environ)
  3220. def test_unset_variable_produces_warning(self):
  3221. os.environ.pop('FOO', None)
  3222. os.environ.pop('BAR', None)
  3223. config_details = build_config_details(
  3224. {
  3225. 'web': {
  3226. 'image': '${FOO}',
  3227. 'command': '${BAR}',
  3228. 'container_name': '${BAR}',
  3229. },
  3230. },
  3231. '.',
  3232. None,
  3233. )
  3234. with mock.patch('compose.config.environment.log') as log:
  3235. config.load(config_details)
  3236. assert 2 == log.warning.call_count
  3237. warnings = sorted(args[0][0] for args in log.warning.call_args_list)
  3238. assert 'BAR' in warnings[0]
  3239. assert 'FOO' in warnings[1]
  3240. def test_compatibility_mode_warnings(self):
  3241. config_details = build_config_details({
  3242. 'version': '3.5',
  3243. 'services': {
  3244. 'web': {
  3245. 'deploy': {
  3246. 'labels': ['abc=def'],
  3247. 'endpoint_mode': 'dnsrr',
  3248. 'update_config': {'max_failure_ratio': 0.4},
  3249. 'placement': {'constraints': ['node.id==deadbeef']},
  3250. 'resources': {
  3251. 'reservations': {'cpus': '0.2'}
  3252. },
  3253. 'restart_policy': {
  3254. 'delay': '2s',
  3255. 'window': '12s'
  3256. }
  3257. },
  3258. 'image': 'busybox'
  3259. }
  3260. }
  3261. })
  3262. with mock.patch('compose.config.config.log') as log:
  3263. config.load(config_details, compatibility=True)
  3264. assert log.warning.call_count == 1
  3265. warn_message = log.warning.call_args[0][0]
  3266. assert warn_message.startswith(
  3267. 'The following deploy sub-keys are not supported in compatibility mode'
  3268. )
  3269. assert 'labels' in warn_message
  3270. assert 'endpoint_mode' in warn_message
  3271. assert 'update_config' in warn_message
  3272. assert 'placement' in warn_message
  3273. assert 'resources.reservations.cpus' in warn_message
  3274. assert 'restart_policy.delay' in warn_message
  3275. assert 'restart_policy.window' in warn_message
  3276. def test_compatibility_mode_load(self):
  3277. config_details = build_config_details({
  3278. 'version': '3.5',
  3279. 'services': {
  3280. 'foo': {
  3281. 'image': 'alpine:3.7',
  3282. 'deploy': {
  3283. 'replicas': 3,
  3284. 'restart_policy': {
  3285. 'condition': 'any',
  3286. 'max_attempts': 7,
  3287. },
  3288. 'resources': {
  3289. 'limits': {'memory': '300M', 'cpus': '0.7'},
  3290. 'reservations': {'memory': '100M'},
  3291. },
  3292. },
  3293. 'credential_spec': {
  3294. 'file': 'spec.json'
  3295. },
  3296. },
  3297. },
  3298. })
  3299. with mock.patch('compose.config.config.log') as log:
  3300. cfg = config.load(config_details, compatibility=True)
  3301. assert log.warning.call_count == 0
  3302. service_dict = cfg.services[0]
  3303. assert service_dict == {
  3304. 'image': 'alpine:3.7',
  3305. 'scale': 3,
  3306. 'restart': {'MaximumRetryCount': 7, 'Name': 'always'},
  3307. 'mem_limit': '300M',
  3308. 'mem_reservation': '100M',
  3309. 'cpus': 0.7,
  3310. 'name': 'foo',
  3311. 'security_opt': ['credentialspec=file://spec.json'],
  3312. }
  3313. @mock.patch.dict(os.environ)
  3314. def test_invalid_interpolation(self):
  3315. with pytest.raises(config.ConfigurationError) as cm:
  3316. config.load(
  3317. build_config_details(
  3318. {'web': {'image': '${'}},
  3319. 'working_dir',
  3320. 'filename.yml'
  3321. )
  3322. )
  3323. assert 'Invalid' in cm.value.msg
  3324. assert 'for "image" option' in cm.value.msg
  3325. assert 'in service "web"' in cm.value.msg
  3326. assert '"${"' in cm.value.msg
  3327. @mock.patch.dict(os.environ)
  3328. def test_interpolation_secrets_section(self):
  3329. os.environ['FOO'] = 'baz.bar'
  3330. config_dict = config.load(build_config_details({
  3331. 'version': '3.1',
  3332. 'secrets': {
  3333. 'secretdata': {
  3334. 'external': {'name': '$FOO'}
  3335. }
  3336. }
  3337. }))
  3338. assert config_dict.secrets == {
  3339. 'secretdata': {
  3340. 'external': {'name': 'baz.bar'},
  3341. 'name': 'baz.bar'
  3342. }
  3343. }
  3344. @mock.patch.dict(os.environ)
  3345. def test_interpolation_configs_section(self):
  3346. os.environ['FOO'] = 'baz.bar'
  3347. config_dict = config.load(build_config_details({
  3348. 'version': '3.3',
  3349. 'configs': {
  3350. 'configdata': {
  3351. 'external': {'name': '$FOO'}
  3352. }
  3353. }
  3354. }))
  3355. assert config_dict.configs == {
  3356. 'configdata': {
  3357. 'external': {'name': 'baz.bar'},
  3358. 'name': 'baz.bar'
  3359. }
  3360. }
  3361. class VolumeConfigTest(unittest.TestCase):
  3362. def test_no_binding(self):
  3363. d = make_service_dict('foo', {'build': '.', 'volumes': ['/data']}, working_dir='.')
  3364. assert d['volumes'] == ['/data']
  3365. @mock.patch.dict(os.environ)
  3366. def test_volume_binding_with_environment_variable(self):
  3367. os.environ['VOLUME_PATH'] = '/host/path'
  3368. d = config.load(
  3369. build_config_details(
  3370. {'foo': {'build': '.', 'volumes': ['${VOLUME_PATH}:/container/path']}},
  3371. '.',
  3372. None,
  3373. )
  3374. ).services[0]
  3375. assert d['volumes'] == [VolumeSpec.parse('/host/path:/container/path')]
  3376. @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='posix paths')
  3377. def test_volumes_order_is_preserved(self):
  3378. volumes = ['/{0}:/{0}'.format(i) for i in range(0, 6)]
  3379. shuffle(volumes)
  3380. cfg = make_service_dict('foo', {'build': '.', 'volumes': volumes})
  3381. assert cfg['volumes'] == volumes
  3382. @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='posix paths')
  3383. @mock.patch.dict(os.environ)
  3384. def test_volume_binding_with_home(self):
  3385. os.environ['HOME'] = '/home/user'
  3386. d = make_service_dict('foo', {'build': '.', 'volumes': ['~:/container/path']}, working_dir='.')
  3387. assert d['volumes'] == ['/home/user:/container/path']
  3388. def test_name_does_not_expand(self):
  3389. d = make_service_dict('foo', {'build': '.', 'volumes': ['mydatavolume:/data']}, working_dir='.')
  3390. assert d['volumes'] == ['mydatavolume:/data']
  3391. def test_absolute_posix_path_does_not_expand(self):
  3392. d = make_service_dict('foo', {'build': '.', 'volumes': ['/var/lib/data:/data']}, working_dir='.')
  3393. assert d['volumes'] == ['/var/lib/data:/data']
  3394. def test_absolute_windows_path_does_not_expand(self):
  3395. d = make_service_dict('foo', {'build': '.', 'volumes': ['c:\\data:/data']}, working_dir='.')
  3396. assert d['volumes'] == ['c:\\data:/data']
  3397. @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='posix paths')
  3398. def test_relative_path_does_expand_posix(self):
  3399. d = make_service_dict(
  3400. 'foo',
  3401. {'build': '.', 'volumes': ['./data:/data']},
  3402. working_dir='/home/me/myproject')
  3403. assert d['volumes'] == ['/home/me/myproject/data:/data']
  3404. d = make_service_dict(
  3405. 'foo',
  3406. {'build': '.', 'volumes': ['.:/data']},
  3407. working_dir='/home/me/myproject')
  3408. assert d['volumes'] == ['/home/me/myproject:/data']
  3409. d = make_service_dict(
  3410. 'foo',
  3411. {'build': '.', 'volumes': ['../otherproject:/data']},
  3412. working_dir='/home/me/myproject')
  3413. assert d['volumes'] == ['/home/me/otherproject:/data']
  3414. @pytest.mark.skipif(not IS_WINDOWS_PLATFORM, reason='windows paths')
  3415. def test_relative_path_does_expand_windows(self):
  3416. d = make_service_dict(
  3417. 'foo',
  3418. {'build': '.', 'volumes': ['./data:/data']},
  3419. working_dir='c:\\Users\\me\\myproject')
  3420. assert d['volumes'] == ['c:\\Users\\me\\myproject\\data:/data']
  3421. d = make_service_dict(
  3422. 'foo',
  3423. {'build': '.', 'volumes': ['.:/data']},
  3424. working_dir='c:\\Users\\me\\myproject')
  3425. assert d['volumes'] == ['c:\\Users\\me\\myproject:/data']
  3426. d = make_service_dict(
  3427. 'foo',
  3428. {'build': '.', 'volumes': ['../otherproject:/data']},
  3429. working_dir='c:\\Users\\me\\myproject')
  3430. assert d['volumes'] == ['c:\\Users\\me\\otherproject:/data']
  3431. @mock.patch.dict(os.environ)
  3432. def test_home_directory_with_driver_does_not_expand(self):
  3433. os.environ['NAME'] = 'surprise!'
  3434. d = make_service_dict('foo', {
  3435. 'build': '.',
  3436. 'volumes': ['~:/data'],
  3437. 'volume_driver': 'foodriver',
  3438. }, working_dir='.')
  3439. assert d['volumes'] == ['~:/data']
  3440. def test_volume_path_with_non_ascii_directory(self):
  3441. volume = u'/Füü/data:/data'
  3442. container_path = config.resolve_volume_path(".", volume)
  3443. assert container_path == volume
  3444. class MergePathMappingTest(object):
  3445. config_name = ""
  3446. def test_empty(self):
  3447. service_dict = config.merge_service_dicts({}, {}, DEFAULT_VERSION)
  3448. assert self.config_name not in service_dict
  3449. def test_no_override(self):
  3450. service_dict = config.merge_service_dicts(
  3451. {self.config_name: ['/foo:/code', '/data']},
  3452. {},
  3453. DEFAULT_VERSION)
  3454. assert set(service_dict[self.config_name]) == set(['/foo:/code', '/data'])
  3455. def test_no_base(self):
  3456. service_dict = config.merge_service_dicts(
  3457. {},
  3458. {self.config_name: ['/bar:/code']},
  3459. DEFAULT_VERSION)
  3460. assert set(service_dict[self.config_name]) == set(['/bar:/code'])
  3461. def test_override_explicit_path(self):
  3462. service_dict = config.merge_service_dicts(
  3463. {self.config_name: ['/foo:/code', '/data']},
  3464. {self.config_name: ['/bar:/code']},
  3465. DEFAULT_VERSION)
  3466. assert set(service_dict[self.config_name]) == set(['/bar:/code', '/data'])
  3467. def test_add_explicit_path(self):
  3468. service_dict = config.merge_service_dicts(
  3469. {self.config_name: ['/foo:/code', '/data']},
  3470. {self.config_name: ['/bar:/code', '/quux:/data']},
  3471. DEFAULT_VERSION)
  3472. assert set(service_dict[self.config_name]) == set(['/bar:/code', '/quux:/data'])
  3473. def test_remove_explicit_path(self):
  3474. service_dict = config.merge_service_dicts(
  3475. {self.config_name: ['/foo:/code', '/quux:/data']},
  3476. {self.config_name: ['/bar:/code', '/data']},
  3477. DEFAULT_VERSION)
  3478. assert set(service_dict[self.config_name]) == set(['/bar:/code', '/data'])
  3479. class MergeVolumesTest(unittest.TestCase, MergePathMappingTest):
  3480. config_name = 'volumes'
  3481. class MergeDevicesTest(unittest.TestCase, MergePathMappingTest):
  3482. config_name = 'devices'
  3483. class BuildOrImageMergeTest(unittest.TestCase):
  3484. def test_merge_build_or_image_no_override(self):
  3485. assert config.merge_service_dicts({'build': '.'}, {}, V1) == {'build': '.'}
  3486. assert config.merge_service_dicts({'image': 'redis'}, {}, V1) == {'image': 'redis'}
  3487. def test_merge_build_or_image_override_with_same(self):
  3488. assert config.merge_service_dicts({'build': '.'}, {'build': './web'}, V1) == {'build': './web'}
  3489. assert config.merge_service_dicts({'image': 'redis'}, {'image': 'postgres'}, V1) == {
  3490. 'image': 'postgres'
  3491. }
  3492. def test_merge_build_or_image_override_with_other(self):
  3493. assert config.merge_service_dicts({'build': '.'}, {'image': 'redis'}, V1) == {
  3494. 'image': 'redis'
  3495. }
  3496. assert config.merge_service_dicts({'image': 'redis'}, {'build': '.'}, V1) == {'build': '.'}
  3497. class MergeListsTest(object):
  3498. config_name = ""
  3499. base_config = []
  3500. override_config = []
  3501. def merged_config(self):
  3502. return set(self.base_config) | set(self.override_config)
  3503. def test_empty(self):
  3504. assert self.config_name not in config.merge_service_dicts({}, {}, DEFAULT_VERSION)
  3505. def test_no_override(self):
  3506. service_dict = config.merge_service_dicts(
  3507. {self.config_name: self.base_config},
  3508. {},
  3509. DEFAULT_VERSION)
  3510. assert set(service_dict[self.config_name]) == set(self.base_config)
  3511. def test_no_base(self):
  3512. service_dict = config.merge_service_dicts(
  3513. {},
  3514. {self.config_name: self.base_config},
  3515. DEFAULT_VERSION)
  3516. assert set(service_dict[self.config_name]) == set(self.base_config)
  3517. def test_add_item(self):
  3518. service_dict = config.merge_service_dicts(
  3519. {self.config_name: self.base_config},
  3520. {self.config_name: self.override_config},
  3521. DEFAULT_VERSION)
  3522. assert set(service_dict[self.config_name]) == set(self.merged_config())
  3523. class MergePortsTest(unittest.TestCase, MergeListsTest):
  3524. config_name = 'ports'
  3525. base_config = ['10:8000', '9000']
  3526. override_config = ['20:8000']
  3527. def merged_config(self):
  3528. return self.convert(self.base_config) | self.convert(self.override_config)
  3529. def convert(self, port_config):
  3530. return set(config.merge_service_dicts(
  3531. {self.config_name: port_config},
  3532. {self.config_name: []},
  3533. DEFAULT_VERSION
  3534. )[self.config_name])
  3535. def test_duplicate_port_mappings(self):
  3536. service_dict = config.merge_service_dicts(
  3537. {self.config_name: self.base_config},
  3538. {self.config_name: self.base_config},
  3539. DEFAULT_VERSION
  3540. )
  3541. assert set(service_dict[self.config_name]) == self.convert(self.base_config)
  3542. def test_no_override(self):
  3543. service_dict = config.merge_service_dicts(
  3544. {self.config_name: self.base_config},
  3545. {},
  3546. DEFAULT_VERSION)
  3547. assert set(service_dict[self.config_name]) == self.convert(self.base_config)
  3548. def test_no_base(self):
  3549. service_dict = config.merge_service_dicts(
  3550. {},
  3551. {self.config_name: self.base_config},
  3552. DEFAULT_VERSION)
  3553. assert set(service_dict[self.config_name]) == self.convert(self.base_config)
  3554. class MergeNetworksTest(unittest.TestCase, MergeListsTest):
  3555. config_name = 'networks'
  3556. base_config = {'default': {'aliases': ['foo.bar', 'foo.baz']}}
  3557. override_config = {'default': {'ipv4_address': '123.234.123.234'}}
  3558. def test_no_network_overrides(self):
  3559. service_dict = config.merge_service_dicts(
  3560. {self.config_name: self.base_config},
  3561. {self.config_name: self.override_config},
  3562. DEFAULT_VERSION)
  3563. assert service_dict[self.config_name] == {
  3564. 'default': {
  3565. 'aliases': ['foo.bar', 'foo.baz'],
  3566. 'ipv4_address': '123.234.123.234'
  3567. }
  3568. }
  3569. def test_network_has_none_value(self):
  3570. service_dict = config.merge_service_dicts(
  3571. {self.config_name: {
  3572. 'default': None
  3573. }},
  3574. {self.config_name: {
  3575. 'default': {
  3576. 'aliases': []
  3577. }
  3578. }},
  3579. DEFAULT_VERSION)
  3580. assert service_dict[self.config_name] == {
  3581. 'default': {
  3582. 'aliases': []
  3583. }
  3584. }
  3585. def test_all_properties(self):
  3586. service_dict = config.merge_service_dicts(
  3587. {self.config_name: {
  3588. 'default': {
  3589. 'aliases': ['foo.bar', 'foo.baz'],
  3590. 'link_local_ips': ['192.168.1.10', '192.168.1.11'],
  3591. 'ipv4_address': '111.111.111.111',
  3592. 'ipv6_address': 'FE80:CD00:0000:0CDE:1257:0000:211E:729C-first'
  3593. }
  3594. }},
  3595. {self.config_name: {
  3596. 'default': {
  3597. 'aliases': ['foo.baz', 'foo.baz2'],
  3598. 'link_local_ips': ['192.168.1.11', '192.168.1.12'],
  3599. 'ipv4_address': '123.234.123.234',
  3600. 'ipv6_address': 'FE80:CD00:0000:0CDE:1257:0000:211E:729C-second'
  3601. }
  3602. }},
  3603. DEFAULT_VERSION)
  3604. assert service_dict[self.config_name] == {
  3605. 'default': {
  3606. 'aliases': ['foo.bar', 'foo.baz', 'foo.baz2'],
  3607. 'link_local_ips': ['192.168.1.10', '192.168.1.11', '192.168.1.12'],
  3608. 'ipv4_address': '123.234.123.234',
  3609. 'ipv6_address': 'FE80:CD00:0000:0CDE:1257:0000:211E:729C-second'
  3610. }
  3611. }
  3612. def test_no_network_name_overrides(self):
  3613. service_dict = config.merge_service_dicts(
  3614. {
  3615. self.config_name: {
  3616. 'default': {
  3617. 'aliases': ['foo.bar', 'foo.baz'],
  3618. 'ipv4_address': '123.234.123.234'
  3619. }
  3620. }
  3621. },
  3622. {
  3623. self.config_name: {
  3624. 'another_network': {
  3625. 'ipv4_address': '123.234.123.234'
  3626. }
  3627. }
  3628. },
  3629. DEFAULT_VERSION)
  3630. assert service_dict[self.config_name] == {
  3631. 'default': {
  3632. 'aliases': ['foo.bar', 'foo.baz'],
  3633. 'ipv4_address': '123.234.123.234'
  3634. },
  3635. 'another_network': {
  3636. 'ipv4_address': '123.234.123.234'
  3637. }
  3638. }
  3639. class MergeStringsOrListsTest(unittest.TestCase):
  3640. def test_no_override(self):
  3641. service_dict = config.merge_service_dicts(
  3642. {'dns': '8.8.8.8'},
  3643. {},
  3644. DEFAULT_VERSION)
  3645. assert set(service_dict['dns']) == set(['8.8.8.8'])
  3646. def test_no_base(self):
  3647. service_dict = config.merge_service_dicts(
  3648. {},
  3649. {'dns': '8.8.8.8'},
  3650. DEFAULT_VERSION)
  3651. assert set(service_dict['dns']) == set(['8.8.8.8'])
  3652. def test_add_string(self):
  3653. service_dict = config.merge_service_dicts(
  3654. {'dns': ['8.8.8.8']},
  3655. {'dns': '9.9.9.9'},
  3656. DEFAULT_VERSION)
  3657. assert set(service_dict['dns']) == set(['8.8.8.8', '9.9.9.9'])
  3658. def test_add_list(self):
  3659. service_dict = config.merge_service_dicts(
  3660. {'dns': '8.8.8.8'},
  3661. {'dns': ['9.9.9.9']},
  3662. DEFAULT_VERSION)
  3663. assert set(service_dict['dns']) == set(['8.8.8.8', '9.9.9.9'])
  3664. class MergeLabelsTest(unittest.TestCase):
  3665. def test_empty(self):
  3666. assert 'labels' not in config.merge_service_dicts({}, {}, DEFAULT_VERSION)
  3667. def test_no_override(self):
  3668. service_dict = config.merge_service_dicts(
  3669. make_service_dict('foo', {'build': '.', 'labels': ['foo=1', 'bar']}, 'tests/'),
  3670. make_service_dict('foo', {'build': '.'}, 'tests/'),
  3671. DEFAULT_VERSION)
  3672. assert service_dict['labels'] == {'foo': '1', 'bar': ''}
  3673. def test_no_base(self):
  3674. service_dict = config.merge_service_dicts(
  3675. make_service_dict('foo', {'build': '.'}, 'tests/'),
  3676. make_service_dict('foo', {'build': '.', 'labels': ['foo=2']}, 'tests/'),
  3677. DEFAULT_VERSION)
  3678. assert service_dict['labels'] == {'foo': '2'}
  3679. def test_override_explicit_value(self):
  3680. service_dict = config.merge_service_dicts(
  3681. make_service_dict('foo', {'build': '.', 'labels': ['foo=1', 'bar']}, 'tests/'),
  3682. make_service_dict('foo', {'build': '.', 'labels': ['foo=2']}, 'tests/'),
  3683. DEFAULT_VERSION)
  3684. assert service_dict['labels'] == {'foo': '2', 'bar': ''}
  3685. def test_add_explicit_value(self):
  3686. service_dict = config.merge_service_dicts(
  3687. make_service_dict('foo', {'build': '.', 'labels': ['foo=1', 'bar']}, 'tests/'),
  3688. make_service_dict('foo', {'build': '.', 'labels': ['bar=2']}, 'tests/'),
  3689. DEFAULT_VERSION)
  3690. assert service_dict['labels'] == {'foo': '1', 'bar': '2'}
  3691. def test_remove_explicit_value(self):
  3692. service_dict = config.merge_service_dicts(
  3693. make_service_dict('foo', {'build': '.', 'labels': ['foo=1', 'bar=2']}, 'tests/'),
  3694. make_service_dict('foo', {'build': '.', 'labels': ['bar']}, 'tests/'),
  3695. DEFAULT_VERSION)
  3696. assert service_dict['labels'] == {'foo': '1', 'bar': ''}
  3697. class MergeBuildTest(unittest.TestCase):
  3698. def test_full(self):
  3699. base = {
  3700. 'context': '.',
  3701. 'dockerfile': 'Dockerfile',
  3702. 'args': {
  3703. 'x': '1',
  3704. 'y': '2',
  3705. },
  3706. 'cache_from': ['ubuntu'],
  3707. 'labels': ['com.docker.compose.test=true']
  3708. }
  3709. override = {
  3710. 'context': './prod',
  3711. 'dockerfile': 'Dockerfile.prod',
  3712. 'args': ['x=12'],
  3713. 'cache_from': ['debian'],
  3714. 'labels': {
  3715. 'com.docker.compose.test': 'false',
  3716. 'com.docker.compose.prod': 'true',
  3717. }
  3718. }
  3719. result = config.merge_build(None, {'build': base}, {'build': override})
  3720. assert result['context'] == override['context']
  3721. assert result['dockerfile'] == override['dockerfile']
  3722. assert result['args'] == {'x': '12', 'y': '2'}
  3723. assert set(result['cache_from']) == set(['ubuntu', 'debian'])
  3724. assert result['labels'] == override['labels']
  3725. def test_empty_override(self):
  3726. base = {
  3727. 'context': '.',
  3728. 'dockerfile': 'Dockerfile',
  3729. 'args': {
  3730. 'x': '1',
  3731. 'y': '2',
  3732. },
  3733. 'cache_from': ['ubuntu'],
  3734. 'labels': {
  3735. 'com.docker.compose.test': 'true'
  3736. }
  3737. }
  3738. override = {}
  3739. result = config.merge_build(None, {'build': base}, {'build': override})
  3740. assert result == base
  3741. def test_empty_base(self):
  3742. base = {}
  3743. override = {
  3744. 'context': './prod',
  3745. 'dockerfile': 'Dockerfile.prod',
  3746. 'args': {'x': '12'},
  3747. 'cache_from': ['debian'],
  3748. 'labels': {
  3749. 'com.docker.compose.test': 'false',
  3750. 'com.docker.compose.prod': 'true',
  3751. }
  3752. }
  3753. result = config.merge_build(None, {'build': base}, {'build': override})
  3754. assert result == override
  3755. class MemoryOptionsTest(unittest.TestCase):
  3756. def test_validation_fails_with_just_memswap_limit(self):
  3757. """
  3758. When you set a 'memswap_limit' it is invalid config unless you also set
  3759. a mem_limit
  3760. """
  3761. with pytest.raises(ConfigurationError) as excinfo:
  3762. config.load(
  3763. build_config_details(
  3764. {
  3765. 'foo': {'image': 'busybox', 'memswap_limit': 2000000},
  3766. },
  3767. 'tests/fixtures/extends',
  3768. 'filename.yml'
  3769. )
  3770. )
  3771. assert "foo.memswap_limit is invalid: when defining " \
  3772. "'memswap_limit' you must set 'mem_limit' as well" \
  3773. in excinfo.exconly()
  3774. def test_validation_with_correct_memswap_values(self):
  3775. service_dict = config.load(
  3776. build_config_details(
  3777. {'foo': {'image': 'busybox', 'mem_limit': 1000000, 'memswap_limit': 2000000}},
  3778. 'tests/fixtures/extends',
  3779. 'common.yml'
  3780. )
  3781. ).services
  3782. assert service_dict[0]['memswap_limit'] == 2000000
  3783. def test_memswap_can_be_a_string(self):
  3784. service_dict = config.load(
  3785. build_config_details(
  3786. {'foo': {'image': 'busybox', 'mem_limit': "1G", 'memswap_limit': "512M"}},
  3787. 'tests/fixtures/extends',
  3788. 'common.yml'
  3789. )
  3790. ).services
  3791. assert service_dict[0]['memswap_limit'] == "512M"
  3792. class EnvTest(unittest.TestCase):
  3793. def test_parse_environment_as_list(self):
  3794. environment = [
  3795. 'NORMAL=F1',
  3796. 'CONTAINS_EQUALS=F=2',
  3797. 'TRAILING_EQUALS=',
  3798. ]
  3799. assert config.parse_environment(environment) == {
  3800. 'NORMAL': 'F1', 'CONTAINS_EQUALS': 'F=2', 'TRAILING_EQUALS': ''
  3801. }
  3802. def test_parse_environment_as_dict(self):
  3803. environment = {
  3804. 'NORMAL': 'F1',
  3805. 'CONTAINS_EQUALS': 'F=2',
  3806. 'TRAILING_EQUALS': None,
  3807. }
  3808. assert config.parse_environment(environment) == environment
  3809. def test_parse_environment_invalid(self):
  3810. with pytest.raises(ConfigurationError):
  3811. config.parse_environment('a=b')
  3812. def test_parse_environment_empty(self):
  3813. assert config.parse_environment(None) == {}
  3814. @mock.patch.dict(os.environ)
  3815. def test_resolve_environment(self):
  3816. os.environ['FILE_DEF'] = 'E1'
  3817. os.environ['FILE_DEF_EMPTY'] = 'E2'
  3818. os.environ['ENV_DEF'] = 'E3'
  3819. service_dict = {
  3820. 'build': '.',
  3821. 'environment': {
  3822. 'FILE_DEF': 'F1',
  3823. 'FILE_DEF_EMPTY': '',
  3824. 'ENV_DEF': None,
  3825. 'NO_DEF': None
  3826. },
  3827. }
  3828. assert resolve_environment(
  3829. service_dict, Environment.from_env_file(None)
  3830. ) == {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': None}
  3831. def test_resolve_environment_from_env_file(self):
  3832. assert resolve_environment({'env_file': ['tests/fixtures/env/one.env']}) == {
  3833. 'ONE': '2', 'TWO': '1', 'THREE': '3', 'FOO': 'bar'
  3834. }
  3835. def test_environment_overrides_env_file(self):
  3836. assert resolve_environment({
  3837. 'environment': {'FOO': 'baz'},
  3838. 'env_file': ['tests/fixtures/env/one.env'],
  3839. }) == {'ONE': '2', 'TWO': '1', 'THREE': '3', 'FOO': 'baz'}
  3840. def test_resolve_environment_with_multiple_env_files(self):
  3841. service_dict = {
  3842. 'env_file': [
  3843. 'tests/fixtures/env/one.env',
  3844. 'tests/fixtures/env/two.env'
  3845. ]
  3846. }
  3847. assert resolve_environment(service_dict) == {
  3848. 'ONE': '2', 'TWO': '1', 'THREE': '3', 'FOO': 'baz', 'DOO': 'dah'
  3849. }
  3850. def test_resolve_environment_nonexistent_file(self):
  3851. with pytest.raises(ConfigurationError) as exc:
  3852. config.load(build_config_details(
  3853. {'foo': {'image': 'example', 'env_file': 'nonexistent.env'}},
  3854. working_dir='tests/fixtures/env'))
  3855. assert 'Couldn\'t find env file' in exc.exconly()
  3856. assert 'nonexistent.env' in exc.exconly()
  3857. @mock.patch.dict(os.environ)
  3858. def test_resolve_environment_from_env_file_with_empty_values(self):
  3859. os.environ['FILE_DEF'] = 'E1'
  3860. os.environ['FILE_DEF_EMPTY'] = 'E2'
  3861. os.environ['ENV_DEF'] = 'E3'
  3862. assert resolve_environment(
  3863. {'env_file': ['tests/fixtures/env/resolve.env']},
  3864. Environment.from_env_file(None)
  3865. ) == {
  3866. 'FILE_DEF': u'bär',
  3867. 'FILE_DEF_EMPTY': '',
  3868. 'ENV_DEF': 'E3',
  3869. 'NO_DEF': None
  3870. }
  3871. @mock.patch.dict(os.environ)
  3872. def test_resolve_build_args(self):
  3873. os.environ['env_arg'] = 'value2'
  3874. build = {
  3875. 'context': '.',
  3876. 'args': {
  3877. 'arg1': 'value1',
  3878. 'empty_arg': '',
  3879. 'env_arg': None,
  3880. 'no_env': None
  3881. }
  3882. }
  3883. assert resolve_build_args(build['args'], Environment.from_env_file(build['context'])) == {
  3884. 'arg1': 'value1', 'empty_arg': '', 'env_arg': 'value2', 'no_env': None
  3885. }
  3886. @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash')
  3887. @mock.patch.dict(os.environ)
  3888. def test_resolve_path(self):
  3889. os.environ['HOSTENV'] = '/tmp'
  3890. os.environ['CONTAINERENV'] = '/host/tmp'
  3891. service_dict = config.load(
  3892. build_config_details(
  3893. {'foo': {'build': '.', 'volumes': ['$HOSTENV:$CONTAINERENV']}},
  3894. "tests/fixtures/env",
  3895. )
  3896. ).services[0]
  3897. assert set(service_dict['volumes']) == set([VolumeSpec.parse('/tmp:/host/tmp')])
  3898. service_dict = config.load(
  3899. build_config_details(
  3900. {'foo': {'build': '.', 'volumes': ['/opt${HOSTENV}:/opt${CONTAINERENV}']}},
  3901. "tests/fixtures/env",
  3902. )
  3903. ).services[0]
  3904. assert set(service_dict['volumes']) == set([VolumeSpec.parse('/opt/tmp:/opt/host/tmp')])
  3905. def load_from_filename(filename, override_dir=None):
  3906. return config.load(
  3907. config.find('.', [filename], Environment.from_env_file('.'), override_dir=override_dir)
  3908. ).services
  3909. class ExtendsTest(unittest.TestCase):
  3910. def test_extends(self):
  3911. service_dicts = load_from_filename('tests/fixtures/extends/docker-compose.yml')
  3912. assert service_sort(service_dicts) == service_sort([
  3913. {
  3914. 'name': 'mydb',
  3915. 'image': 'busybox',
  3916. 'command': 'top',
  3917. },
  3918. {
  3919. 'name': 'myweb',
  3920. 'image': 'busybox',
  3921. 'command': 'top',
  3922. 'network_mode': 'bridge',
  3923. 'links': ['mydb:db'],
  3924. 'environment': {
  3925. "FOO": "1",
  3926. "BAR": "2",
  3927. "BAZ": "2",
  3928. },
  3929. }
  3930. ])
  3931. def test_merging_env_labels_ulimits(self):
  3932. service_dicts = load_from_filename('tests/fixtures/extends/common-env-labels-ulimits.yml')
  3933. assert service_sort(service_dicts) == service_sort([
  3934. {
  3935. 'name': 'web',
  3936. 'image': 'busybox',
  3937. 'command': '/bin/true',
  3938. 'network_mode': 'host',
  3939. 'environment': {
  3940. "FOO": "2",
  3941. "BAR": "1",
  3942. "BAZ": "3",
  3943. },
  3944. 'labels': {'label': 'one'},
  3945. 'ulimits': {'nproc': 65535, 'memlock': {'soft': 1024, 'hard': 2048}}
  3946. }
  3947. ])
  3948. def test_nested(self):
  3949. service_dicts = load_from_filename('tests/fixtures/extends/nested.yml')
  3950. assert service_dicts == [
  3951. {
  3952. 'name': 'myweb',
  3953. 'image': 'busybox',
  3954. 'command': '/bin/true',
  3955. 'network_mode': 'host',
  3956. 'environment': {
  3957. "FOO": "2",
  3958. "BAR": "2",
  3959. },
  3960. },
  3961. ]
  3962. def test_self_referencing_file(self):
  3963. """
  3964. We specify a 'file' key that is the filename we're already in.
  3965. """
  3966. service_dicts = load_from_filename('tests/fixtures/extends/specify-file-as-self.yml')
  3967. assert service_sort(service_dicts) == service_sort([
  3968. {
  3969. 'environment':
  3970. {
  3971. 'YEP': '1', 'BAR': '1', 'BAZ': '3'
  3972. },
  3973. 'image': 'busybox',
  3974. 'name': 'myweb'
  3975. },
  3976. {
  3977. 'environment':
  3978. {'YEP': '1'},
  3979. 'image': 'busybox',
  3980. 'name': 'otherweb'
  3981. },
  3982. {
  3983. 'environment':
  3984. {'YEP': '1', 'BAZ': '3'},
  3985. 'image': 'busybox',
  3986. 'name': 'web'
  3987. }
  3988. ])
  3989. def test_circular(self):
  3990. with pytest.raises(config.CircularReference) as exc:
  3991. load_from_filename('tests/fixtures/extends/circle-1.yml')
  3992. path = [
  3993. (os.path.basename(filename), service_name)
  3994. for (filename, service_name) in exc.value.trail
  3995. ]
  3996. expected = [
  3997. ('circle-1.yml', 'web'),
  3998. ('circle-2.yml', 'other'),
  3999. ('circle-1.yml', 'web'),
  4000. ]
  4001. assert path == expected
  4002. def test_extends_validation_empty_dictionary(self):
  4003. with pytest.raises(ConfigurationError) as excinfo:
  4004. config.load(
  4005. build_config_details(
  4006. {
  4007. 'web': {'image': 'busybox', 'extends': {}},
  4008. },
  4009. 'tests/fixtures/extends',
  4010. 'filename.yml'
  4011. )
  4012. )
  4013. assert 'service' in excinfo.exconly()
  4014. def test_extends_validation_missing_service_key(self):
  4015. with pytest.raises(ConfigurationError) as excinfo:
  4016. config.load(
  4017. build_config_details(
  4018. {
  4019. 'web': {'image': 'busybox', 'extends': {'file': 'common.yml'}},
  4020. },
  4021. 'tests/fixtures/extends',
  4022. 'filename.yml'
  4023. )
  4024. )
  4025. assert "'service' is a required property" in excinfo.exconly()
  4026. def test_extends_validation_invalid_key(self):
  4027. with pytest.raises(ConfigurationError) as excinfo:
  4028. config.load(
  4029. build_config_details(
  4030. {
  4031. 'web': {
  4032. 'image': 'busybox',
  4033. 'extends': {
  4034. 'file': 'common.yml',
  4035. 'service': 'web',
  4036. 'rogue_key': 'is not allowed'
  4037. }
  4038. },
  4039. },
  4040. 'tests/fixtures/extends',
  4041. 'filename.yml'
  4042. )
  4043. )
  4044. assert "web.extends contains unsupported option: 'rogue_key'" \
  4045. in excinfo.exconly()
  4046. def test_extends_validation_sub_property_key(self):
  4047. with pytest.raises(ConfigurationError) as excinfo:
  4048. config.load(
  4049. build_config_details(
  4050. {
  4051. 'web': {
  4052. 'image': 'busybox',
  4053. 'extends': {
  4054. 'file': 1,
  4055. 'service': 'web',
  4056. }
  4057. },
  4058. },
  4059. 'tests/fixtures/extends',
  4060. 'filename.yml'
  4061. )
  4062. )
  4063. assert "web.extends.file contains 1, which is an invalid type, it should be a string" \
  4064. in excinfo.exconly()
  4065. def test_extends_validation_no_file_key_no_filename_set(self):
  4066. dictionary = {'extends': {'service': 'web'}}
  4067. with pytest.raises(ConfigurationError) as excinfo:
  4068. make_service_dict('myweb', dictionary, working_dir='tests/fixtures/extends')
  4069. assert 'file' in excinfo.exconly()
  4070. def test_extends_validation_valid_config(self):
  4071. service = config.load(
  4072. build_config_details(
  4073. {
  4074. 'web': {'image': 'busybox', 'extends': {'service': 'web', 'file': 'common.yml'}},
  4075. },
  4076. 'tests/fixtures/extends',
  4077. 'common.yml'
  4078. )
  4079. ).services
  4080. assert len(service) == 1
  4081. assert isinstance(service[0], dict)
  4082. assert service[0]['command'] == "/bin/true"
  4083. def test_extended_service_with_invalid_config(self):
  4084. with pytest.raises(ConfigurationError) as exc:
  4085. load_from_filename('tests/fixtures/extends/service-with-invalid-schema.yml')
  4086. assert (
  4087. "myweb has neither an image nor a build context specified" in
  4088. exc.exconly()
  4089. )
  4090. def test_extended_service_with_valid_config(self):
  4091. service = load_from_filename('tests/fixtures/extends/service-with-valid-composite-extends.yml')
  4092. assert service[0]['command'] == "top"
  4093. def test_extends_file_defaults_to_self(self):
  4094. """
  4095. Test not specifying a file in our extends options that the
  4096. config is valid and correctly extends from itself.
  4097. """
  4098. service_dicts = load_from_filename('tests/fixtures/extends/no-file-specified.yml')
  4099. assert service_sort(service_dicts) == service_sort([
  4100. {
  4101. 'name': 'myweb',
  4102. 'image': 'busybox',
  4103. 'environment': {
  4104. "BAR": "1",
  4105. "BAZ": "3",
  4106. }
  4107. },
  4108. {
  4109. 'name': 'web',
  4110. 'image': 'busybox',
  4111. 'environment': {
  4112. "BAZ": "3",
  4113. }
  4114. }
  4115. ])
  4116. def test_invalid_links_in_extended_service(self):
  4117. with pytest.raises(ConfigurationError) as excinfo:
  4118. load_from_filename('tests/fixtures/extends/invalid-links.yml')
  4119. assert "services with 'links' cannot be extended" in excinfo.exconly()
  4120. def test_invalid_volumes_from_in_extended_service(self):
  4121. with pytest.raises(ConfigurationError) as excinfo:
  4122. load_from_filename('tests/fixtures/extends/invalid-volumes.yml')
  4123. assert "services with 'volumes_from' cannot be extended" in excinfo.exconly()
  4124. def test_invalid_net_in_extended_service(self):
  4125. with pytest.raises(ConfigurationError) as excinfo:
  4126. load_from_filename('tests/fixtures/extends/invalid-net-v2.yml')
  4127. assert 'network_mode: service' in excinfo.exconly()
  4128. assert 'cannot be extended' in excinfo.exconly()
  4129. with pytest.raises(ConfigurationError) as excinfo:
  4130. load_from_filename('tests/fixtures/extends/invalid-net.yml')
  4131. assert 'net: container' in excinfo.exconly()
  4132. assert 'cannot be extended' in excinfo.exconly()
  4133. @mock.patch.dict(os.environ)
  4134. def test_load_config_runs_interpolation_in_extended_service(self):
  4135. os.environ.update(HOSTNAME_VALUE="penguin")
  4136. expected_interpolated_value = "host-penguin"
  4137. service_dicts = load_from_filename(
  4138. 'tests/fixtures/extends/valid-interpolation.yml')
  4139. for service in service_dicts:
  4140. assert service['hostname'] == expected_interpolated_value
  4141. @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash')
  4142. def test_volume_path(self):
  4143. dicts = load_from_filename('tests/fixtures/volume-path/docker-compose.yml')
  4144. paths = [
  4145. VolumeSpec(
  4146. os.path.abspath('tests/fixtures/volume-path/common/foo'),
  4147. '/foo',
  4148. 'rw'),
  4149. VolumeSpec(
  4150. os.path.abspath('tests/fixtures/volume-path/bar'),
  4151. '/bar',
  4152. 'rw')
  4153. ]
  4154. assert set(dicts[0]['volumes']) == set(paths)
  4155. def test_parent_build_path_dne(self):
  4156. child = load_from_filename('tests/fixtures/extends/nonexistent-path-child.yml')
  4157. assert child == [
  4158. {
  4159. 'name': 'dnechild',
  4160. 'image': 'busybox',
  4161. 'command': '/bin/true',
  4162. 'environment': {
  4163. "FOO": "1",
  4164. "BAR": "2",
  4165. },
  4166. },
  4167. ]
  4168. def test_load_throws_error_when_base_service_does_not_exist(self):
  4169. with pytest.raises(ConfigurationError) as excinfo:
  4170. load_from_filename('tests/fixtures/extends/nonexistent-service.yml')
  4171. assert "Cannot extend service 'foo'" in excinfo.exconly()
  4172. assert "Service not found" in excinfo.exconly()
  4173. def test_partial_service_config_in_extends_is_still_valid(self):
  4174. dicts = load_from_filename('tests/fixtures/extends/valid-common-config.yml')
  4175. assert dicts[0]['environment'] == {'FOO': '1'}
  4176. def test_extended_service_with_verbose_and_shorthand_way(self):
  4177. services = load_from_filename('tests/fixtures/extends/verbose-and-shorthand.yml')
  4178. assert service_sort(services) == service_sort([
  4179. {
  4180. 'name': 'base',
  4181. 'image': 'busybox',
  4182. 'environment': {'BAR': '1'},
  4183. },
  4184. {
  4185. 'name': 'verbose',
  4186. 'image': 'busybox',
  4187. 'environment': {'BAR': '1', 'FOO': '1'},
  4188. },
  4189. {
  4190. 'name': 'shorthand',
  4191. 'image': 'busybox',
  4192. 'environment': {'BAR': '1', 'FOO': '2'},
  4193. },
  4194. ])
  4195. @mock.patch.dict(os.environ)
  4196. def test_extends_with_environment_and_env_files(self):
  4197. tmpdir = py.test.ensuretemp('test_extends_with_environment')
  4198. self.addCleanup(tmpdir.remove)
  4199. commondir = tmpdir.mkdir('common')
  4200. commondir.join('base.yml').write("""
  4201. app:
  4202. image: 'example/app'
  4203. env_file:
  4204. - 'envs'
  4205. environment:
  4206. - SECRET
  4207. - TEST_ONE=common
  4208. - TEST_TWO=common
  4209. """)
  4210. tmpdir.join('docker-compose.yml').write("""
  4211. ext:
  4212. extends:
  4213. file: common/base.yml
  4214. service: app
  4215. env_file:
  4216. - 'envs'
  4217. environment:
  4218. - THING
  4219. - TEST_ONE=top
  4220. """)
  4221. commondir.join('envs').write("""
  4222. COMMON_ENV_FILE
  4223. TEST_ONE=common-env-file
  4224. TEST_TWO=common-env-file
  4225. TEST_THREE=common-env-file
  4226. TEST_FOUR=common-env-file
  4227. """)
  4228. tmpdir.join('envs').write("""
  4229. TOP_ENV_FILE
  4230. TEST_ONE=top-env-file
  4231. TEST_TWO=top-env-file
  4232. TEST_THREE=top-env-file
  4233. """)
  4234. expected = [
  4235. {
  4236. 'name': 'ext',
  4237. 'image': 'example/app',
  4238. 'environment': {
  4239. 'SECRET': 'secret',
  4240. 'TOP_ENV_FILE': 'secret',
  4241. 'COMMON_ENV_FILE': 'secret',
  4242. 'THING': 'thing',
  4243. 'TEST_ONE': 'top',
  4244. 'TEST_TWO': 'common',
  4245. 'TEST_THREE': 'top-env-file',
  4246. 'TEST_FOUR': 'common-env-file',
  4247. },
  4248. },
  4249. ]
  4250. os.environ['SECRET'] = 'secret'
  4251. os.environ['THING'] = 'thing'
  4252. os.environ['COMMON_ENV_FILE'] = 'secret'
  4253. os.environ['TOP_ENV_FILE'] = 'secret'
  4254. config = load_from_filename(str(tmpdir.join('docker-compose.yml')))
  4255. assert config == expected
  4256. def test_extends_with_mixed_versions_is_error(self):
  4257. tmpdir = py.test.ensuretemp('test_extends_with_mixed_version')
  4258. self.addCleanup(tmpdir.remove)
  4259. tmpdir.join('docker-compose.yml').write("""
  4260. version: "2"
  4261. services:
  4262. web:
  4263. extends:
  4264. file: base.yml
  4265. service: base
  4266. image: busybox
  4267. """)
  4268. tmpdir.join('base.yml').write("""
  4269. base:
  4270. volumes: ['/foo']
  4271. ports: ['3000:3000']
  4272. """)
  4273. with pytest.raises(ConfigurationError) as exc:
  4274. load_from_filename(str(tmpdir.join('docker-compose.yml')))
  4275. assert 'Version mismatch' in exc.exconly()
  4276. def test_extends_with_defined_version_passes(self):
  4277. tmpdir = py.test.ensuretemp('test_extends_with_defined_version')
  4278. self.addCleanup(tmpdir.remove)
  4279. tmpdir.join('docker-compose.yml').write("""
  4280. version: "2"
  4281. services:
  4282. web:
  4283. extends:
  4284. file: base.yml
  4285. service: base
  4286. image: busybox
  4287. """)
  4288. tmpdir.join('base.yml').write("""
  4289. version: "2"
  4290. services:
  4291. base:
  4292. volumes: ['/foo']
  4293. ports: ['3000:3000']
  4294. command: top
  4295. """)
  4296. service = load_from_filename(str(tmpdir.join('docker-compose.yml')))
  4297. assert service[0]['command'] == "top"
  4298. def test_extends_with_depends_on(self):
  4299. tmpdir = py.test.ensuretemp('test_extends_with_depends_on')
  4300. self.addCleanup(tmpdir.remove)
  4301. tmpdir.join('docker-compose.yml').write("""
  4302. version: "2"
  4303. services:
  4304. base:
  4305. image: example
  4306. web:
  4307. extends: base
  4308. image: busybox
  4309. depends_on: ['other']
  4310. other:
  4311. image: example
  4312. """)
  4313. services = load_from_filename(str(tmpdir.join('docker-compose.yml')))
  4314. assert service_sort(services)[2]['depends_on'] == {
  4315. 'other': {'condition': 'service_started'}
  4316. }
  4317. def test_extends_with_healthcheck(self):
  4318. service_dicts = load_from_filename('tests/fixtures/extends/healthcheck-2.yml')
  4319. assert service_sort(service_dicts) == [{
  4320. 'name': 'demo',
  4321. 'image': 'foobar:latest',
  4322. 'healthcheck': {
  4323. 'test': ['CMD', '/health.sh'],
  4324. 'interval': 10000000000,
  4325. 'timeout': 5000000000,
  4326. 'retries': 36,
  4327. }
  4328. }]
  4329. def test_extends_with_ports(self):
  4330. tmpdir = py.test.ensuretemp('test_extends_with_ports')
  4331. self.addCleanup(tmpdir.remove)
  4332. tmpdir.join('docker-compose.yml').write("""
  4333. version: '2'
  4334. services:
  4335. a:
  4336. image: nginx
  4337. ports:
  4338. - 80
  4339. b:
  4340. extends:
  4341. service: a
  4342. """)
  4343. services = load_from_filename(str(tmpdir.join('docker-compose.yml')))
  4344. assert len(services) == 2
  4345. for svc in services:
  4346. assert svc['ports'] == [types.ServicePort('80', None, None, None, None)]
  4347. def test_extends_with_security_opt(self):
  4348. tmpdir = py.test.ensuretemp('test_extends_with_ports')
  4349. self.addCleanup(tmpdir.remove)
  4350. tmpdir.join('docker-compose.yml').write("""
  4351. version: '2'
  4352. services:
  4353. a:
  4354. image: nginx
  4355. security_opt:
  4356. - apparmor:unconfined
  4357. - seccomp:unconfined
  4358. b:
  4359. extends:
  4360. service: a
  4361. """)
  4362. services = load_from_filename(str(tmpdir.join('docker-compose.yml')))
  4363. assert len(services) == 2
  4364. for svc in services:
  4365. assert types.SecurityOpt.parse('apparmor:unconfined') in svc['security_opt']
  4366. assert types.SecurityOpt.parse('seccomp:unconfined') in svc['security_opt']
  4367. @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash')
  4368. class ExpandPathTest(unittest.TestCase):
  4369. working_dir = '/home/user/somedir'
  4370. def test_expand_path_normal(self):
  4371. result = config.expand_path(self.working_dir, 'myfile')
  4372. assert result == self.working_dir + '/' + 'myfile'
  4373. def test_expand_path_absolute(self):
  4374. abs_path = '/home/user/otherdir/somefile'
  4375. result = config.expand_path(self.working_dir, abs_path)
  4376. assert result == abs_path
  4377. def test_expand_path_with_tilde(self):
  4378. test_path = '~/otherdir/somefile'
  4379. with mock.patch.dict(os.environ):
  4380. os.environ['HOME'] = user_path = '/home/user/'
  4381. result = config.expand_path(self.working_dir, test_path)
  4382. assert result == user_path + 'otherdir/somefile'
  4383. class VolumePathTest(unittest.TestCase):
  4384. def test_split_path_mapping_with_windows_path(self):
  4385. host_path = "c:\\Users\\msamblanet\\Documents\\anvil\\connect\\config"
  4386. windows_volume_path = host_path + ":/opt/connect/config:ro"
  4387. expected_mapping = ("/opt/connect/config", (host_path, 'ro'))
  4388. mapping = config.split_path_mapping(windows_volume_path)
  4389. assert mapping == expected_mapping
  4390. def test_split_path_mapping_with_windows_path_in_container(self):
  4391. host_path = 'c:\\Users\\remilia\\data'
  4392. container_path = 'c:\\scarletdevil\\data'
  4393. expected_mapping = (container_path, (host_path, None))
  4394. mapping = config.split_path_mapping('{0}:{1}'.format(host_path, container_path))
  4395. assert mapping == expected_mapping
  4396. def test_split_path_mapping_with_root_mount(self):
  4397. host_path = '/'
  4398. container_path = '/var/hostroot'
  4399. expected_mapping = (container_path, (host_path, None))
  4400. mapping = config.split_path_mapping('{0}:{1}'.format(host_path, container_path))
  4401. assert mapping == expected_mapping
  4402. @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash')
  4403. class BuildPathTest(unittest.TestCase):
  4404. def setUp(self):
  4405. self.abs_context_path = os.path.join(os.getcwd(), 'tests/fixtures/build-ctx')
  4406. def test_nonexistent_path(self):
  4407. with pytest.raises(ConfigurationError):
  4408. config.load(
  4409. build_config_details(
  4410. {
  4411. 'foo': {'build': 'nonexistent.path'},
  4412. },
  4413. 'working_dir',
  4414. 'filename.yml'
  4415. )
  4416. )
  4417. def test_relative_path(self):
  4418. relative_build_path = '../build-ctx/'
  4419. service_dict = make_service_dict(
  4420. 'relpath',
  4421. {'build': relative_build_path},
  4422. working_dir='tests/fixtures/build-path'
  4423. )
  4424. assert service_dict['build'] == self.abs_context_path
  4425. def test_absolute_path(self):
  4426. service_dict = make_service_dict(
  4427. 'abspath',
  4428. {'build': self.abs_context_path},
  4429. working_dir='tests/fixtures/build-path'
  4430. )
  4431. assert service_dict['build'] == self.abs_context_path
  4432. def test_from_file(self):
  4433. service_dict = load_from_filename('tests/fixtures/build-path/docker-compose.yml')
  4434. assert service_dict == [{'name': 'foo', 'build': {'context': self.abs_context_path}}]
  4435. def test_from_file_override_dir(self):
  4436. override_dir = os.path.join(os.getcwd(), 'tests/fixtures/')
  4437. service_dict = load_from_filename(
  4438. 'tests/fixtures/build-path-override-dir/docker-compose.yml', override_dir=override_dir)
  4439. assert service_dict == [{'name': 'foo', 'build': {'context': self.abs_context_path}}]
  4440. def test_valid_url_in_build_path(self):
  4441. valid_urls = [
  4442. 'git://github.com/docker/docker',
  4443. '[email protected]:docker/docker.git',
  4444. '[email protected]:atlassianlabs/atlassian-docker.git',
  4445. 'https://github.com/docker/docker.git',
  4446. 'http://github.com/docker/docker.git',
  4447. 'github.com/docker/docker.git',
  4448. ]
  4449. for valid_url in valid_urls:
  4450. service_dict = config.load(build_config_details({
  4451. 'validurl': {'build': valid_url},
  4452. }, '.', None)).services
  4453. assert service_dict[0]['build'] == {'context': valid_url}
  4454. def test_invalid_url_in_build_path(self):
  4455. invalid_urls = [
  4456. 'example.com/bogus',
  4457. 'ftp://example.com/',
  4458. '/path/does/not/exist',
  4459. ]
  4460. for invalid_url in invalid_urls:
  4461. with pytest.raises(ConfigurationError) as exc:
  4462. config.load(build_config_details({
  4463. 'invalidurl': {'build': invalid_url},
  4464. }, '.', None))
  4465. assert 'build path' in exc.exconly()
  4466. class HealthcheckTest(unittest.TestCase):
  4467. def test_healthcheck(self):
  4468. config_dict = config.load(
  4469. build_config_details({
  4470. 'version': '2.3',
  4471. 'services': {
  4472. 'test': {
  4473. 'image': 'busybox',
  4474. 'healthcheck': {
  4475. 'test': ['CMD', 'true'],
  4476. 'interval': '1s',
  4477. 'timeout': '1m',
  4478. 'retries': 3,
  4479. 'start_period': '10s',
  4480. }
  4481. }
  4482. }
  4483. })
  4484. )
  4485. serialized_config = yaml.load(serialize_config(config_dict))
  4486. serialized_service = serialized_config['services']['test']
  4487. assert serialized_service['healthcheck'] == {
  4488. 'test': ['CMD', 'true'],
  4489. 'interval': '1s',
  4490. 'timeout': '1m',
  4491. 'retries': 3,
  4492. 'start_period': '10s'
  4493. }
  4494. def test_disable(self):
  4495. config_dict = config.load(
  4496. build_config_details({
  4497. 'version': '2.3',
  4498. 'services': {
  4499. 'test': {
  4500. 'image': 'busybox',
  4501. 'healthcheck': {
  4502. 'disable': True,
  4503. }
  4504. }
  4505. }
  4506. })
  4507. )
  4508. serialized_config = yaml.load(serialize_config(config_dict))
  4509. serialized_service = serialized_config['services']['test']
  4510. assert serialized_service['healthcheck'] == {
  4511. 'test': ['NONE'],
  4512. }
  4513. def test_disable_with_other_config_is_invalid(self):
  4514. with pytest.raises(ConfigurationError) as excinfo:
  4515. config.load(
  4516. build_config_details({
  4517. 'version': '2.3',
  4518. 'services': {
  4519. 'invalid-healthcheck': {
  4520. 'image': 'busybox',
  4521. 'healthcheck': {
  4522. 'disable': True,
  4523. 'interval': '1s',
  4524. }
  4525. }
  4526. }
  4527. })
  4528. )
  4529. assert 'invalid-healthcheck' in excinfo.exconly()
  4530. assert '"disable: true" cannot be combined with other options' in excinfo.exconly()
  4531. def test_healthcheck_with_invalid_test(self):
  4532. with pytest.raises(ConfigurationError) as excinfo:
  4533. config.load(
  4534. build_config_details({
  4535. 'version': '2.3',
  4536. 'services': {
  4537. 'invalid-healthcheck': {
  4538. 'image': 'busybox',
  4539. 'healthcheck': {
  4540. 'test': ['true'],
  4541. 'interval': '1s',
  4542. 'timeout': '1m',
  4543. 'retries': 3,
  4544. 'start_period': '10s',
  4545. }
  4546. }
  4547. }
  4548. })
  4549. )
  4550. assert 'invalid-healthcheck' in excinfo.exconly()
  4551. assert 'the first item must be either NONE, CMD or CMD-SHELL' in excinfo.exconly()
  4552. class GetDefaultConfigFilesTestCase(unittest.TestCase):
  4553. files = [
  4554. 'docker-compose.yml',
  4555. 'docker-compose.yaml',
  4556. ]
  4557. def test_get_config_path_default_file_in_basedir(self):
  4558. for index, filename in enumerate(self.files):
  4559. assert filename == get_config_filename_for_files(self.files[index:])
  4560. with pytest.raises(config.ComposeFileNotFound):
  4561. get_config_filename_for_files([])
  4562. def test_get_config_path_default_file_in_parent_dir(self):
  4563. """Test with files placed in the subdir"""
  4564. def get_config_in_subdir(files):
  4565. return get_config_filename_for_files(files, subdir=True)
  4566. for index, filename in enumerate(self.files):
  4567. assert filename == get_config_in_subdir(self.files[index:])
  4568. with pytest.raises(config.ComposeFileNotFound):
  4569. get_config_in_subdir([])
  4570. def get_config_filename_for_files(filenames, subdir=None):
  4571. def make_files(dirname, filenames):
  4572. for fname in filenames:
  4573. with open(os.path.join(dirname, fname), 'w') as f:
  4574. f.write('')
  4575. project_dir = tempfile.mkdtemp()
  4576. try:
  4577. make_files(project_dir, filenames)
  4578. if subdir:
  4579. base_dir = tempfile.mkdtemp(dir=project_dir)
  4580. else:
  4581. base_dir = project_dir
  4582. filename, = config.get_default_config_files(base_dir)
  4583. return os.path.basename(filename)
  4584. finally:
  4585. shutil.rmtree(project_dir)
  4586. class SerializeTest(unittest.TestCase):
  4587. def test_denormalize_depends_on_v3(self):
  4588. service_dict = {
  4589. 'image': 'busybox',
  4590. 'command': 'true',
  4591. 'depends_on': {
  4592. 'service2': {'condition': 'service_started'},
  4593. 'service3': {'condition': 'service_started'},
  4594. }
  4595. }
  4596. assert denormalize_service_dict(service_dict, V3_0) == {
  4597. 'image': 'busybox',
  4598. 'command': 'true',
  4599. 'depends_on': ['service2', 'service3']
  4600. }
  4601. def test_denormalize_depends_on_v2_1(self):
  4602. service_dict = {
  4603. 'image': 'busybox',
  4604. 'command': 'true',
  4605. 'depends_on': {
  4606. 'service2': {'condition': 'service_started'},
  4607. 'service3': {'condition': 'service_started'},
  4608. }
  4609. }
  4610. assert denormalize_service_dict(service_dict, V2_1) == service_dict
  4611. def test_serialize_time(self):
  4612. data = {
  4613. 9: '9ns',
  4614. 9000: '9us',
  4615. 9000000: '9ms',
  4616. 90000000: '90ms',
  4617. 900000000: '900ms',
  4618. 999999999: '999999999ns',
  4619. 1000000000: '1s',
  4620. 60000000000: '1m',
  4621. 60000000001: '60000000001ns',
  4622. 9000000000000: '150m',
  4623. 90000000000000: '25h',
  4624. }
  4625. for k, v in data.items():
  4626. assert serialize_ns_time_value(k) == v
  4627. def test_denormalize_healthcheck(self):
  4628. service_dict = {
  4629. 'image': 'test',
  4630. 'healthcheck': {
  4631. 'test': 'exit 1',
  4632. 'interval': '1m40s',
  4633. 'timeout': '30s',
  4634. 'retries': 5,
  4635. 'start_period': '2s90ms'
  4636. }
  4637. }
  4638. processed_service = config.process_service(config.ServiceConfig(
  4639. '.', 'test', 'test', service_dict
  4640. ))
  4641. denormalized_service = denormalize_service_dict(processed_service, V2_3)
  4642. assert denormalized_service['healthcheck']['interval'] == '100s'
  4643. assert denormalized_service['healthcheck']['timeout'] == '30s'
  4644. assert denormalized_service['healthcheck']['start_period'] == '2090ms'
  4645. def test_denormalize_image_has_digest(self):
  4646. service_dict = {
  4647. 'image': 'busybox'
  4648. }
  4649. image_digest = 'busybox@sha256:abcde'
  4650. assert denormalize_service_dict(service_dict, V3_0, image_digest) == {
  4651. 'image': 'busybox@sha256:abcde'
  4652. }
  4653. def test_denormalize_image_no_digest(self):
  4654. service_dict = {
  4655. 'image': 'busybox'
  4656. }
  4657. assert denormalize_service_dict(service_dict, V3_0) == {
  4658. 'image': 'busybox'
  4659. }
  4660. def test_serialize_secrets(self):
  4661. service_dict = {
  4662. 'image': 'example/web',
  4663. 'secrets': [
  4664. {'source': 'one'},
  4665. {
  4666. 'source': 'source',
  4667. 'target': 'target',
  4668. 'uid': '100',
  4669. 'gid': '200',
  4670. 'mode': 0o777,
  4671. }
  4672. ]
  4673. }
  4674. secrets_dict = {
  4675. 'one': {'file': '/one.txt'},
  4676. 'source': {'file': '/source.pem'},
  4677. 'two': {'external': True},
  4678. }
  4679. config_dict = config.load(build_config_details({
  4680. 'version': '3.1',
  4681. 'services': {'web': service_dict},
  4682. 'secrets': secrets_dict
  4683. }))
  4684. serialized_config = yaml.load(serialize_config(config_dict))
  4685. serialized_service = serialized_config['services']['web']
  4686. assert secret_sort(serialized_service['secrets']) == secret_sort(service_dict['secrets'])
  4687. assert 'secrets' in serialized_config
  4688. assert serialized_config['secrets']['two'] == secrets_dict['two']
  4689. def test_serialize_ports(self):
  4690. config_dict = config.Config(version=V2_0, services=[
  4691. {
  4692. 'ports': [types.ServicePort('80', '8080', None, None, None)],
  4693. 'image': 'alpine',
  4694. 'name': 'web'
  4695. }
  4696. ], volumes={}, networks={}, secrets={}, configs={})
  4697. serialized_config = yaml.load(serialize_config(config_dict))
  4698. assert '8080:80/tcp' in serialized_config['services']['web']['ports']
  4699. def test_serialize_ports_with_ext_ip(self):
  4700. config_dict = config.Config(version=V3_5, services=[
  4701. {
  4702. 'ports': [types.ServicePort('80', '8080', None, None, '127.0.0.1')],
  4703. 'image': 'alpine',
  4704. 'name': 'web'
  4705. }
  4706. ], volumes={}, networks={}, secrets={}, configs={})
  4707. serialized_config = yaml.load(serialize_config(config_dict))
  4708. assert '127.0.0.1:8080:80/tcp' in serialized_config['services']['web']['ports']
  4709. def test_serialize_configs(self):
  4710. service_dict = {
  4711. 'image': 'example/web',
  4712. 'configs': [
  4713. {'source': 'one'},
  4714. {
  4715. 'source': 'source',
  4716. 'target': 'target',
  4717. 'uid': '100',
  4718. 'gid': '200',
  4719. 'mode': 0o777,
  4720. }
  4721. ]
  4722. }
  4723. configs_dict = {
  4724. 'one': {'file': '/one.txt'},
  4725. 'source': {'file': '/source.pem'},
  4726. 'two': {'external': True},
  4727. }
  4728. config_dict = config.load(build_config_details({
  4729. 'version': '3.3',
  4730. 'services': {'web': service_dict},
  4731. 'configs': configs_dict
  4732. }))
  4733. serialized_config = yaml.load(serialize_config(config_dict))
  4734. serialized_service = serialized_config['services']['web']
  4735. assert secret_sort(serialized_service['configs']) == secret_sort(service_dict['configs'])
  4736. assert 'configs' in serialized_config
  4737. assert serialized_config['configs']['two'] == configs_dict['two']
  4738. def test_serialize_bool_string(self):
  4739. cfg = {
  4740. 'version': '2.2',
  4741. 'services': {
  4742. 'web': {
  4743. 'image': 'example/web',
  4744. 'command': 'true',
  4745. 'environment': {'FOO': 'Y', 'BAR': 'on'}
  4746. }
  4747. }
  4748. }
  4749. config_dict = config.load(build_config_details(cfg))
  4750. serialized_config = serialize_config(config_dict)
  4751. assert 'command: "true"\n' in serialized_config
  4752. assert 'FOO: "Y"\n' in serialized_config
  4753. assert 'BAR: "on"\n' in serialized_config
  4754. def test_serialize_escape_dollar_sign(self):
  4755. cfg = {
  4756. 'version': '2.2',
  4757. 'services': {
  4758. 'web': {
  4759. 'image': 'busybox',
  4760. 'command': 'echo $$FOO',
  4761. 'environment': {
  4762. 'CURRENCY': '$$'
  4763. },
  4764. 'entrypoint': ['$$SHELL', '-c'],
  4765. }
  4766. }
  4767. }
  4768. config_dict = config.load(build_config_details(cfg))
  4769. serialized_config = yaml.load(serialize_config(config_dict))
  4770. serialized_service = serialized_config['services']['web']
  4771. assert serialized_service['environment']['CURRENCY'] == '$$'
  4772. assert serialized_service['command'] == 'echo $$FOO'
  4773. assert serialized_service['entrypoint'][0] == '$$SHELL'
  4774. def test_serialize_escape_dont_interpolate(self):
  4775. cfg = {
  4776. 'version': '2.2',
  4777. 'services': {
  4778. 'web': {
  4779. 'image': 'busybox',
  4780. 'command': 'echo $FOO',
  4781. 'environment': {
  4782. 'CURRENCY': '$'
  4783. },
  4784. 'entrypoint': ['$SHELL', '-c'],
  4785. }
  4786. }
  4787. }
  4788. config_dict = config.load(build_config_details(cfg), interpolate=False)
  4789. serialized_config = yaml.load(serialize_config(config_dict, escape_dollar=False))
  4790. serialized_service = serialized_config['services']['web']
  4791. assert serialized_service['environment']['CURRENCY'] == '$'
  4792. assert serialized_service['command'] == 'echo $FOO'
  4793. assert serialized_service['entrypoint'][0] == '$SHELL'
  4794. def test_serialize_unicode_values(self):
  4795. cfg = {
  4796. 'version': '2.3',
  4797. 'services': {
  4798. 'web': {
  4799. 'image': 'busybox',
  4800. 'command': 'echo 十六夜 咲夜'
  4801. }
  4802. }
  4803. }
  4804. config_dict = config.load(build_config_details(cfg))
  4805. serialized_config = yaml.load(serialize_config(config_dict))
  4806. serialized_service = serialized_config['services']['web']
  4807. assert serialized_service['command'] == 'echo 十六夜 咲夜'
  4808. def test_serialize_external_false(self):
  4809. cfg = {
  4810. 'version': '3.4',
  4811. 'volumes': {
  4812. 'test': {
  4813. 'name': 'test-false',
  4814. 'external': False
  4815. }
  4816. }
  4817. }
  4818. config_dict = config.load(build_config_details(cfg))
  4819. serialized_config = yaml.load(serialize_config(config_dict))
  4820. serialized_volume = serialized_config['volumes']['test']
  4821. assert serialized_volume['external'] is False