config_test.py 167 KB

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