12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033 |
- /*
- Copyright 2014 The Kubernetes Authors.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package validation
- import (
- "math/rand"
- "reflect"
- "strings"
- "testing"
- "time"
- "k8s.io/kubernetes/pkg/api"
- "k8s.io/kubernetes/pkg/api/resource"
- "k8s.io/kubernetes/pkg/api/service"
- "k8s.io/kubernetes/pkg/api/testapi"
- "k8s.io/kubernetes/pkg/api/unversioned"
- "k8s.io/kubernetes/pkg/capabilities"
- "k8s.io/kubernetes/pkg/security/apparmor"
- "k8s.io/kubernetes/pkg/util/intstr"
- "k8s.io/kubernetes/pkg/util/sets"
- "k8s.io/kubernetes/pkg/util/validation/field"
- )
- func expectPrefix(t *testing.T, prefix string, errs field.ErrorList) {
- for i := range errs {
- if f, p := errs[i].Field, prefix; !strings.HasPrefix(f, p) {
- t.Errorf("expected prefix '%s' for field '%s' (%v)", p, f, errs[i])
- }
- }
- }
- // Ensure custom name functions are allowed
- func TestValidateObjectMetaCustomName(t *testing.T) {
- errs := ValidateObjectMeta(
- &api.ObjectMeta{Name: "test", GenerateName: "foo"},
- false,
- func(s string, prefix bool) []string {
- if s == "test" {
- return nil
- }
- return []string{"name-gen"}
- },
- field.NewPath("field"))
- if len(errs) != 1 {
- t.Fatalf("unexpected errors: %v", errs)
- }
- if !strings.Contains(errs[0].Error(), "name-gen") {
- t.Errorf("unexpected error message: %v", errs)
- }
- }
- // Ensure namespace names follow dns label format
- func TestValidateObjectMetaNamespaces(t *testing.T) {
- errs := ValidateObjectMeta(
- &api.ObjectMeta{Name: "test", Namespace: "foo.bar"},
- true,
- func(s string, prefix bool) []string {
- return nil
- },
- field.NewPath("field"))
- if len(errs) != 1 {
- t.Fatalf("unexpected errors: %v", errs)
- }
- if !strings.Contains(errs[0].Error(), `Invalid value: "foo.bar"`) {
- t.Errorf("unexpected error message: %v", errs)
- }
- maxLength := 63
- letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
- b := make([]rune, maxLength+1)
- for i := range b {
- b[i] = letters[rand.Intn(len(letters))]
- }
- errs = ValidateObjectMeta(
- &api.ObjectMeta{Name: "test", Namespace: string(b)},
- true,
- func(s string, prefix bool) []string {
- return nil
- },
- field.NewPath("field"))
- if len(errs) != 2 {
- t.Fatalf("unexpected errors: %v", errs)
- }
- if !strings.Contains(errs[0].Error(), "Invalid value") || !strings.Contains(errs[1].Error(), "Invalid value") {
- t.Errorf("unexpected error message: %v", errs)
- }
- }
- func TestValidateObjectMetaOwnerReferences(t *testing.T) {
- trueVar := true
- falseVar := false
- testCases := []struct {
- description string
- ownerReferences []api.OwnerReference
- expectError bool
- expectedErrorMessage string
- }{
- {
- description: "simple success - third party extension.",
- ownerReferences: []api.OwnerReference{
- {
- APIVersion: "thirdpartyVersion",
- Kind: "thirdpartyKind",
- Name: "name",
- UID: "1",
- },
- },
- expectError: false,
- expectedErrorMessage: "",
- },
- {
- description: "simple failures - event shouldn't be set as an owner",
- ownerReferences: []api.OwnerReference{
- {
- APIVersion: "v1",
- Kind: "Event",
- Name: "name",
- UID: "1",
- },
- },
- expectError: true,
- expectedErrorMessage: "is disallowed from being an owner",
- },
- {
- description: "simple controller ref success - one reference with Controller set",
- ownerReferences: []api.OwnerReference{
- {
- APIVersion: "thirdpartyVersion",
- Kind: "thirdpartyKind",
- Name: "name",
- UID: "1",
- Controller: &falseVar,
- },
- {
- APIVersion: "thirdpartyVersion",
- Kind: "thirdpartyKind",
- Name: "name",
- UID: "2",
- Controller: &trueVar,
- },
- {
- APIVersion: "thirdpartyVersion",
- Kind: "thirdpartyKind",
- Name: "name",
- UID: "3",
- Controller: &falseVar,
- },
- {
- APIVersion: "thirdpartyVersion",
- Kind: "thirdpartyKind",
- Name: "name",
- UID: "4",
- },
- },
- expectError: false,
- expectedErrorMessage: "",
- },
- {
- description: "simple controller ref failure - two references with Controller set",
- ownerReferences: []api.OwnerReference{
- {
- APIVersion: "thirdpartyVersion",
- Kind: "thirdpartyKind",
- Name: "name",
- UID: "1",
- Controller: &falseVar,
- },
- {
- APIVersion: "thirdpartyVersion",
- Kind: "thirdpartyKind",
- Name: "name",
- UID: "2",
- Controller: &trueVar,
- },
- {
- APIVersion: "thirdpartyVersion",
- Kind: "thirdpartyKind",
- Name: "name",
- UID: "3",
- Controller: &trueVar,
- },
- {
- APIVersion: "thirdpartyVersion",
- Kind: "thirdpartyKind",
- Name: "name",
- UID: "4",
- },
- },
- expectError: true,
- expectedErrorMessage: "Only one reference can have Controller set to true",
- },
- }
- for _, tc := range testCases {
- errs := ValidateObjectMeta(
- &api.ObjectMeta{Name: "test", Namespace: "test", OwnerReferences: tc.ownerReferences},
- true,
- func(s string, prefix bool) []string {
- return nil
- },
- field.NewPath("field"))
- if len(errs) != 0 && !tc.expectError {
- t.Errorf("unexpected error: %v in test case %v", errs, tc.description)
- }
- if len(errs) == 0 && tc.expectError {
- t.Errorf("expect error in test case %v", tc.description)
- }
- if len(errs) != 0 && !strings.Contains(errs[0].Error(), tc.expectedErrorMessage) {
- t.Errorf("unexpected error message: %v in test case %v", errs, tc.description)
- }
- }
- }
- func TestValidateObjectMetaUpdateIgnoresCreationTimestamp(t *testing.T) {
- if errs := ValidateObjectMetaUpdate(
- &api.ObjectMeta{Name: "test", ResourceVersion: "1"},
- &api.ObjectMeta{Name: "test", ResourceVersion: "1", CreationTimestamp: unversioned.NewTime(time.Unix(10, 0))},
- field.NewPath("field"),
- ); len(errs) != 0 {
- t.Fatalf("unexpected errors: %v", errs)
- }
- if errs := ValidateObjectMetaUpdate(
- &api.ObjectMeta{Name: "test", ResourceVersion: "1", CreationTimestamp: unversioned.NewTime(time.Unix(10, 0))},
- &api.ObjectMeta{Name: "test", ResourceVersion: "1"},
- field.NewPath("field"),
- ); len(errs) != 0 {
- t.Fatalf("unexpected errors: %v", errs)
- }
- if errs := ValidateObjectMetaUpdate(
- &api.ObjectMeta{Name: "test", ResourceVersion: "1", CreationTimestamp: unversioned.NewTime(time.Unix(10, 0))},
- &api.ObjectMeta{Name: "test", ResourceVersion: "1", CreationTimestamp: unversioned.NewTime(time.Unix(11, 0))},
- field.NewPath("field"),
- ); len(errs) != 0 {
- t.Fatalf("unexpected errors: %v", errs)
- }
- }
- func TestValidateObjectMetaUpdatePreventsDeletionFieldMutation(t *testing.T) {
- now := unversioned.NewTime(time.Unix(1000, 0).UTC())
- later := unversioned.NewTime(time.Unix(2000, 0).UTC())
- gracePeriodShort := int64(30)
- gracePeriodLong := int64(40)
- testcases := map[string]struct {
- Old api.ObjectMeta
- New api.ObjectMeta
- ExpectedNew api.ObjectMeta
- ExpectedErrs []string
- }{
- "valid without deletion fields": {
- Old: api.ObjectMeta{Name: "test", ResourceVersion: "1"},
- New: api.ObjectMeta{Name: "test", ResourceVersion: "1"},
- ExpectedNew: api.ObjectMeta{Name: "test", ResourceVersion: "1"},
- ExpectedErrs: []string{},
- },
- "valid with deletion fields": {
- Old: api.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now, DeletionGracePeriodSeconds: &gracePeriodShort},
- New: api.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now, DeletionGracePeriodSeconds: &gracePeriodShort},
- ExpectedNew: api.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now, DeletionGracePeriodSeconds: &gracePeriodShort},
- ExpectedErrs: []string{},
- },
- "invalid set deletionTimestamp": {
- Old: api.ObjectMeta{Name: "test", ResourceVersion: "1"},
- New: api.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now},
- ExpectedNew: api.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now},
- ExpectedErrs: []string{"field.deletionTimestamp: Invalid value: \"1970-01-01T00:16:40Z\": field is immutable; may only be changed via deletion"},
- },
- "invalid clear deletionTimestamp": {
- Old: api.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now},
- New: api.ObjectMeta{Name: "test", ResourceVersion: "1"},
- ExpectedNew: api.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now},
- ExpectedErrs: []string{}, // no errors, validation copies the old value
- },
- "invalid change deletionTimestamp": {
- Old: api.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now},
- New: api.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &later},
- ExpectedNew: api.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now},
- ExpectedErrs: []string{}, // no errors, validation copies the old value
- },
- "invalid set deletionGracePeriodSeconds": {
- Old: api.ObjectMeta{Name: "test", ResourceVersion: "1"},
- New: api.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionGracePeriodSeconds: &gracePeriodShort},
- ExpectedNew: api.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionGracePeriodSeconds: &gracePeriodShort},
- ExpectedErrs: []string{"field.deletionGracePeriodSeconds: Invalid value: 30: field is immutable; may only be changed via deletion"},
- },
- "invalid clear deletionGracePeriodSeconds": {
- Old: api.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionGracePeriodSeconds: &gracePeriodShort},
- New: api.ObjectMeta{Name: "test", ResourceVersion: "1"},
- ExpectedNew: api.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionGracePeriodSeconds: &gracePeriodShort},
- ExpectedErrs: []string{}, // no errors, validation copies the old value
- },
- "invalid change deletionGracePeriodSeconds": {
- Old: api.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionGracePeriodSeconds: &gracePeriodShort},
- New: api.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionGracePeriodSeconds: &gracePeriodLong},
- ExpectedNew: api.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionGracePeriodSeconds: &gracePeriodLong},
- ExpectedErrs: []string{"field.deletionGracePeriodSeconds: Invalid value: 40: field is immutable; may only be changed via deletion"},
- },
- }
- for k, tc := range testcases {
- errs := ValidateObjectMetaUpdate(&tc.New, &tc.Old, field.NewPath("field"))
- if len(errs) != len(tc.ExpectedErrs) {
- t.Logf("%s: Expected: %#v", k, tc.ExpectedErrs)
- t.Logf("%s: Got: %#v", k, errs)
- t.Errorf("%s: expected %d errors, got %d", k, len(tc.ExpectedErrs), len(errs))
- continue
- }
- for i := range errs {
- if errs[i].Error() != tc.ExpectedErrs[i] {
- t.Errorf("%s: error #%d: expected %q, got %q", k, i, tc.ExpectedErrs[i], errs[i].Error())
- }
- }
- if !reflect.DeepEqual(tc.New, tc.ExpectedNew) {
- t.Errorf("%s: Expected after validation:\n%#v\ngot\n%#v", k, tc.ExpectedNew, tc.New)
- }
- }
- }
- func TestObjectMetaGenerationUpdate(t *testing.T) {
- testcases := map[string]struct {
- Old api.ObjectMeta
- New api.ObjectMeta
- ExpectedErrs []string
- }{
- "invalid generation change - decremented": {
- Old: api.ObjectMeta{Name: "test", ResourceVersion: "1", Generation: 5},
- New: api.ObjectMeta{Name: "test", ResourceVersion: "1", Generation: 4},
- ExpectedErrs: []string{"field.generation: Invalid value: 4: must not be decremented"},
- },
- "valid generation change - incremented by one": {
- Old: api.ObjectMeta{Name: "test", ResourceVersion: "1", Generation: 1},
- New: api.ObjectMeta{Name: "test", ResourceVersion: "1", Generation: 2},
- ExpectedErrs: []string{},
- },
- "valid generation field - not updated": {
- Old: api.ObjectMeta{Name: "test", ResourceVersion: "1", Generation: 5},
- New: api.ObjectMeta{Name: "test", ResourceVersion: "1", Generation: 5},
- ExpectedErrs: []string{},
- },
- }
- for k, tc := range testcases {
- errList := []string{}
- errs := ValidateObjectMetaUpdate(&tc.New, &tc.Old, field.NewPath("field"))
- if len(errs) != len(tc.ExpectedErrs) {
- t.Logf("%s: Expected: %#v", k, tc.ExpectedErrs)
- for _, err := range errs {
- errList = append(errList, err.Error())
- }
- t.Logf("%s: Got: %#v", k, errList)
- t.Errorf("%s: expected %d errors, got %d", k, len(tc.ExpectedErrs), len(errs))
- continue
- }
- for i := range errList {
- if errList[i] != tc.ExpectedErrs[i] {
- t.Errorf("%s: error #%d: expected %q, got %q", k, i, tc.ExpectedErrs[i], errList[i])
- }
- }
- }
- }
- // Ensure trailing slash is allowed in generate name
- func TestValidateObjectMetaTrimsTrailingSlash(t *testing.T) {
- errs := ValidateObjectMeta(
- &api.ObjectMeta{Name: "test", GenerateName: "foo-"},
- false,
- NameIsDNSSubdomain,
- field.NewPath("field"))
- if len(errs) != 0 {
- t.Fatalf("unexpected errors: %v", errs)
- }
- }
- func TestValidateAnnotations(t *testing.T) {
- successCases := []map[string]string{
- {"simple": "bar"},
- {"now-with-dashes": "bar"},
- {"1-starts-with-num": "bar"},
- {"1234": "bar"},
- {"simple/simple": "bar"},
- {"now-with-dashes/simple": "bar"},
- {"now-with-dashes/now-with-dashes": "bar"},
- {"now.with.dots/simple": "bar"},
- {"now-with.dashes-and.dots/simple": "bar"},
- {"1-num.2-num/3-num": "bar"},
- {"1234/5678": "bar"},
- {"1.2.3.4/5678": "bar"},
- {"UpperCase123": "bar"},
- {"a": strings.Repeat("b", totalAnnotationSizeLimitB-1)},
- {
- "a": strings.Repeat("b", totalAnnotationSizeLimitB/2-1),
- "c": strings.Repeat("d", totalAnnotationSizeLimitB/2-1),
- },
- }
- for i := range successCases {
- errs := ValidateAnnotations(successCases[i], field.NewPath("field"))
- if len(errs) != 0 {
- t.Errorf("case[%d] expected success, got %#v", i, errs)
- }
- }
- nameErrorCases := []struct {
- annotations map[string]string
- expect string
- }{
- {map[string]string{"nospecialchars^=@": "bar"}, "must match the regex"},
- {map[string]string{"cantendwithadash-": "bar"}, "must match the regex"},
- {map[string]string{"only/one/slash": "bar"}, "must match the regex"},
- {map[string]string{strings.Repeat("a", 254): "bar"}, "must be no more than"},
- }
- for i := range nameErrorCases {
- errs := ValidateAnnotations(nameErrorCases[i].annotations, field.NewPath("field"))
- if len(errs) != 1 {
- t.Errorf("case[%d]: expected failure", i)
- } else {
- if !strings.Contains(errs[0].Detail, nameErrorCases[i].expect) {
- t.Errorf("case[%d]: error details do not include %q: %q", i, nameErrorCases[i].expect, errs[0].Detail)
- }
- }
- }
- totalSizeErrorCases := []map[string]string{
- {"a": strings.Repeat("b", totalAnnotationSizeLimitB)},
- {
- "a": strings.Repeat("b", totalAnnotationSizeLimitB/2),
- "c": strings.Repeat("d", totalAnnotationSizeLimitB/2),
- },
- }
- for i := range totalSizeErrorCases {
- errs := ValidateAnnotations(totalSizeErrorCases[i], field.NewPath("field"))
- if len(errs) != 1 {
- t.Errorf("case[%d] expected failure", i)
- }
- }
- }
- func testVolume(name string, namespace string, spec api.PersistentVolumeSpec) *api.PersistentVolume {
- objMeta := api.ObjectMeta{Name: name}
- if namespace != "" {
- objMeta.Namespace = namespace
- }
- return &api.PersistentVolume{
- ObjectMeta: objMeta,
- Spec: spec,
- }
- }
- func TestValidatePersistentVolumes(t *testing.T) {
- scenarios := map[string]struct {
- isExpectedFailure bool
- volume *api.PersistentVolume
- }{
- "good-volume": {
- isExpectedFailure: false,
- volume: testVolume("foo", "", api.PersistentVolumeSpec{
- Capacity: api.ResourceList{
- api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
- },
- AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce},
- PersistentVolumeSource: api.PersistentVolumeSource{
- HostPath: &api.HostPathVolumeSource{Path: "/foo"},
- },
- }),
- },
- "invalid-accessmode": {
- isExpectedFailure: true,
- volume: testVolume("foo", "", api.PersistentVolumeSpec{
- Capacity: api.ResourceList{
- api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
- },
- AccessModes: []api.PersistentVolumeAccessMode{"fakemode"},
- PersistentVolumeSource: api.PersistentVolumeSource{
- HostPath: &api.HostPathVolumeSource{Path: "/foo"},
- },
- }),
- },
- "unexpected-namespace": {
- isExpectedFailure: true,
- volume: testVolume("foo", "unexpected-namespace", api.PersistentVolumeSpec{
- Capacity: api.ResourceList{
- api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
- },
- AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce},
- PersistentVolumeSource: api.PersistentVolumeSource{
- HostPath: &api.HostPathVolumeSource{Path: "/foo"},
- },
- }),
- },
- "bad-name": {
- isExpectedFailure: true,
- volume: testVolume("123*Bad(Name", "unexpected-namespace", api.PersistentVolumeSpec{
- Capacity: api.ResourceList{
- api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
- },
- AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce},
- PersistentVolumeSource: api.PersistentVolumeSource{
- HostPath: &api.HostPathVolumeSource{Path: "/foo"},
- },
- }),
- },
- "missing-name": {
- isExpectedFailure: true,
- volume: testVolume("", "", api.PersistentVolumeSpec{
- Capacity: api.ResourceList{
- api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
- },
- AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce},
- }),
- },
- "missing-capacity": {
- isExpectedFailure: true,
- volume: testVolume("foo", "", api.PersistentVolumeSpec{}),
- },
- "missing-accessmodes": {
- isExpectedFailure: true,
- volume: testVolume("goodname", "missing-accessmodes", api.PersistentVolumeSpec{
- Capacity: api.ResourceList{
- api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
- },
- PersistentVolumeSource: api.PersistentVolumeSource{
- HostPath: &api.HostPathVolumeSource{Path: "/foo"},
- },
- }),
- },
- "too-many-sources": {
- isExpectedFailure: true,
- volume: testVolume("", "", api.PersistentVolumeSpec{
- Capacity: api.ResourceList{
- api.ResourceName(api.ResourceStorage): resource.MustParse("5G"),
- },
- PersistentVolumeSource: api.PersistentVolumeSource{
- HostPath: &api.HostPathVolumeSource{Path: "/foo"},
- GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{PDName: "foo", FSType: "ext4"},
- },
- }),
- },
- "host mount of / with recycle reclaim policy": {
- isExpectedFailure: true,
- volume: testVolume("bad-recycle-do-not-want", "", api.PersistentVolumeSpec{
- Capacity: api.ResourceList{
- api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
- },
- AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce},
- PersistentVolumeSource: api.PersistentVolumeSource{
- HostPath: &api.HostPathVolumeSource{Path: "/"},
- },
- PersistentVolumeReclaimPolicy: api.PersistentVolumeReclaimRecycle,
- }),
- },
- "host mount of / with recycle reclaim policy 2": {
- isExpectedFailure: true,
- volume: testVolume("bad-recycle-do-not-want", "", api.PersistentVolumeSpec{
- Capacity: api.ResourceList{
- api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
- },
- AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce},
- PersistentVolumeSource: api.PersistentVolumeSource{
- HostPath: &api.HostPathVolumeSource{Path: "/a/.."},
- },
- PersistentVolumeReclaimPolicy: api.PersistentVolumeReclaimRecycle,
- }),
- },
- }
- for name, scenario := range scenarios {
- errs := ValidatePersistentVolume(scenario.volume)
- if len(errs) == 0 && scenario.isExpectedFailure {
- t.Errorf("Unexpected success for scenario: %s", name)
- }
- if len(errs) > 0 && !scenario.isExpectedFailure {
- t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs)
- }
- }
- }
- func testVolumeClaim(name string, namespace string, spec api.PersistentVolumeClaimSpec) *api.PersistentVolumeClaim {
- return &api.PersistentVolumeClaim{
- ObjectMeta: api.ObjectMeta{Name: name, Namespace: namespace},
- Spec: spec,
- }
- }
- func TestValidatePersistentVolumeClaim(t *testing.T) {
- scenarios := map[string]struct {
- isExpectedFailure bool
- claim *api.PersistentVolumeClaim
- }{
- "good-claim": {
- isExpectedFailure: false,
- claim: testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{
- Selector: &unversioned.LabelSelector{
- MatchExpressions: []unversioned.LabelSelectorRequirement{
- {
- Key: "key2",
- Operator: "Exists",
- },
- },
- },
- AccessModes: []api.PersistentVolumeAccessMode{
- api.ReadWriteOnce,
- api.ReadOnlyMany,
- },
- Resources: api.ResourceRequirements{
- Requests: api.ResourceList{
- api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
- },
- },
- }),
- },
- "invalid-label-selector": {
- isExpectedFailure: true,
- claim: testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{
- Selector: &unversioned.LabelSelector{
- MatchExpressions: []unversioned.LabelSelectorRequirement{
- {
- Key: "key2",
- Operator: "InvalidOp",
- Values: []string{"value1", "value2"},
- },
- },
- },
- AccessModes: []api.PersistentVolumeAccessMode{
- api.ReadWriteOnce,
- api.ReadOnlyMany,
- },
- Resources: api.ResourceRequirements{
- Requests: api.ResourceList{
- api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
- },
- },
- }),
- },
- "invalid-accessmode": {
- isExpectedFailure: true,
- claim: testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{
- AccessModes: []api.PersistentVolumeAccessMode{"fakemode"},
- Resources: api.ResourceRequirements{
- Requests: api.ResourceList{
- api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
- },
- },
- }),
- },
- "missing-namespace": {
- isExpectedFailure: true,
- claim: testVolumeClaim("foo", "", api.PersistentVolumeClaimSpec{
- AccessModes: []api.PersistentVolumeAccessMode{
- api.ReadWriteOnce,
- api.ReadOnlyMany,
- },
- Resources: api.ResourceRequirements{
- Requests: api.ResourceList{
- api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
- },
- },
- }),
- },
- "no-access-modes": {
- isExpectedFailure: true,
- claim: testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{
- Resources: api.ResourceRequirements{
- Requests: api.ResourceList{
- api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
- },
- },
- }),
- },
- "no-resource-requests": {
- isExpectedFailure: true,
- claim: testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{
- AccessModes: []api.PersistentVolumeAccessMode{
- api.ReadWriteOnce,
- },
- }),
- },
- "invalid-resource-requests": {
- isExpectedFailure: true,
- claim: testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{
- AccessModes: []api.PersistentVolumeAccessMode{
- api.ReadWriteOnce,
- },
- Resources: api.ResourceRequirements{
- Requests: api.ResourceList{
- api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
- },
- },
- }),
- },
- "negative-storage-request": {
- isExpectedFailure: true,
- claim: testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{
- Selector: &unversioned.LabelSelector{
- MatchExpressions: []unversioned.LabelSelectorRequirement{
- {
- Key: "key2",
- Operator: "Exists",
- },
- },
- },
- AccessModes: []api.PersistentVolumeAccessMode{
- api.ReadWriteOnce,
- api.ReadOnlyMany,
- },
- Resources: api.ResourceRequirements{
- Requests: api.ResourceList{
- api.ResourceName(api.ResourceStorage): resource.MustParse("-10G"),
- },
- },
- }),
- },
- }
- for name, scenario := range scenarios {
- errs := ValidatePersistentVolumeClaim(scenario.claim)
- if len(errs) == 0 && scenario.isExpectedFailure {
- t.Errorf("Unexpected success for scenario: %s", name)
- }
- if len(errs) > 0 && !scenario.isExpectedFailure {
- t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs)
- }
- }
- }
- func TestValidatePersistentVolumeClaimUpdate(t *testing.T) {
- validClaim := testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{
- AccessModes: []api.PersistentVolumeAccessMode{
- api.ReadWriteOnce,
- api.ReadOnlyMany,
- },
- Resources: api.ResourceRequirements{
- Requests: api.ResourceList{
- api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
- },
- },
- })
- validUpdateClaim := testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{
- AccessModes: []api.PersistentVolumeAccessMode{
- api.ReadWriteOnce,
- api.ReadOnlyMany,
- },
- Resources: api.ResourceRequirements{
- Requests: api.ResourceList{
- api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
- },
- },
- VolumeName: "volume",
- })
- invalidUpdateClaimResources := testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{
- AccessModes: []api.PersistentVolumeAccessMode{
- api.ReadWriteOnce,
- api.ReadOnlyMany,
- },
- Resources: api.ResourceRequirements{
- Requests: api.ResourceList{
- api.ResourceName(api.ResourceStorage): resource.MustParse("20G"),
- },
- },
- VolumeName: "volume",
- })
- invalidUpdateClaimAccessModes := testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{
- AccessModes: []api.PersistentVolumeAccessMode{
- api.ReadWriteOnce,
- },
- Resources: api.ResourceRequirements{
- Requests: api.ResourceList{
- api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
- },
- },
- VolumeName: "volume",
- })
- scenarios := map[string]struct {
- isExpectedFailure bool
- oldClaim *api.PersistentVolumeClaim
- newClaim *api.PersistentVolumeClaim
- }{
- "valid-update-volumeName-only": {
- isExpectedFailure: false,
- oldClaim: validClaim,
- newClaim: validUpdateClaim,
- },
- "valid-no-op-update": {
- isExpectedFailure: false,
- oldClaim: validUpdateClaim,
- newClaim: validUpdateClaim,
- },
- "invalid-update-change-resources-on-bound-claim": {
- isExpectedFailure: true,
- oldClaim: validUpdateClaim,
- newClaim: invalidUpdateClaimResources,
- },
- "invalid-update-change-access-modes-on-bound-claim": {
- isExpectedFailure: true,
- oldClaim: validUpdateClaim,
- newClaim: invalidUpdateClaimAccessModes,
- },
- }
- for name, scenario := range scenarios {
- // ensure we have a resource version specified for updates
- scenario.oldClaim.ResourceVersion = "1"
- scenario.newClaim.ResourceVersion = "1"
- errs := ValidatePersistentVolumeClaimUpdate(scenario.newClaim, scenario.oldClaim)
- if len(errs) == 0 && scenario.isExpectedFailure {
- t.Errorf("Unexpected success for scenario: %s", name)
- }
- if len(errs) > 0 && !scenario.isExpectedFailure {
- t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs)
- }
- }
- }
- func TestValidateKeyToPath(t *testing.T) {
- testCases := []struct {
- kp api.KeyToPath
- ok bool
- errtype field.ErrorType
- }{
- {
- kp: api.KeyToPath{Key: "k", Path: "p"},
- ok: true,
- },
- {
- kp: api.KeyToPath{Key: "k", Path: "p/p/p/p"},
- ok: true,
- },
- {
- kp: api.KeyToPath{Key: "k", Path: "p/..p/p../p..p"},
- ok: true,
- },
- {
- kp: api.KeyToPath{Key: "k", Path: "p", Mode: newInt32(0644)},
- ok: true,
- },
- {
- kp: api.KeyToPath{Key: "", Path: "p"},
- ok: false,
- errtype: field.ErrorTypeRequired,
- },
- {
- kp: api.KeyToPath{Key: "k", Path: ""},
- ok: false,
- errtype: field.ErrorTypeRequired,
- },
- {
- kp: api.KeyToPath{Key: "k", Path: "..p"},
- ok: false,
- errtype: field.ErrorTypeInvalid,
- },
- {
- kp: api.KeyToPath{Key: "k", Path: "../p"},
- ok: false,
- errtype: field.ErrorTypeInvalid,
- },
- {
- kp: api.KeyToPath{Key: "k", Path: "p/../p"},
- ok: false,
- errtype: field.ErrorTypeInvalid,
- },
- {
- kp: api.KeyToPath{Key: "k", Path: "p/.."},
- ok: false,
- errtype: field.ErrorTypeInvalid,
- },
- {
- kp: api.KeyToPath{Key: "k", Path: "p", Mode: newInt32(01000)},
- ok: false,
- errtype: field.ErrorTypeInvalid,
- },
- {
- kp: api.KeyToPath{Key: "k", Path: "p", Mode: newInt32(-1)},
- ok: false,
- errtype: field.ErrorTypeInvalid,
- },
- }
- for i, tc := range testCases {
- errs := validateKeyToPath(&tc.kp, field.NewPath("field"))
- if tc.ok && len(errs) > 0 {
- t.Errorf("[%d] unexpected errors: %v", i, errs)
- } else if !tc.ok && len(errs) == 0 {
- t.Errorf("[%d] expected error type %v", i, tc.errtype)
- } else if len(errs) > 1 {
- t.Errorf("[%d] expected only one error, got %d", i, len(errs))
- } else if !tc.ok {
- if errs[0].Type != tc.errtype {
- t.Errorf("[%d] expected error type %v, got %v", i, tc.errtype, errs[0].Type)
- }
- }
- }
- }
- // helper
- func newInt32(val int) *int32 {
- p := new(int32)
- *p = int32(val)
- return p
- }
- // This test is a little too top-to-bottom. Ideally we would test each volume
- // type on its own, but we want to also make sure that the logic works through
- // the one-of wrapper, so we just do it all in one place.
- func TestValidateVolumes(t *testing.T) {
- testCases := []struct {
- name string
- vol api.Volume
- errtype field.ErrorType
- errfield string
- errdetail string
- }{
- // EmptyDir and basic volume names
- {
- name: "valid alpha name",
- vol: api.Volume{
- Name: "empty",
- VolumeSource: api.VolumeSource{
- EmptyDir: &api.EmptyDirVolumeSource{},
- },
- },
- },
- {
- name: "valid num name",
- vol: api.Volume{
- Name: "123",
- VolumeSource: api.VolumeSource{
- EmptyDir: &api.EmptyDirVolumeSource{},
- },
- },
- },
- {
- name: "valid alphanum name",
- vol: api.Volume{
- Name: "empty-123",
- VolumeSource: api.VolumeSource{
- EmptyDir: &api.EmptyDirVolumeSource{},
- },
- },
- },
- {
- name: "valid numalpha name",
- vol: api.Volume{
- Name: "123-empty",
- VolumeSource: api.VolumeSource{
- EmptyDir: &api.EmptyDirVolumeSource{},
- },
- },
- },
- {
- name: "zero-length name",
- vol: api.Volume{
- Name: "",
- VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}},
- },
- errtype: field.ErrorTypeRequired,
- errfield: "name",
- },
- {
- name: "name > 63 characters",
- vol: api.Volume{
- Name: strings.Repeat("a", 64),
- VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}},
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "name",
- errdetail: "must be no more than",
- },
- {
- name: "name not a DNS label",
- vol: api.Volume{
- Name: "a.b.c",
- VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}},
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "name",
- errdetail: "must match the regex",
- },
- // More than one source field specified.
- {
- name: "more than one source",
- vol: api.Volume{
- Name: "dups",
- VolumeSource: api.VolumeSource{
- EmptyDir: &api.EmptyDirVolumeSource{},
- HostPath: &api.HostPathVolumeSource{
- Path: "/mnt/path",
- },
- },
- },
- errtype: field.ErrorTypeForbidden,
- errfield: "hostPath",
- errdetail: "may not specify more than 1 volume",
- },
- // HostPath
- {
- name: "valid HostPath",
- vol: api.Volume{
- Name: "hostpath",
- VolumeSource: api.VolumeSource{
- HostPath: &api.HostPathVolumeSource{
- Path: "/mnt/path",
- },
- },
- },
- },
- // GcePersistentDisk
- {
- name: "valid GcePersistentDisk",
- vol: api.Volume{
- Name: "gce-pd",
- VolumeSource: api.VolumeSource{
- GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{
- PDName: "my-PD",
- FSType: "ext4",
- Partition: 1,
- ReadOnly: false,
- },
- },
- },
- },
- // AWSElasticBlockStore
- {
- name: "valid AWSElasticBlockStore",
- vol: api.Volume{
- Name: "aws-ebs",
- VolumeSource: api.VolumeSource{
- AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
- VolumeID: "my-PD",
- FSType: "ext4",
- Partition: 1,
- ReadOnly: false,
- },
- },
- },
- },
- // GitRepo
- {
- name: "valid GitRepo",
- vol: api.Volume{
- Name: "git-repo",
- VolumeSource: api.VolumeSource{
- GitRepo: &api.GitRepoVolumeSource{
- Repository: "my-repo",
- Revision: "hashstring",
- Directory: "target",
- },
- },
- },
- },
- {
- name: "valid GitRepo in .",
- vol: api.Volume{
- Name: "git-repo-dot",
- VolumeSource: api.VolumeSource{
- GitRepo: &api.GitRepoVolumeSource{
- Repository: "my-repo",
- Directory: ".",
- },
- },
- },
- },
- {
- name: "valid GitRepo with .. in name",
- vol: api.Volume{
- Name: "git-repo-dot-dot-foo",
- VolumeSource: api.VolumeSource{
- GitRepo: &api.GitRepoVolumeSource{
- Repository: "my-repo",
- Directory: "..foo",
- },
- },
- },
- },
- {
- name: "GitRepo starts with ../",
- vol: api.Volume{
- Name: "gitrepo",
- VolumeSource: api.VolumeSource{
- GitRepo: &api.GitRepoVolumeSource{
- Repository: "foo",
- Directory: "../dots/bar",
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "gitRepo.directory",
- errdetail: `must not contain '..'`,
- },
- {
- name: "GitRepo contains ..",
- vol: api.Volume{
- Name: "gitrepo",
- VolumeSource: api.VolumeSource{
- GitRepo: &api.GitRepoVolumeSource{
- Repository: "foo",
- Directory: "dots/../bar",
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "gitRepo.directory",
- errdetail: `must not contain '..'`,
- },
- {
- name: "GitRepo absolute target",
- vol: api.Volume{
- Name: "gitrepo",
- VolumeSource: api.VolumeSource{
- GitRepo: &api.GitRepoVolumeSource{
- Repository: "foo",
- Directory: "/abstarget",
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "gitRepo.directory",
- },
- // ISCSI
- {
- name: "valid ISCSI",
- vol: api.Volume{
- Name: "iscsi",
- VolumeSource: api.VolumeSource{
- ISCSI: &api.ISCSIVolumeSource{
- TargetPortal: "127.0.0.1",
- IQN: "iqn.2015-02.example.com:test",
- Lun: 1,
- FSType: "ext4",
- ReadOnly: false,
- },
- },
- },
- },
- {
- name: "empty portal",
- vol: api.Volume{
- Name: "iscsi",
- VolumeSource: api.VolumeSource{
- ISCSI: &api.ISCSIVolumeSource{
- TargetPortal: "",
- IQN: "iqn.2015-02.example.com:test",
- Lun: 1,
- FSType: "ext4",
- ReadOnly: false,
- },
- },
- },
- errtype: field.ErrorTypeRequired,
- errfield: "iscsi.targetPortal",
- },
- {
- name: "empty iqn",
- vol: api.Volume{
- Name: "iscsi",
- VolumeSource: api.VolumeSource{
- ISCSI: &api.ISCSIVolumeSource{
- TargetPortal: "127.0.0.1",
- IQN: "",
- Lun: 1,
- FSType: "ext4",
- ReadOnly: false,
- },
- },
- },
- errtype: field.ErrorTypeRequired,
- errfield: "iscsi.iqn",
- },
- // Secret
- {
- name: "valid Secret",
- vol: api.Volume{
- Name: "secret",
- VolumeSource: api.VolumeSource{
- Secret: &api.SecretVolumeSource{
- SecretName: "my-secret",
- },
- },
- },
- },
- {
- name: "valid Secret with defaultMode",
- vol: api.Volume{
- Name: "secret",
- VolumeSource: api.VolumeSource{
- Secret: &api.SecretVolumeSource{
- SecretName: "my-secret",
- DefaultMode: newInt32(0644),
- },
- },
- },
- },
- {
- name: "valid Secret with projection and mode",
- vol: api.Volume{
- Name: "secret",
- VolumeSource: api.VolumeSource{
- Secret: &api.SecretVolumeSource{
- SecretName: "my-secret",
- Items: []api.KeyToPath{{
- Key: "key",
- Path: "filename",
- Mode: newInt32(0644),
- }},
- },
- },
- },
- },
- {
- name: "valid Secret with subdir projection",
- vol: api.Volume{
- Name: "secret",
- VolumeSource: api.VolumeSource{
- Secret: &api.SecretVolumeSource{
- SecretName: "my-secret",
- Items: []api.KeyToPath{{
- Key: "key",
- Path: "dir/filename",
- }},
- },
- },
- },
- },
- {
- name: "secret with missing path",
- vol: api.Volume{
- Name: "secret",
- VolumeSource: api.VolumeSource{
- Secret: &api.SecretVolumeSource{
- SecretName: "s",
- Items: []api.KeyToPath{{Key: "key", Path: ""}},
- },
- },
- },
- errtype: field.ErrorTypeRequired,
- errfield: "secret.items[0].path",
- },
- {
- name: "secret with leading ..",
- vol: api.Volume{
- Name: "secret",
- VolumeSource: api.VolumeSource{
- Secret: &api.SecretVolumeSource{
- SecretName: "s",
- Items: []api.KeyToPath{{Key: "key", Path: "../foo"}},
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "secret.items[0].path",
- },
- {
- name: "secret with .. inside",
- vol: api.Volume{
- Name: "secret",
- VolumeSource: api.VolumeSource{
- Secret: &api.SecretVolumeSource{
- SecretName: "s",
- Items: []api.KeyToPath{{Key: "key", Path: "foo/../bar"}},
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "secret.items[0].path",
- },
- {
- name: "secret with invalid positive defaultMode",
- vol: api.Volume{
- Name: "secret",
- VolumeSource: api.VolumeSource{
- Secret: &api.SecretVolumeSource{
- SecretName: "s",
- DefaultMode: newInt32(01000),
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "secret.defaultMode",
- },
- {
- name: "secret with invalid negative defaultMode",
- vol: api.Volume{
- Name: "secret",
- VolumeSource: api.VolumeSource{
- Secret: &api.SecretVolumeSource{
- SecretName: "s",
- DefaultMode: newInt32(-1),
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "secret.defaultMode",
- },
- // ConfigMap
- {
- name: "valid ConfigMap",
- vol: api.Volume{
- Name: "cfgmap",
- VolumeSource: api.VolumeSource{
- ConfigMap: &api.ConfigMapVolumeSource{
- LocalObjectReference: api.LocalObjectReference{
- Name: "my-cfgmap",
- },
- },
- },
- },
- },
- {
- name: "valid ConfigMap with defaultMode",
- vol: api.Volume{
- Name: "cfgmap",
- VolumeSource: api.VolumeSource{
- ConfigMap: &api.ConfigMapVolumeSource{
- LocalObjectReference: api.LocalObjectReference{
- Name: "my-cfgmap",
- },
- DefaultMode: newInt32(0644),
- },
- },
- },
- },
- {
- name: "valid ConfigMap with projection and mode",
- vol: api.Volume{
- Name: "cfgmap",
- VolumeSource: api.VolumeSource{
- ConfigMap: &api.ConfigMapVolumeSource{
- LocalObjectReference: api.LocalObjectReference{
- Name: "my-cfgmap"},
- Items: []api.KeyToPath{{
- Key: "key",
- Path: "filename",
- Mode: newInt32(0644),
- }},
- },
- },
- },
- },
- {
- name: "valid ConfigMap with subdir projection",
- vol: api.Volume{
- Name: "cfgmap",
- VolumeSource: api.VolumeSource{
- ConfigMap: &api.ConfigMapVolumeSource{
- LocalObjectReference: api.LocalObjectReference{
- Name: "my-cfgmap"},
- Items: []api.KeyToPath{{
- Key: "key",
- Path: "dir/filename",
- }},
- },
- },
- },
- },
- {
- name: "configmap with missing path",
- vol: api.Volume{
- Name: "cfgmap",
- VolumeSource: api.VolumeSource{
- ConfigMap: &api.ConfigMapVolumeSource{
- LocalObjectReference: api.LocalObjectReference{Name: "c"},
- Items: []api.KeyToPath{{Key: "key", Path: ""}},
- },
- },
- },
- errtype: field.ErrorTypeRequired,
- errfield: "configMap.items[0].path",
- },
- {
- name: "configmap with leading ..",
- vol: api.Volume{
- Name: "cfgmap",
- VolumeSource: api.VolumeSource{
- ConfigMap: &api.ConfigMapVolumeSource{
- LocalObjectReference: api.LocalObjectReference{Name: "c"},
- Items: []api.KeyToPath{{Key: "key", Path: "../foo"}},
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "configMap.items[0].path",
- },
- {
- name: "configmap with .. inside",
- vol: api.Volume{
- Name: "cfgmap",
- VolumeSource: api.VolumeSource{
- ConfigMap: &api.ConfigMapVolumeSource{
- LocalObjectReference: api.LocalObjectReference{Name: "c"},
- Items: []api.KeyToPath{{Key: "key", Path: "foo/../bar"}},
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "configMap.items[0].path",
- },
- {
- name: "configmap with invalid positive defaultMode",
- vol: api.Volume{
- Name: "cfgmap",
- VolumeSource: api.VolumeSource{
- ConfigMap: &api.ConfigMapVolumeSource{
- LocalObjectReference: api.LocalObjectReference{Name: "c"},
- DefaultMode: newInt32(01000),
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "configMap.defaultMode",
- },
- {
- name: "configmap with invalid negative defaultMode",
- vol: api.Volume{
- Name: "cfgmap",
- VolumeSource: api.VolumeSource{
- ConfigMap: &api.ConfigMapVolumeSource{
- LocalObjectReference: api.LocalObjectReference{Name: "c"},
- DefaultMode: newInt32(-1),
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "configMap.defaultMode",
- },
- // Glusterfs
- {
- name: "valid Glusterfs",
- vol: api.Volume{
- Name: "glusterfs",
- VolumeSource: api.VolumeSource{
- Glusterfs: &api.GlusterfsVolumeSource{
- EndpointsName: "host1",
- Path: "path",
- ReadOnly: false,
- },
- },
- },
- },
- {
- name: "empty hosts",
- vol: api.Volume{
- Name: "glusterfs",
- VolumeSource: api.VolumeSource{
- Glusterfs: &api.GlusterfsVolumeSource{
- EndpointsName: "",
- Path: "path",
- ReadOnly: false,
- },
- },
- },
- errtype: field.ErrorTypeRequired,
- errfield: "glusterfs.endpoints",
- },
- {
- name: "empty path",
- vol: api.Volume{
- Name: "glusterfs",
- VolumeSource: api.VolumeSource{
- Glusterfs: &api.GlusterfsVolumeSource{
- EndpointsName: "host",
- Path: "",
- ReadOnly: false,
- },
- },
- },
- errtype: field.ErrorTypeRequired,
- errfield: "glusterfs.path",
- },
- // Flocker
- {
- name: "valid Flocker",
- vol: api.Volume{
- Name: "flocker",
- VolumeSource: api.VolumeSource{
- Flocker: &api.FlockerVolumeSource{
- DatasetName: "datasetName",
- },
- },
- },
- },
- {
- name: "empty flocker datasetName",
- vol: api.Volume{
- Name: "flocker",
- VolumeSource: api.VolumeSource{
- Flocker: &api.FlockerVolumeSource{
- DatasetName: "",
- },
- },
- },
- errtype: field.ErrorTypeRequired,
- errfield: "flocker.datasetName",
- },
- {
- name: "slash in flocker datasetName",
- vol: api.Volume{
- Name: "flocker",
- VolumeSource: api.VolumeSource{
- Flocker: &api.FlockerVolumeSource{
- DatasetName: "foo/bar",
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "flocker.datasetName",
- errdetail: "must not contain '/'",
- },
- // RBD
- {
- name: "valid RBD",
- vol: api.Volume{
- Name: "rbd",
- VolumeSource: api.VolumeSource{
- RBD: &api.RBDVolumeSource{
- CephMonitors: []string{"foo"},
- RBDImage: "bar",
- FSType: "ext4",
- },
- },
- },
- },
- {
- name: "empty rbd monitors",
- vol: api.Volume{
- Name: "rbd",
- VolumeSource: api.VolumeSource{
- RBD: &api.RBDVolumeSource{
- CephMonitors: []string{},
- RBDImage: "bar",
- FSType: "ext4",
- },
- },
- },
- errtype: field.ErrorTypeRequired,
- errfield: "rbd.monitors",
- },
- {
- name: "empty image",
- vol: api.Volume{
- Name: "rbd",
- VolumeSource: api.VolumeSource{
- RBD: &api.RBDVolumeSource{
- CephMonitors: []string{"foo"},
- RBDImage: "",
- FSType: "ext4",
- },
- },
- },
- errtype: field.ErrorTypeRequired,
- errfield: "rbd.image",
- },
- // Cinder
- {
- name: "valid Cinder",
- vol: api.Volume{
- Name: "cinder",
- VolumeSource: api.VolumeSource{
- Cinder: &api.CinderVolumeSource{
- VolumeID: "29ea5088-4f60-4757-962e-dba678767887",
- FSType: "ext4",
- ReadOnly: false,
- },
- },
- },
- },
- // CephFS
- {
- name: "valid CephFS",
- vol: api.Volume{
- Name: "cephfs",
- VolumeSource: api.VolumeSource{
- CephFS: &api.CephFSVolumeSource{
- Monitors: []string{"foo"},
- },
- },
- },
- },
- {
- name: "empty cephfs monitors",
- vol: api.Volume{
- Name: "cephfs",
- VolumeSource: api.VolumeSource{
- CephFS: &api.CephFSVolumeSource{
- Monitors: []string{},
- },
- },
- },
- errtype: field.ErrorTypeRequired,
- errfield: "cephfs.monitors",
- },
- // DownwardAPI
- {
- name: "valid DownwardAPI",
- vol: api.Volume{
- Name: "downwardapi",
- VolumeSource: api.VolumeSource{
- DownwardAPI: &api.DownwardAPIVolumeSource{
- Items: []api.DownwardAPIVolumeFile{
- {
- Path: "labels",
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: "v1",
- FieldPath: "metadata.labels",
- },
- },
- {
- Path: "annotations",
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: "v1",
- FieldPath: "metadata.annotations",
- },
- },
- {
- Path: "namespace",
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: "v1",
- FieldPath: "metadata.namespace",
- },
- },
- {
- Path: "name",
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: "v1",
- FieldPath: "metadata.name",
- },
- },
- {
- Path: "path/with/subdirs",
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: "v1",
- FieldPath: "metadata.labels",
- },
- },
- {
- Path: "path/./withdot",
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: "v1",
- FieldPath: "metadata.labels",
- },
- },
- {
- Path: "path/with/embedded..dotdot",
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: "v1",
- FieldPath: "metadata.labels",
- },
- },
- {
- Path: "path/with/leading/..dotdot",
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: "v1",
- FieldPath: "metadata.labels",
- },
- },
- {
- Path: "cpu_limit",
- ResourceFieldRef: &api.ResourceFieldSelector{
- ContainerName: "test-container",
- Resource: "limits.cpu",
- },
- },
- {
- Path: "cpu_request",
- ResourceFieldRef: &api.ResourceFieldSelector{
- ContainerName: "test-container",
- Resource: "requests.cpu",
- },
- },
- {
- Path: "memory_limit",
- ResourceFieldRef: &api.ResourceFieldSelector{
- ContainerName: "test-container",
- Resource: "limits.memory",
- },
- },
- {
- Path: "memory_request",
- ResourceFieldRef: &api.ResourceFieldSelector{
- ContainerName: "test-container",
- Resource: "requests.memory",
- },
- },
- },
- },
- },
- },
- },
- {
- name: "downapi valid defaultMode",
- vol: api.Volume{
- Name: "downapi",
- VolumeSource: api.VolumeSource{
- DownwardAPI: &api.DownwardAPIVolumeSource{
- DefaultMode: newInt32(0644),
- },
- },
- },
- },
- {
- name: "downapi valid item mode",
- vol: api.Volume{
- Name: "downapi",
- VolumeSource: api.VolumeSource{
- DownwardAPI: &api.DownwardAPIVolumeSource{
- Items: []api.DownwardAPIVolumeFile{{
- Mode: newInt32(0644),
- Path: "path",
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: "v1",
- FieldPath: "metadata.labels",
- },
- }},
- },
- },
- },
- },
- {
- name: "downapi invalid positive item mode",
- vol: api.Volume{
- Name: "downapi",
- VolumeSource: api.VolumeSource{
- DownwardAPI: &api.DownwardAPIVolumeSource{
- Items: []api.DownwardAPIVolumeFile{{
- Mode: newInt32(01000),
- Path: "path",
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: "v1",
- FieldPath: "metadata.labels",
- },
- }},
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "downwardAPI.mode",
- },
- {
- name: "downapi invalid negative item mode",
- vol: api.Volume{
- Name: "downapi",
- VolumeSource: api.VolumeSource{
- DownwardAPI: &api.DownwardAPIVolumeSource{
- Items: []api.DownwardAPIVolumeFile{{
- Mode: newInt32(-1),
- Path: "path",
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: "v1",
- FieldPath: "metadata.labels",
- },
- }},
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "downwardAPI.mode",
- },
- {
- name: "downapi empty metatada path",
- vol: api.Volume{
- Name: "downapi",
- VolumeSource: api.VolumeSource{
- DownwardAPI: &api.DownwardAPIVolumeSource{
- Items: []api.DownwardAPIVolumeFile{{
- Path: "",
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: "v1",
- FieldPath: "metadata.labels",
- },
- }},
- },
- },
- },
- errtype: field.ErrorTypeRequired,
- errfield: "downwardAPI.path",
- },
- {
- name: "downapi absolute path",
- vol: api.Volume{
- Name: "downapi",
- VolumeSource: api.VolumeSource{
- DownwardAPI: &api.DownwardAPIVolumeSource{
- Items: []api.DownwardAPIVolumeFile{{
- Path: "/absolutepath",
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: "v1",
- FieldPath: "metadata.labels",
- },
- }},
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "downwardAPI.path",
- },
- {
- name: "downapi dot dot path",
- vol: api.Volume{
- Name: "downapi",
- VolumeSource: api.VolumeSource{
- DownwardAPI: &api.DownwardAPIVolumeSource{
- Items: []api.DownwardAPIVolumeFile{{
- Path: "../../passwd",
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: "v1",
- FieldPath: "metadata.labels",
- },
- }},
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "downwardAPI.path",
- errdetail: `must not contain '..'`,
- },
- {
- name: "downapi dot dot file name",
- vol: api.Volume{
- Name: "downapi",
- VolumeSource: api.VolumeSource{
- DownwardAPI: &api.DownwardAPIVolumeSource{
- Items: []api.DownwardAPIVolumeFile{{
- Path: "..badFileName",
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: "v1",
- FieldPath: "metadata.labels",
- },
- }},
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "downwardAPI.path",
- errdetail: `must not start with '..'`,
- },
- {
- name: "downapi dot dot first level dirent",
- vol: api.Volume{
- Name: "downapi",
- VolumeSource: api.VolumeSource{
- DownwardAPI: &api.DownwardAPIVolumeSource{
- Items: []api.DownwardAPIVolumeFile{{
- Path: "..badDirName/goodFileName",
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: "v1",
- FieldPath: "metadata.labels",
- },
- }},
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "downwardAPI.path",
- errdetail: `must not start with '..'`,
- },
- {
- name: "downapi fieldRef and ResourceFieldRef together",
- vol: api.Volume{
- Name: "downapi",
- VolumeSource: api.VolumeSource{
- DownwardAPI: &api.DownwardAPIVolumeSource{
- Items: []api.DownwardAPIVolumeFile{{
- Path: "test",
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: "v1",
- FieldPath: "metadata.labels",
- },
- ResourceFieldRef: &api.ResourceFieldSelector{
- ContainerName: "test-container",
- Resource: "requests.memory",
- },
- }},
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "downwardAPI",
- errdetail: "fieldRef and resourceFieldRef can not be specified simultaneously",
- },
- {
- name: "downapi invalid positive defaultMode",
- vol: api.Volume{
- Name: "downapi",
- VolumeSource: api.VolumeSource{
- DownwardAPI: &api.DownwardAPIVolumeSource{
- DefaultMode: newInt32(01000),
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "downwardAPI.defaultMode",
- },
- {
- name: "downapi invalid negative defaultMode",
- vol: api.Volume{
- Name: "downapi",
- VolumeSource: api.VolumeSource{
- DownwardAPI: &api.DownwardAPIVolumeSource{
- DefaultMode: newInt32(-1),
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "downwardAPI.defaultMode",
- },
- // FC
- {
- name: "valid FC",
- vol: api.Volume{
- Name: "fc",
- VolumeSource: api.VolumeSource{
- FC: &api.FCVolumeSource{
- TargetWWNs: []string{"some_wwn"},
- Lun: newInt32(1),
- FSType: "ext4",
- ReadOnly: false,
- },
- },
- },
- },
- {
- name: "fc empty wwn",
- vol: api.Volume{
- Name: "fc",
- VolumeSource: api.VolumeSource{
- FC: &api.FCVolumeSource{
- TargetWWNs: []string{},
- Lun: newInt32(1),
- FSType: "ext4",
- ReadOnly: false,
- },
- },
- },
- errtype: field.ErrorTypeRequired,
- errfield: "fc.targetWWNs",
- },
- {
- name: "fc empty lun",
- vol: api.Volume{
- Name: "fc",
- VolumeSource: api.VolumeSource{
- FC: &api.FCVolumeSource{
- TargetWWNs: []string{"wwn"},
- Lun: nil,
- FSType: "ext4",
- ReadOnly: false,
- },
- },
- },
- errtype: field.ErrorTypeRequired,
- errfield: "fc.lun",
- },
- // FlexVolume
- {
- name: "valid FlexVolume",
- vol: api.Volume{
- Name: "flex-volume",
- VolumeSource: api.VolumeSource{
- FlexVolume: &api.FlexVolumeSource{
- Driver: "kubernetes.io/blue",
- FSType: "ext4",
- },
- },
- },
- },
- // AzureFile
- {
- name: "valid AzureFile",
- vol: api.Volume{
- Name: "azure-file",
- VolumeSource: api.VolumeSource{
- AzureFile: &api.AzureFileVolumeSource{
- SecretName: "key",
- ShareName: "share",
- ReadOnly: false,
- },
- },
- },
- },
- {
- name: "AzureFile empty secret",
- vol: api.Volume{
- Name: "azure-file",
- VolumeSource: api.VolumeSource{
- AzureFile: &api.AzureFileVolumeSource{
- SecretName: "",
- ShareName: "share",
- ReadOnly: false,
- },
- },
- },
- errtype: field.ErrorTypeRequired,
- errfield: "azureFile.secretName",
- },
- {
- name: "AzureFile empty share",
- vol: api.Volume{
- Name: "azure-file",
- VolumeSource: api.VolumeSource{
- AzureFile: &api.AzureFileVolumeSource{
- SecretName: "name",
- ShareName: "",
- ReadOnly: false,
- },
- },
- },
- errtype: field.ErrorTypeRequired,
- errfield: "azureFile.shareName",
- },
- // Quobyte
- {
- name: "valid Quobyte",
- vol: api.Volume{
- Name: "quobyte",
- VolumeSource: api.VolumeSource{
- Quobyte: &api.QuobyteVolumeSource{
- Registry: "registry:7861",
- Volume: "volume",
- ReadOnly: false,
- User: "root",
- Group: "root",
- },
- },
- },
- },
- {
- name: "empty registry quobyte",
- vol: api.Volume{
- Name: "quobyte",
- VolumeSource: api.VolumeSource{
- Quobyte: &api.QuobyteVolumeSource{
- Volume: "/test",
- },
- },
- },
- errtype: field.ErrorTypeRequired,
- errfield: "quobyte.registry",
- },
- {
- name: "wrong format registry quobyte",
- vol: api.Volume{
- Name: "quobyte",
- VolumeSource: api.VolumeSource{
- Quobyte: &api.QuobyteVolumeSource{
- Registry: "registry7861",
- Volume: "/test",
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "quobyte.registry",
- },
- {
- name: "wrong format multiple registries quobyte",
- vol: api.Volume{
- Name: "quobyte",
- VolumeSource: api.VolumeSource{
- Quobyte: &api.QuobyteVolumeSource{
- Registry: "registry:7861,reg2",
- Volume: "/test",
- },
- },
- },
- errtype: field.ErrorTypeInvalid,
- errfield: "quobyte.registry",
- },
- {
- name: "empty volume quobyte",
- vol: api.Volume{
- Name: "quobyte",
- VolumeSource: api.VolumeSource{
- Quobyte: &api.QuobyteVolumeSource{
- Registry: "registry:7861",
- },
- },
- },
- errtype: field.ErrorTypeRequired,
- errfield: "quobyte.volume",
- },
- // AzureDisk
- {
- name: "valid AzureDisk",
- vol: api.Volume{
- Name: "azure-disk",
- VolumeSource: api.VolumeSource{
- AzureDisk: &api.AzureDiskVolumeSource{
- DiskName: "foo",
- DataDiskURI: "https://blob/vhds/bar.vhd",
- },
- },
- },
- },
- {
- name: "AzureDisk empty disk name",
- vol: api.Volume{
- Name: "azure-disk",
- VolumeSource: api.VolumeSource{
- AzureDisk: &api.AzureDiskVolumeSource{
- DiskName: "",
- DataDiskURI: "https://blob/vhds/bar.vhd",
- },
- },
- },
- errtype: field.ErrorTypeRequired,
- errfield: "azureDisk.diskName",
- },
- {
- name: "AzureDisk empty disk uri",
- vol: api.Volume{
- Name: "azure-disk",
- VolumeSource: api.VolumeSource{
- AzureDisk: &api.AzureDiskVolumeSource{
- DiskName: "foo",
- DataDiskURI: "",
- },
- },
- },
- errtype: field.ErrorTypeRequired,
- errfield: "azureDisk.diskURI",
- },
- }
- for i, tc := range testCases {
- names, errs := validateVolumes([]api.Volume{tc.vol}, field.NewPath("field"))
- if len(errs) > 0 && tc.errtype == "" {
- t.Errorf("[%d: %q] unexpected error(s): %v", i, tc.name, errs)
- } else if len(errs) > 1 {
- t.Errorf("[%d: %q] expected 1 error, got %d: %v", i, tc.name, len(errs), errs)
- } else if len(errs) == 0 && tc.errtype != "" {
- t.Errorf("[%d: %q] expected error type %v", i, tc.name, tc.errtype)
- } else if len(errs) == 1 {
- if errs[0].Type != tc.errtype {
- t.Errorf("[%d: %q] expected error type %v, got %v", i, tc.name, tc.errtype, errs[0].Type)
- } else if !strings.HasSuffix(errs[0].Field, "."+tc.errfield) {
- t.Errorf("[%d: %q] expected error on field %q, got %q", i, tc.name, tc.errfield, errs[0].Field)
- } else if !strings.Contains(errs[0].Detail, tc.errdetail) {
- t.Errorf("[%d: %q] expected error detail %q, got %q", i, tc.name, tc.errdetail, errs[0].Detail)
- }
- } else {
- if len(names) != 1 || !names.Has(tc.vol.Name) {
- t.Errorf("[%d: %q] wrong names result: %v", i, tc.name, names)
- }
- }
- }
- dupsCase := []api.Volume{
- {Name: "abc", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}},
- {Name: "abc", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}},
- }
- _, errs := validateVolumes(dupsCase, field.NewPath("field"))
- if len(errs) == 0 {
- t.Errorf("expected error")
- } else if len(errs) != 1 {
- t.Errorf("expected 1 error, got %d: %v", len(errs), errs)
- } else if errs[0].Type != field.ErrorTypeDuplicate {
- t.Errorf("expected error type %v, got %v", field.ErrorTypeDuplicate, errs[0].Type)
- }
- }
- func TestValidatePorts(t *testing.T) {
- successCase := []api.ContainerPort{
- {Name: "abc", ContainerPort: 80, HostPort: 80, Protocol: "TCP"},
- {Name: "easy", ContainerPort: 82, Protocol: "TCP"},
- {Name: "as", ContainerPort: 83, Protocol: "UDP"},
- {Name: "do-re-me", ContainerPort: 84, Protocol: "UDP"},
- {ContainerPort: 85, Protocol: "TCP"},
- }
- if errs := validateContainerPorts(successCase, field.NewPath("field")); len(errs) != 0 {
- t.Errorf("expected success: %v", errs)
- }
- nonCanonicalCase := []api.ContainerPort{
- {ContainerPort: 80, Protocol: "TCP"},
- }
- if errs := validateContainerPorts(nonCanonicalCase, field.NewPath("field")); len(errs) != 0 {
- t.Errorf("expected success: %v", errs)
- }
- errorCases := map[string]struct {
- P []api.ContainerPort
- T field.ErrorType
- F string
- D string
- }{
- "name > 15 characters": {
- []api.ContainerPort{{Name: strings.Repeat("a", 16), ContainerPort: 80, Protocol: "TCP"}},
- field.ErrorTypeInvalid,
- "name", "15",
- },
- "name contains invalid characters": {
- []api.ContainerPort{{Name: "a.b.c", ContainerPort: 80, Protocol: "TCP"}},
- field.ErrorTypeInvalid,
- "name", "alpha-numeric",
- },
- "name is a number": {
- []api.ContainerPort{{Name: "80", ContainerPort: 80, Protocol: "TCP"}},
- field.ErrorTypeInvalid,
- "name", "at least one letter",
- },
- "name not unique": {
- []api.ContainerPort{
- {Name: "abc", ContainerPort: 80, Protocol: "TCP"},
- {Name: "abc", ContainerPort: 81, Protocol: "TCP"},
- },
- field.ErrorTypeDuplicate,
- "[1].name", "",
- },
- "zero container port": {
- []api.ContainerPort{{ContainerPort: 0, Protocol: "TCP"}},
- field.ErrorTypeRequired,
- "containerPort", "",
- },
- "invalid container port": {
- []api.ContainerPort{{ContainerPort: 65536, Protocol: "TCP"}},
- field.ErrorTypeInvalid,
- "containerPort", "between",
- },
- "invalid host port": {
- []api.ContainerPort{{ContainerPort: 80, HostPort: 65536, Protocol: "TCP"}},
- field.ErrorTypeInvalid,
- "hostPort", "between",
- },
- "invalid protocol case": {
- []api.ContainerPort{{ContainerPort: 80, Protocol: "tcp"}},
- field.ErrorTypeNotSupported,
- "protocol", "supported values: TCP, UDP",
- },
- "invalid protocol": {
- []api.ContainerPort{{ContainerPort: 80, Protocol: "ICMP"}},
- field.ErrorTypeNotSupported,
- "protocol", "supported values: TCP, UDP",
- },
- "protocol required": {
- []api.ContainerPort{{Name: "abc", ContainerPort: 80}},
- field.ErrorTypeRequired,
- "protocol", "",
- },
- }
- for k, v := range errorCases {
- errs := validateContainerPorts(v.P, field.NewPath("field"))
- if len(errs) == 0 {
- t.Errorf("expected failure for %s", k)
- }
- for i := range errs {
- if errs[i].Type != v.T {
- t.Errorf("%s: expected error to have type %q: %q", k, v.T, errs[i].Type)
- }
- if !strings.Contains(errs[i].Field, v.F) {
- t.Errorf("%s: expected error field %q: %q", k, v.F, errs[i].Field)
- }
- if !strings.Contains(errs[i].Detail, v.D) {
- t.Errorf("%s: expected error detail %q, got %q", k, v.D, errs[i].Detail)
- }
- }
- }
- }
- func TestValidateEnv(t *testing.T) {
- successCase := []api.EnvVar{
- {Name: "abc", Value: "value"},
- {Name: "ABC", Value: "value"},
- {Name: "AbC_123", Value: "value"},
- {Name: "abc", Value: ""},
- {
- Name: "abc",
- ValueFrom: &api.EnvVarSource{
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: testapi.Default.GroupVersion().String(),
- FieldPath: "metadata.name",
- },
- },
- },
- {
- Name: "abc",
- ValueFrom: &api.EnvVarSource{
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: testapi.Default.GroupVersion().String(),
- FieldPath: "spec.nodeName",
- },
- },
- },
- {
- Name: "abc",
- ValueFrom: &api.EnvVarSource{
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: testapi.Default.GroupVersion().String(),
- FieldPath: "spec.serviceAccountName",
- },
- },
- },
- {
- Name: "secret_value",
- ValueFrom: &api.EnvVarSource{
- SecretKeyRef: &api.SecretKeySelector{
- LocalObjectReference: api.LocalObjectReference{
- Name: "some-secret",
- },
- Key: "secret-key",
- },
- },
- },
- {
- Name: "ENV_VAR_1",
- ValueFrom: &api.EnvVarSource{
- ConfigMapKeyRef: &api.ConfigMapKeySelector{
- LocalObjectReference: api.LocalObjectReference{
- Name: "some-config-map",
- },
- Key: "some-key",
- },
- },
- },
- }
- if errs := validateEnv(successCase, field.NewPath("field")); len(errs) != 0 {
- t.Errorf("expected success: %v", errs)
- }
- errorCases := []struct {
- name string
- envs []api.EnvVar
- expectedError string
- }{
- {
- name: "zero-length name",
- envs: []api.EnvVar{{Name: ""}},
- expectedError: "[0].name: Required value",
- },
- {
- name: "name not a C identifier",
- envs: []api.EnvVar{{Name: "a.b.c"}},
- expectedError: `[0].name: Invalid value: "a.b.c": must match the regex`,
- },
- {
- name: "value and valueFrom specified",
- envs: []api.EnvVar{{
- Name: "abc",
- Value: "foo",
- ValueFrom: &api.EnvVarSource{
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: testapi.Default.GroupVersion().String(),
- FieldPath: "metadata.name",
- },
- },
- }},
- expectedError: "[0].valueFrom: Invalid value: \"\": may not be specified when `value` is not empty",
- },
- {
- name: "valueFrom.fieldRef and valueFrom.secretKeyRef specified",
- envs: []api.EnvVar{{
- Name: "abc",
- ValueFrom: &api.EnvVarSource{
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: testapi.Default.GroupVersion().String(),
- FieldPath: "metadata.name",
- },
- SecretKeyRef: &api.SecretKeySelector{
- LocalObjectReference: api.LocalObjectReference{
- Name: "a-secret",
- },
- Key: "a-key",
- },
- },
- }},
- expectedError: "[0].valueFrom: Invalid value: \"\": may not have more than one field specified at a time",
- },
- {
- name: "valueFrom.fieldRef and valueFrom.configMapKeyRef set",
- envs: []api.EnvVar{{
- Name: "some_var_name",
- ValueFrom: &api.EnvVarSource{
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: testapi.Default.GroupVersion().String(),
- FieldPath: "metadata.name",
- },
- ConfigMapKeyRef: &api.ConfigMapKeySelector{
- LocalObjectReference: api.LocalObjectReference{
- Name: "some-config-map",
- },
- Key: "some-key",
- },
- },
- }},
- expectedError: `[0].valueFrom: Invalid value: "": may not have more than one field specified at a time`,
- },
- {
- name: "valueFrom.fieldRef and valueFrom.secretKeyRef specified",
- envs: []api.EnvVar{{
- Name: "abc",
- ValueFrom: &api.EnvVarSource{
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: testapi.Default.GroupVersion().String(),
- FieldPath: "metadata.name",
- },
- SecretKeyRef: &api.SecretKeySelector{
- LocalObjectReference: api.LocalObjectReference{
- Name: "a-secret",
- },
- Key: "a-key",
- },
- ConfigMapKeyRef: &api.ConfigMapKeySelector{
- LocalObjectReference: api.LocalObjectReference{
- Name: "some-config-map",
- },
- Key: "some-key",
- },
- },
- }},
- expectedError: `[0].valueFrom: Invalid value: "": may not have more than one field specified at a time`,
- },
- {
- name: "missing FieldPath on ObjectFieldSelector",
- envs: []api.EnvVar{{
- Name: "abc",
- ValueFrom: &api.EnvVarSource{
- FieldRef: &api.ObjectFieldSelector{
- APIVersion: testapi.Default.GroupVersion().String(),
- },
- },
- }},
- expectedError: `[0].valueFrom.fieldRef.fieldPath: Required value`,
- },
- {
- name: "missing APIVersion on ObjectFieldSelector",
- envs: []api.EnvVar{{
- Name: "abc",
- ValueFrom: &api.EnvVarSource{
- FieldRef: &api.ObjectFieldSelector{
- FieldPath: "metadata.name",
- },
- },
- }},
- expectedError: `[0].valueFrom.fieldRef.apiVersion: Required value`,
- },
- {
- name: "invalid fieldPath",
- envs: []api.EnvVar{{
- Name: "abc",
- ValueFrom: &api.EnvVarSource{
- FieldRef: &api.ObjectFieldSelector{
- FieldPath: "metadata.whoops",
- APIVersion: testapi.Default.GroupVersion().String(),
- },
- },
- }},
- expectedError: `[0].valueFrom.fieldRef.fieldPath: Invalid value: "metadata.whoops": error converting fieldPath`,
- },
- {
- name: "invalid fieldPath labels",
- envs: []api.EnvVar{{
- Name: "labels",
- ValueFrom: &api.EnvVarSource{
- FieldRef: &api.ObjectFieldSelector{
- FieldPath: "metadata.labels",
- APIVersion: "v1",
- },
- },
- }},
- expectedError: `[0].valueFrom.fieldRef.fieldPath: Unsupported value: "metadata.labels": supported values: metadata.name, metadata.namespace, spec.nodeName, spec.serviceAccountName, status.podIP`,
- },
- {
- name: "invalid fieldPath annotations",
- envs: []api.EnvVar{{
- Name: "abc",
- ValueFrom: &api.EnvVarSource{
- FieldRef: &api.ObjectFieldSelector{
- FieldPath: "metadata.annotations",
- APIVersion: "v1",
- },
- },
- }},
- expectedError: `[0].valueFrom.fieldRef.fieldPath: Unsupported value: "metadata.annotations": supported values: metadata.name, metadata.namespace, spec.nodeName, spec.serviceAccountName, status.podIP`,
- },
- {
- name: "unsupported fieldPath",
- envs: []api.EnvVar{{
- Name: "abc",
- ValueFrom: &api.EnvVarSource{
- FieldRef: &api.ObjectFieldSelector{
- FieldPath: "status.phase",
- APIVersion: testapi.Default.GroupVersion().String(),
- },
- },
- }},
- expectedError: `valueFrom.fieldRef.fieldPath: Unsupported value: "status.phase": supported values: metadata.name, metadata.namespace, spec.nodeName, spec.serviceAccountName, status.podIP`,
- },
- }
- for _, tc := range errorCases {
- if errs := validateEnv(tc.envs, field.NewPath("field")); len(errs) == 0 {
- t.Errorf("expected failure for %s", tc.name)
- } else {
- for i := range errs {
- str := errs[i].Error()
- if str != "" && !strings.Contains(str, tc.expectedError) {
- t.Errorf("%s: expected error detail either empty or %q, got %q", tc.name, tc.expectedError, str)
- }
- }
- }
- }
- }
- func TestValidateVolumeMounts(t *testing.T) {
- volumes := sets.NewString("abc", "123", "abc-123")
- successCase := []api.VolumeMount{
- {Name: "abc", MountPath: "/foo"},
- {Name: "123", MountPath: "/bar"},
- {Name: "abc-123", MountPath: "/baz"},
- {Name: "abc-123", MountPath: "/baa", SubPath: ""},
- {Name: "abc-123", MountPath: "/bab", SubPath: "baz"},
- {Name: "abc-123", MountPath: "/bac", SubPath: ".baz"},
- {Name: "abc-123", MountPath: "/bad", SubPath: "..baz"},
- }
- if errs := validateVolumeMounts(successCase, volumes, field.NewPath("field")); len(errs) != 0 {
- t.Errorf("expected success: %v", errs)
- }
- errorCases := map[string][]api.VolumeMount{
- "empty name": {{Name: "", MountPath: "/foo"}},
- "name not found": {{Name: "", MountPath: "/foo"}},
- "empty mountpath": {{Name: "abc", MountPath: ""}},
- "colon mountpath": {{Name: "abc", MountPath: "foo:bar"}},
- "mountpath collision": {{Name: "foo", MountPath: "/path/a"}, {Name: "bar", MountPath: "/path/a"}},
- "absolute subpath": {{Name: "abc", MountPath: "/bar", SubPath: "/baz"}},
- "subpath in ..": {{Name: "abc", MountPath: "/bar", SubPath: "../baz"}},
- "subpath contains ..": {{Name: "abc", MountPath: "/bar", SubPath: "baz/../bat"}},
- "subpath ends in ..": {{Name: "abc", MountPath: "/bar", SubPath: "./.."}},
- }
- for k, v := range errorCases {
- if errs := validateVolumeMounts(v, volumes, field.NewPath("field")); len(errs) == 0 {
- t.Errorf("expected failure for %s", k)
- }
- }
- }
- func TestValidateProbe(t *testing.T) {
- handler := api.Handler{Exec: &api.ExecAction{Command: []string{"echo"}}}
- // These fields must be positive.
- positiveFields := [...]string{"InitialDelaySeconds", "TimeoutSeconds", "PeriodSeconds", "SuccessThreshold", "FailureThreshold"}
- successCases := []*api.Probe{nil}
- for _, field := range positiveFields {
- probe := &api.Probe{Handler: handler}
- reflect.ValueOf(probe).Elem().FieldByName(field).SetInt(10)
- successCases = append(successCases, probe)
- }
- for _, p := range successCases {
- if errs := validateProbe(p, field.NewPath("field")); len(errs) != 0 {
- t.Errorf("expected success: %v", errs)
- }
- }
- errorCases := []*api.Probe{{TimeoutSeconds: 10, InitialDelaySeconds: 10}}
- for _, field := range positiveFields {
- probe := &api.Probe{Handler: handler}
- reflect.ValueOf(probe).Elem().FieldByName(field).SetInt(-10)
- errorCases = append(errorCases, probe)
- }
- for _, p := range errorCases {
- if errs := validateProbe(p, field.NewPath("field")); len(errs) == 0 {
- t.Errorf("expected failure for %v", p)
- }
- }
- }
- func TestValidateHandler(t *testing.T) {
- successCases := []api.Handler{
- {Exec: &api.ExecAction{Command: []string{"echo"}}},
- {HTTPGet: &api.HTTPGetAction{Path: "/", Port: intstr.FromInt(1), Host: "", Scheme: "HTTP"}},
- {HTTPGet: &api.HTTPGetAction{Path: "/foo", Port: intstr.FromInt(65535), Host: "host", Scheme: "HTTP"}},
- {HTTPGet: &api.HTTPGetAction{Path: "/", Port: intstr.FromString("port"), Host: "", Scheme: "HTTP"}},
- {HTTPGet: &api.HTTPGetAction{Path: "/", Port: intstr.FromString("port"), Host: "", Scheme: "HTTP", HTTPHeaders: []api.HTTPHeader{{Name: "Host", Value: "foo.example.com"}}}},
- {HTTPGet: &api.HTTPGetAction{Path: "/", Port: intstr.FromString("port"), Host: "", Scheme: "HTTP", HTTPHeaders: []api.HTTPHeader{{Name: "X-Forwarded-For", Value: "1.2.3.4"}, {Name: "X-Forwarded-For", Value: "5.6.7.8"}}}},
- }
- for _, h := range successCases {
- if errs := validateHandler(&h, field.NewPath("field")); len(errs) != 0 {
- t.Errorf("expected success: %v", errs)
- }
- }
- errorCases := []api.Handler{
- {},
- {Exec: &api.ExecAction{Command: []string{}}},
- {HTTPGet: &api.HTTPGetAction{Path: "", Port: intstr.FromInt(0), Host: ""}},
- {HTTPGet: &api.HTTPGetAction{Path: "/foo", Port: intstr.FromInt(65536), Host: "host"}},
- {HTTPGet: &api.HTTPGetAction{Path: "", Port: intstr.FromString(""), Host: ""}},
- {HTTPGet: &api.HTTPGetAction{Path: "/", Port: intstr.FromString("port"), Host: "", Scheme: "HTTP", HTTPHeaders: []api.HTTPHeader{{Name: "Host:", Value: "foo.example.com"}}}},
- {HTTPGet: &api.HTTPGetAction{Path: "/", Port: intstr.FromString("port"), Host: "", Scheme: "HTTP", HTTPHeaders: []api.HTTPHeader{{Name: "X_Forwarded_For", Value: "foo.example.com"}}}},
- }
- for _, h := range errorCases {
- if errs := validateHandler(&h, field.NewPath("field")); len(errs) == 0 {
- t.Errorf("expected failure for %#v", h)
- }
- }
- }
- func TestValidatePullPolicy(t *testing.T) {
- type T struct {
- Container api.Container
- ExpectedPolicy api.PullPolicy
- }
- testCases := map[string]T{
- "NotPresent1": {
- api.Container{Name: "abc", Image: "image:latest", ImagePullPolicy: "IfNotPresent"},
- api.PullIfNotPresent,
- },
- "NotPresent2": {
- api.Container{Name: "abc1", Image: "image", ImagePullPolicy: "IfNotPresent"},
- api.PullIfNotPresent,
- },
- "Always1": {
- api.Container{Name: "123", Image: "image:latest", ImagePullPolicy: "Always"},
- api.PullAlways,
- },
- "Always2": {
- api.Container{Name: "1234", Image: "image", ImagePullPolicy: "Always"},
- api.PullAlways,
- },
- "Never1": {
- api.Container{Name: "abc-123", Image: "image:latest", ImagePullPolicy: "Never"},
- api.PullNever,
- },
- "Never2": {
- api.Container{Name: "abc-1234", Image: "image", ImagePullPolicy: "Never"},
- api.PullNever,
- },
- }
- for k, v := range testCases {
- ctr := &v.Container
- errs := validatePullPolicy(ctr.ImagePullPolicy, field.NewPath("field"))
- if len(errs) != 0 {
- t.Errorf("case[%s] expected success, got %#v", k, errs)
- }
- if ctr.ImagePullPolicy != v.ExpectedPolicy {
- t.Errorf("case[%s] expected policy %v, got %v", k, v.ExpectedPolicy, ctr.ImagePullPolicy)
- }
- }
- }
- func getResourceLimits(cpu, memory string) api.ResourceList {
- res := api.ResourceList{}
- res[api.ResourceCPU] = resource.MustParse(cpu)
- res[api.ResourceMemory] = resource.MustParse(memory)
- return res
- }
- func TestValidateContainers(t *testing.T) {
- volumes := sets.String{}
- capabilities.SetForTests(capabilities.Capabilities{
- AllowPrivileged: true,
- })
- successCase := []api.Container{
- {Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"},
- {Name: "123", Image: "image", ImagePullPolicy: "IfNotPresent"},
- {Name: "abc-123", Image: "image", ImagePullPolicy: "IfNotPresent"},
- {
- Name: "life-123",
- Image: "image",
- Lifecycle: &api.Lifecycle{
- PreStop: &api.Handler{
- Exec: &api.ExecAction{Command: []string{"ls", "-l"}},
- },
- },
- ImagePullPolicy: "IfNotPresent",
- },
- {
- Name: "resources-test",
- Image: "image",
- Resources: api.ResourceRequirements{
- Limits: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
- api.ResourceName("my.org/resource"): resource.MustParse("10m"),
- },
- },
- ImagePullPolicy: "IfNotPresent",
- },
- {
- Name: "resources-test-with-gpu-with-request",
- Image: "image",
- Resources: api.ResourceRequirements{
- Requests: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
- api.ResourceName(api.ResourceNvidiaGPU): resource.MustParse("1"),
- },
- Limits: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
- api.ResourceName(api.ResourceNvidiaGPU): resource.MustParse("1"),
- },
- },
- ImagePullPolicy: "IfNotPresent",
- },
- {
- Name: "resources-test-with-gpu-without-request",
- Image: "image",
- Resources: api.ResourceRequirements{
- Requests: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
- },
- Limits: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
- api.ResourceName(api.ResourceNvidiaGPU): resource.MustParse("1"),
- },
- },
- ImagePullPolicy: "IfNotPresent",
- },
- {
- Name: "resources-request-limit-simple",
- Image: "image",
- Resources: api.ResourceRequirements{
- Requests: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("8"),
- },
- Limits: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- },
- },
- ImagePullPolicy: "IfNotPresent",
- },
- {
- Name: "resources-request-limit-edge",
- Image: "image",
- Resources: api.ResourceRequirements{
- Requests: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
- api.ResourceName("my.org/resource"): resource.MustParse("10m"),
- },
- Limits: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
- api.ResourceName("my.org/resource"): resource.MustParse("10m"),
- },
- },
- ImagePullPolicy: "IfNotPresent",
- },
- {
- Name: "resources-request-limit-partials",
- Image: "image",
- Resources: api.ResourceRequirements{
- Requests: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("9.5"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
- },
- Limits: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName("my.org/resource"): resource.MustParse("10m"),
- },
- },
- ImagePullPolicy: "IfNotPresent",
- },
- {
- Name: "resources-request",
- Image: "image",
- Resources: api.ResourceRequirements{
- Requests: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("9.5"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
- },
- },
- ImagePullPolicy: "IfNotPresent",
- },
- {
- Name: "same-host-port-different-protocol",
- Image: "image",
- Ports: []api.ContainerPort{
- {ContainerPort: 80, HostPort: 80, Protocol: "TCP"},
- {ContainerPort: 80, HostPort: 80, Protocol: "UDP"},
- },
- ImagePullPolicy: "IfNotPresent",
- },
- {Name: "abc-1234", Image: "image", ImagePullPolicy: "IfNotPresent", SecurityContext: fakeValidSecurityContext(true)},
- }
- if errs := validateContainers(successCase, volumes, field.NewPath("field")); len(errs) != 0 {
- t.Errorf("expected success: %v", errs)
- }
- capabilities.SetForTests(capabilities.Capabilities{
- AllowPrivileged: false,
- })
- errorCases := map[string][]api.Container{
- "zero-length name": {{Name: "", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- "name > 63 characters": {{Name: strings.Repeat("a", 64), Image: "image", ImagePullPolicy: "IfNotPresent"}},
- "name not a DNS label": {{Name: "a.b.c", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- "name not unique": {
- {Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"},
- {Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"},
- },
- "zero-length image": {{Name: "abc", Image: "", ImagePullPolicy: "IfNotPresent"}},
- "host port not unique": {
- {Name: "abc", Image: "image", Ports: []api.ContainerPort{{ContainerPort: 80, HostPort: 80, Protocol: "TCP"}},
- ImagePullPolicy: "IfNotPresent"},
- {Name: "def", Image: "image", Ports: []api.ContainerPort{{ContainerPort: 81, HostPort: 80, Protocol: "TCP"}},
- ImagePullPolicy: "IfNotPresent"},
- },
- "invalid env var name": {
- {Name: "abc", Image: "image", Env: []api.EnvVar{{Name: "ev.1"}}, ImagePullPolicy: "IfNotPresent"},
- },
- "unknown volume name": {
- {Name: "abc", Image: "image", VolumeMounts: []api.VolumeMount{{Name: "anything", MountPath: "/foo"}},
- ImagePullPolicy: "IfNotPresent"},
- },
- "invalid lifecycle, no exec command.": {
- {
- Name: "life-123",
- Image: "image",
- Lifecycle: &api.Lifecycle{
- PreStop: &api.Handler{
- Exec: &api.ExecAction{},
- },
- },
- ImagePullPolicy: "IfNotPresent",
- },
- },
- "invalid lifecycle, no http path.": {
- {
- Name: "life-123",
- Image: "image",
- Lifecycle: &api.Lifecycle{
- PreStop: &api.Handler{
- HTTPGet: &api.HTTPGetAction{},
- },
- },
- ImagePullPolicy: "IfNotPresent",
- },
- },
- "invalid lifecycle, no tcp socket port.": {
- {
- Name: "life-123",
- Image: "image",
- Lifecycle: &api.Lifecycle{
- PreStop: &api.Handler{
- TCPSocket: &api.TCPSocketAction{},
- },
- },
- ImagePullPolicy: "IfNotPresent",
- },
- },
- "invalid lifecycle, zero tcp socket port.": {
- {
- Name: "life-123",
- Image: "image",
- Lifecycle: &api.Lifecycle{
- PreStop: &api.Handler{
- TCPSocket: &api.TCPSocketAction{
- Port: intstr.FromInt(0),
- },
- },
- },
- ImagePullPolicy: "IfNotPresent",
- },
- },
- "invalid lifecycle, no action.": {
- {
- Name: "life-123",
- Image: "image",
- Lifecycle: &api.Lifecycle{
- PreStop: &api.Handler{},
- },
- ImagePullPolicy: "IfNotPresent",
- },
- },
- "invalid liveness probe, no tcp socket port.": {
- {
- Name: "life-123",
- Image: "image",
- LivenessProbe: &api.Probe{
- Handler: api.Handler{
- TCPSocket: &api.TCPSocketAction{},
- },
- },
- ImagePullPolicy: "IfNotPresent",
- },
- },
- "invalid liveness probe, no action.": {
- {
- Name: "life-123",
- Image: "image",
- LivenessProbe: &api.Probe{
- Handler: api.Handler{},
- },
- ImagePullPolicy: "IfNotPresent",
- },
- },
- "privilege disabled": {
- {Name: "abc", Image: "image", SecurityContext: fakeValidSecurityContext(true)},
- },
- "invalid compute resource": {
- {
- Name: "abc-123",
- Image: "image",
- Resources: api.ResourceRequirements{
- Limits: api.ResourceList{
- "disk": resource.MustParse("10G"),
- },
- },
- ImagePullPolicy: "IfNotPresent",
- },
- },
- "Resource CPU invalid": {
- {
- Name: "abc-123",
- Image: "image",
- Resources: api.ResourceRequirements{
- Limits: getResourceLimits("-10", "0"),
- },
- ImagePullPolicy: "IfNotPresent",
- },
- },
- "Resource Requests CPU invalid": {
- {
- Name: "abc-123",
- Image: "image",
- Resources: api.ResourceRequirements{
- Requests: getResourceLimits("-10", "0"),
- },
- ImagePullPolicy: "IfNotPresent",
- },
- },
- "Resource Memory invalid": {
- {
- Name: "abc-123",
- Image: "image",
- Resources: api.ResourceRequirements{
- Limits: getResourceLimits("0", "-10"),
- },
- ImagePullPolicy: "IfNotPresent",
- },
- },
- "Resource GPU limit must match request": {
- {
- Name: "gpu-resource-request-limit",
- Image: "image",
- Resources: api.ResourceRequirements{
- Requests: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
- api.ResourceName(api.ResourceNvidiaGPU): resource.MustParse("0"),
- },
- Limits: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
- api.ResourceName(api.ResourceNvidiaGPU): resource.MustParse("1"),
- },
- },
- ImagePullPolicy: "IfNotPresent",
- },
- },
- "Request limit simple invalid": {
- {
- Name: "abc-123",
- Image: "image",
- Resources: api.ResourceRequirements{
- Limits: getResourceLimits("5", "3"),
- Requests: getResourceLimits("6", "3"),
- },
- ImagePullPolicy: "IfNotPresent",
- },
- },
- "Request limit multiple invalid": {
- {
- Name: "abc-123",
- Image: "image",
- Resources: api.ResourceRequirements{
- Limits: getResourceLimits("5", "3"),
- Requests: getResourceLimits("6", "4"),
- },
- ImagePullPolicy: "IfNotPresent",
- },
- },
- }
- for k, v := range errorCases {
- if errs := validateContainers(v, volumes, field.NewPath("field")); len(errs) == 0 {
- t.Errorf("expected failure for %s", k)
- }
- }
- }
- func TestValidateRestartPolicy(t *testing.T) {
- successCases := []api.RestartPolicy{
- api.RestartPolicyAlways,
- api.RestartPolicyOnFailure,
- api.RestartPolicyNever,
- }
- for _, policy := range successCases {
- if errs := validateRestartPolicy(&policy, field.NewPath("field")); len(errs) != 0 {
- t.Errorf("expected success: %v", errs)
- }
- }
- errorCases := []api.RestartPolicy{"", "newpolicy"}
- for k, policy := range errorCases {
- if errs := validateRestartPolicy(&policy, field.NewPath("field")); len(errs) == 0 {
- t.Errorf("expected failure for %d", k)
- }
- }
- }
- func TestValidateDNSPolicy(t *testing.T) {
- successCases := []api.DNSPolicy{api.DNSClusterFirst, api.DNSDefault, api.DNSPolicy(api.DNSClusterFirst)}
- for _, policy := range successCases {
- if errs := validateDNSPolicy(&policy, field.NewPath("field")); len(errs) != 0 {
- t.Errorf("expected success: %v", errs)
- }
- }
- errorCases := []api.DNSPolicy{api.DNSPolicy("invalid")}
- for _, policy := range errorCases {
- if errs := validateDNSPolicy(&policy, field.NewPath("field")); len(errs) == 0 {
- t.Errorf("expected failure for %v", policy)
- }
- }
- }
- func TestValidatePodSpec(t *testing.T) {
- activeDeadlineSeconds := int64(30)
- minID := int64(0)
- maxID := int64(2147483647)
- successCases := []api.PodSpec{
- { // Populate basic fields, leave defaults for most.
- Volumes: []api.Volume{{Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}},
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- { // Populate all fields.
- Volumes: []api.Volume{
- {Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}},
- },
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- InitContainers: []api.Container{{Name: "ictr", Image: "iimage", ImagePullPolicy: "IfNotPresent"}},
- RestartPolicy: api.RestartPolicyAlways,
- NodeSelector: map[string]string{
- "key": "value",
- },
- NodeName: "foobar",
- DNSPolicy: api.DNSClusterFirst,
- ActiveDeadlineSeconds: &activeDeadlineSeconds,
- ServiceAccountName: "acct",
- },
- { // Populate HostNetwork.
- Containers: []api.Container{
- {Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", Ports: []api.ContainerPort{
- {HostPort: 8080, ContainerPort: 8080, Protocol: "TCP"}},
- },
- },
- SecurityContext: &api.PodSecurityContext{
- HostNetwork: true,
- },
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- { // Populate RunAsUser SupplementalGroups FSGroup with minID 0
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- SecurityContext: &api.PodSecurityContext{
- SupplementalGroups: []int64{minID},
- RunAsUser: &minID,
- FSGroup: &minID,
- },
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- { // Populate RunAsUser SupplementalGroups FSGroup with maxID 2147483647
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- SecurityContext: &api.PodSecurityContext{
- SupplementalGroups: []int64{maxID},
- RunAsUser: &maxID,
- FSGroup: &maxID,
- },
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- { // Populate HostIPC.
- SecurityContext: &api.PodSecurityContext{
- HostIPC: true,
- },
- Volumes: []api.Volume{{Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}},
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- { // Populate HostPID.
- SecurityContext: &api.PodSecurityContext{
- HostPID: true,
- },
- Volumes: []api.Volume{{Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}},
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- { // Populate Affinity.
- Volumes: []api.Volume{{Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}},
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- }
- for i := range successCases {
- if errs := ValidatePodSpec(&successCases[i], field.NewPath("field")); len(errs) != 0 {
- t.Errorf("expected success: %v", errs)
- }
- }
- activeDeadlineSeconds = int64(0)
- minID = int64(-1)
- maxID = int64(2147483648)
- failureCases := map[string]api.PodSpec{
- "bad volume": {
- Volumes: []api.Volume{{}},
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- },
- "no containers": {
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- "bad container": {
- Containers: []api.Container{{}},
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- "bad init container": {
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- InitContainers: []api.Container{{}},
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- "bad DNS policy": {
- DNSPolicy: api.DNSPolicy("invalid"),
- RestartPolicy: api.RestartPolicyAlways,
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- },
- "bad service account name": {
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- ServiceAccountName: "invalidName",
- },
- "bad restart policy": {
- RestartPolicy: "UnknowPolicy",
- DNSPolicy: api.DNSClusterFirst,
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- },
- "with hostNetwork hostPort not equal to containerPort": {
- Containers: []api.Container{
- {Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", Ports: []api.ContainerPort{
- {HostPort: 8080, ContainerPort: 2600, Protocol: "TCP"}},
- },
- },
- SecurityContext: &api.PodSecurityContext{
- HostNetwork: true,
- },
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- "bad supplementalGroups large than math.MaxInt32": {
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- SecurityContext: &api.PodSecurityContext{
- HostNetwork: false,
- SupplementalGroups: []int64{maxID, 1234},
- },
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- "bad supplementalGroups less than 0": {
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- SecurityContext: &api.PodSecurityContext{
- HostNetwork: false,
- SupplementalGroups: []int64{minID, 1234},
- },
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- "bad runAsUser large than math.MaxInt32": {
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- SecurityContext: &api.PodSecurityContext{
- HostNetwork: false,
- RunAsUser: &maxID,
- },
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- "bad runAsUser less than 0": {
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- SecurityContext: &api.PodSecurityContext{
- HostNetwork: false,
- RunAsUser: &minID,
- },
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- "bad fsGroup large than math.MaxInt32": {
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- SecurityContext: &api.PodSecurityContext{
- HostNetwork: false,
- FSGroup: &maxID,
- },
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- "bad fsGroup less than 0": {
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- SecurityContext: &api.PodSecurityContext{
- HostNetwork: false,
- FSGroup: &minID,
- },
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- "bad-active-deadline-seconds": {
- Volumes: []api.Volume{
- {Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}},
- },
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- RestartPolicy: api.RestartPolicyAlways,
- NodeSelector: map[string]string{
- "key": "value",
- },
- NodeName: "foobar",
- DNSPolicy: api.DNSClusterFirst,
- ActiveDeadlineSeconds: &activeDeadlineSeconds,
- },
- "bad nodeName": {
- NodeName: "node name",
- Volumes: []api.Volume{{Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}},
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- }
- for k, v := range failureCases {
- if errs := ValidatePodSpec(&v, field.NewPath("field")); len(errs) == 0 {
- t.Errorf("expected failure for %q", k)
- }
- }
- }
- func TestValidatePod(t *testing.T) {
- validPodSpec := api.PodSpec{
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- }
- successCases := []api.Pod{
- { // Basic fields.
- ObjectMeta: api.ObjectMeta{Name: "123", Namespace: "ns"},
- Spec: api.PodSpec{
- Volumes: []api.Volume{{Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}},
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- },
- { // Just about everything.
- ObjectMeta: api.ObjectMeta{Name: "abc.123.do-re-mi", Namespace: "ns"},
- Spec: api.PodSpec{
- Volumes: []api.Volume{
- {Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}},
- },
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- NodeSelector: map[string]string{
- "key": "value",
- },
- NodeName: "foobar",
- },
- },
- { // Serialized affinity requirements in annotations.
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- // TODO: Uncomment and move this block into Annotations map once
- // RequiredDuringSchedulingRequiredDuringExecution is implemented
- // "requiredDuringSchedulingRequiredDuringExecution": {
- // "nodeSelectorTerms": [{
- // "matchExpressions": [{
- // "key": "key1",
- // "operator": "Exists"
- // }]
- // }]
- // },
- Annotations: map[string]string{
- api.AffinityAnnotationKey: `
- {"nodeAffinity": {
- "requiredDuringSchedulingIgnoredDuringExecution": {
- "nodeSelectorTerms": [{
- "matchExpressions": [{
- "key": "key2",
- "operator": "In",
- "values": ["value1", "value2"]
- }]
- }]
- },
- "preferredDuringSchedulingIgnoredDuringExecution": [
- {
- "weight": 10,
- "preference": {"matchExpressions": [
- {
- "key": "foo",
- "operator": "In", "values": ["bar"]
- }
- ]}
- }
- ]
- }}`,
- },
- },
- Spec: api.PodSpec{
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- },
- { // Serialized pod affinity in affinity requirements in annotations.
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- // TODO: Uncomment and move this block into Annotations map once
- // RequiredDuringSchedulingRequiredDuringExecution is implemented
- // "requiredDuringSchedulingRequiredDuringExecution": [{
- // "labelSelector": {
- // "matchExpressions": [{
- // "key": "key2",
- // "operator": "In",
- // "values": ["value1", "value2"]
- // }]
- // },
- // "namespaces":["ns"],
- // "topologyKey": "zone"
- // }]
- Annotations: map[string]string{
- api.AffinityAnnotationKey: `
- {"podAffinity": {
- "requiredDuringSchedulingIgnoredDuringExecution": [{
- "labelSelector": {
- "matchExpressions": [{
- "key": "key2",
- "operator": "In",
- "values": ["value1", "value2"]
- }]
- },
- "topologyKey": "zone",
- "namespaces": ["ns"]
- }],
- "preferredDuringSchedulingIgnoredDuringExecution": [{
- "weight": 10,
- "podAffinityTerm": {
- "labelSelector": {
- "matchExpressions": [{
- "key": "key2",
- "operator": "NotIn",
- "values": ["value1", "value2"]
- }]
- },
- "namespaces": ["ns"],
- "topologyKey": "region"
- }
- }]
- }}`,
- },
- },
- Spec: validPodSpec,
- },
- { // Serialized pod anti affinity with different Label Operators in affinity requirements in annotations.
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- // TODO: Uncomment and move this block into Annotations map once
- // RequiredDuringSchedulingRequiredDuringExecution is implemented
- // "requiredDuringSchedulingRequiredDuringExecution": [{
- // "labelSelector": {
- // "matchExpressions": [{
- // "key": "key2",
- // "operator": "In",
- // "values": ["value1", "value2"]
- // }]
- // },
- // "namespaces":["ns"],
- // "topologyKey": "zone"
- // }]
- Annotations: map[string]string{
- api.AffinityAnnotationKey: `
- {"podAntiAffinity": {
- "requiredDuringSchedulingIgnoredDuringExecution": [{
- "labelSelector": {
- "matchExpressions": [{
- "key": "key2",
- "operator": "Exists"
- }]
- },
- "topologyKey": "zone",
- "namespaces": ["ns"]
- }],
- "preferredDuringSchedulingIgnoredDuringExecution": [{
- "weight": 10,
- "podAffinityTerm": {
- "labelSelector": {
- "matchExpressions": [{
- "key": "key2",
- "operator": "DoesNotExist"
- }]
- },
- "namespaces": ["ns"],
- "topologyKey": "region"
- }
- }]
- }}`,
- },
- },
- Spec: validPodSpec,
- },
- { // populate tolerations equal operator in annotations.
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.TolerationsAnnotationKey: `
- [{
- "key": "foo",
- "operator": "Equal",
- "value": "bar",
- "effect": "NoSchedule"
- }]`,
- },
- },
- Spec: validPodSpec,
- },
- { // populate tolerations exists operator in annotations.
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.TolerationsAnnotationKey: `
- [{
- "key": "foo",
- "operator": "Exists",
- "effect": "NoSchedule"
- }]`,
- },
- },
- Spec: validPodSpec,
- },
- { // empty operator is ok for toleration
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.TolerationsAnnotationKey: `
- [{
- "key": "foo",
- "value": "bar",
- "effect": "NoSchedule"
- }]`,
- },
- },
- Spec: validPodSpec,
- },
- { // empty efffect is ok for toleration
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.TolerationsAnnotationKey: `
- [{
- "key": "foo",
- "operator": "Equal",
- "value": "bar"
- }]`,
- },
- },
- Spec: validPodSpec,
- },
- { // docker default seccomp profile
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.SeccompPodAnnotationKey: "docker/default",
- },
- },
- Spec: validPodSpec,
- },
- { // unconfined seccomp profile
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.SeccompPodAnnotationKey: "unconfined",
- },
- },
- Spec: validPodSpec,
- },
- { // localhost seccomp profile
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.SeccompPodAnnotationKey: "localhost/foo",
- },
- },
- Spec: validPodSpec,
- },
- { // localhost seccomp profile for a container
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.SeccompContainerAnnotationKeyPrefix + "foo": "localhost/foo",
- },
- },
- Spec: validPodSpec,
- },
- { // default AppArmor profile for a container
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- apparmor.ContainerAnnotationKeyPrefix + "ctr": apparmor.ProfileRuntimeDefault,
- },
- },
- Spec: validPodSpec,
- },
- { // default AppArmor profile for an init container
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- apparmor.ContainerAnnotationKeyPrefix + "init-ctr": apparmor.ProfileRuntimeDefault,
- },
- },
- Spec: api.PodSpec{
- InitContainers: []api.Container{{Name: "init-ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- },
- { // localhost AppArmor profile for a container
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- apparmor.ContainerAnnotationKeyPrefix + "ctr": apparmor.ProfileNamePrefix + "foo",
- },
- },
- Spec: validPodSpec,
- },
- { // syntactically valid sysctls
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.SysctlsPodAnnotationKey: "kernel.shmmni=32768,kernel.shmmax=1000000000",
- api.UnsafeSysctlsPodAnnotationKey: "knet.ipv4.route.min_pmtu=1000",
- },
- },
- Spec: validPodSpec,
- },
- }
- for _, pod := range successCases {
- if errs := ValidatePod(&pod); len(errs) != 0 {
- t.Errorf("expected success: %v", errs)
- }
- }
- errorCases := map[string]api.Pod{
- "bad name": {
- ObjectMeta: api.ObjectMeta{Name: "", Namespace: "ns"},
- Spec: api.PodSpec{
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- },
- },
- "bad namespace": {
- ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: ""},
- Spec: api.PodSpec{
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- },
- },
- "bad spec": {
- ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "ns"},
- Spec: api.PodSpec{
- Containers: []api.Container{{}},
- },
- },
- "bad label": {
- ObjectMeta: api.ObjectMeta{
- Name: "abc",
- Namespace: "ns",
- Labels: map[string]string{
- "NoUppercaseOrSpecialCharsLike=Equals": "bar",
- },
- },
- Spec: api.PodSpec{
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- },
- },
- "invalid json of node affinity in pod annotations": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.AffinityAnnotationKey: `
- {"nodeAffinity": {
- "requiredDuringSchedulingIgnoredDuringExecution": {
- "nodeSelectorTerms": [{
- `,
- },
- },
- Spec: validPodSpec,
- },
- "invalid node selector requirement in node affinity in pod annotations, operator can't be null": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.AffinityAnnotationKey: `
- {"nodeAffinity": {"requiredDuringSchedulingIgnoredDuringExecution": {
- "nodeSelectorTerms": [{
- "matchExpressions": [{
- "key": "key1",
- }]
- }]
- }}}`,
- },
- },
- Spec: validPodSpec,
- },
- "invalid preferredSchedulingTerm in node affinity in pod annotations, weight should be in range 1-100": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.AffinityAnnotationKey: `
- {"nodeAffinity": {"preferredDuringSchedulingIgnoredDuringExecution": [
- {
- "weight": 199,
- "preference": {"matchExpressions": [
- {
- "key": "foo",
- "operator": "In",
- "values": ["bar"]
- }
- ]}
- }
- ]}}`,
- },
- },
- Spec: validPodSpec,
- },
- "invalid requiredDuringSchedulingIgnoredDuringExecution node selector, nodeSelectorTerms must have at least one term": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.AffinityAnnotationKey: `
- {"nodeAffinity": {
- "requiredDuringSchedulingIgnoredDuringExecution": {
- "nodeSelectorTerms": []
- },
- }}`,
- },
- },
- Spec: validPodSpec,
- },
- "invalid requiredDuringSchedulingIgnoredDuringExecution node selector term, matchExpressions must have at least one node selector requirement": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.AffinityAnnotationKey: `
- {"nodeAffinity": {
- "requiredDuringSchedulingIgnoredDuringExecution": {
- "nodeSelectorTerms": [{
- "matchExpressions": []
- }]
- },
- }}`,
- },
- },
- Spec: validPodSpec,
- },
- "invalid weight in preferredDuringSchedulingIgnoredDuringExecution in pod affinity annotations, weight should be in range 1-100": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.AffinityAnnotationKey: `
- {"podAffinity": {"preferredDuringSchedulingIgnoredDuringExecution": [{
- "weight": 109,
- "podAffinityTerm":
- {
- "labelSelector": {
- "matchExpressions": [{
- "key": "key2",
- "operator": "NotIn",
- "values": ["value1", "value2"]
- }]
- },
- "namespaces": ["ns"],
- "topologyKey": "region"
- }
- }]}}`,
- },
- },
- Spec: validPodSpec,
- },
- "invalid labelSelector in preferredDuringSchedulingIgnoredDuringExecution in podaffinity annotations, values should be empty if the operator is Exists": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.AffinityAnnotationKey: `
- {"podAffinity": {"preferredDuringSchedulingIgnoredDuringExecution": [{
- "weight": 10,
- "podAffinityTerm":
- {
- "labelSelector": {
- "matchExpressions": [{
- "key": "key2",
- "operator": "Exists",
- "values": ["value1", "value2"]
- }]
- },
- "namespaces": ["ns"],
- "topologyKey": "region"
- }
- }]}}`,
- },
- },
- Spec: validPodSpec,
- },
- "invalid name space in preferredDuringSchedulingIgnoredDuringExecution in podaffinity annotations, name space shouldbe valid": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.AffinityAnnotationKey: `
- {"podAffinity": {"preferredDuringSchedulingIgnoredDuringExecution": [{
- "weight": 10,
- "podAffinityTerm":
- {
- "labelSelector": {
- "matchExpressions": [{
- "key": "key2",
- "operator": "Exists",
- "values": ["value1", "value2"]
- }]
- },
- "namespaces": ["INVALID_NAMESPACE"],
- "topologyKey": "region"
- }
- }]}}`,
- },
- },
- Spec: validPodSpec,
- },
- "invalid labelOperator in preferredDuringSchedulingIgnoredDuringExecution in podantiaffinity annotations, labelOperator should be proper": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.AffinityAnnotationKey: `
- {"podAntiAffinity": {"preferredDuringSchedulingIgnoredDuringExecution": [{
- "weight": 10,
- "podAffinityTerm":
- {
- "labelSelector": {
- "matchExpressions": [{
- "key": "key2",
- "operator": "WrongOp",
- "values": ["value1", "value2"]
- }]
- },
- "namespaces": ["ns"],
- "topologyKey": "region"
- }
- }]}}`,
- },
- },
- Spec: validPodSpec,
- },
- "invalid pod affinity, empty topologyKey is not allowed for hard pod affinity": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.AffinityAnnotationKey: `
- {"podAffinity": {"requiredDuringSchedulingIgnoredDuringExecution": [{
- "weight": 10,
- "podAffinityTerm":
- {
- "labelSelector": {
- "matchExpressions": [{
- "key": "key2",
- "operator": "In",
- "values": ["value1", "value2"]
- }]
- },
- "namespaces": ["ns"],
- "topologyKey": ""
- }
- }]}}`,
- },
- },
- Spec: validPodSpec,
- },
- "invalid pod anti-affinity, empty topologyKey is not allowed for hard pod anti-affinity": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.AffinityAnnotationKey: `
- {"podAntiAffinity": {"requiredDuringSchedulingIgnoredDuringExecution": [{
- "weight": 10,
- "podAffinityTerm":
- {
- "labelSelector": {
- "matchExpressions": [{
- "key": "key2",
- "operator": "In",
- "values": ["value1", "value2"]
- }]
- },
- "namespaces": ["ns"],
- "topologyKey": ""
- }
- }]}}`,
- },
- },
- Spec: validPodSpec,
- },
- "invalid pod anti-affinity, empty topologyKey is not allowed for soft pod affinity": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.AffinityAnnotationKey: `
- {"podAffinity": {"preferredDuringSchedulingIgnoredDuringExecution": [{
- "weight": 10,
- "podAffinityTerm":
- {
- "labelSelector": {
- "matchExpressions": [{
- "key": "key2",
- "operator": "In",
- "values": ["value1", "value2"]
- }]
- },
- "namespaces": ["ns"],
- "topologyKey": ""
- }
- }]}}`,
- },
- },
- Spec: validPodSpec,
- },
- "invalid toleration key": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.TolerationsAnnotationKey: `
- [{
- "key": "nospecialchars^=@",
- "operator": "Equal",
- "value": "bar",
- "effect": "NoSchedule"
- }]`,
- },
- },
- Spec: validPodSpec,
- },
- "invalid toleration operator": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.TolerationsAnnotationKey: `
- [{
- "key": "foo",
- "operator": "In",
- "value": "bar",
- "effect": "NoSchedule"
- }]`,
- },
- },
- Spec: validPodSpec,
- },
- "value must be empty when `operator` is 'Exists'": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.TolerationsAnnotationKey: `
- [{
- "key": "foo",
- "operator": "Exists",
- "value": "bar",
- "effect": "NoSchedule"
- }]`,
- },
- },
- Spec: validPodSpec,
- },
- "must be a valid pod seccomp profile": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.SeccompPodAnnotationKey: "foo",
- },
- },
- Spec: validPodSpec,
- },
- "must be a valid container seccomp profile": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.SeccompContainerAnnotationKeyPrefix + "foo": "foo",
- },
- },
- Spec: validPodSpec,
- },
- "must be a non-empty container name in seccomp annotation": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.SeccompContainerAnnotationKeyPrefix: "foo",
- },
- },
- Spec: validPodSpec,
- },
- "must be a non-empty container profile in seccomp annotation": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.SeccompContainerAnnotationKeyPrefix + "foo": "",
- },
- },
- Spec: validPodSpec,
- },
- "must be a relative path in a node-local seccomp profile annotation": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.SeccompPodAnnotationKey: "localhost//foo",
- },
- },
- Spec: validPodSpec,
- },
- "must not start with '../'": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.SeccompPodAnnotationKey: "localhost/../foo",
- },
- },
- Spec: validPodSpec,
- },
- "AppArmor profile must apply to a container": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- apparmor.ContainerAnnotationKeyPrefix + "ctr": apparmor.ProfileRuntimeDefault,
- apparmor.ContainerAnnotationKeyPrefix + "init-ctr": apparmor.ProfileRuntimeDefault,
- apparmor.ContainerAnnotationKeyPrefix + "fake-ctr": apparmor.ProfileRuntimeDefault,
- },
- },
- Spec: api.PodSpec{
- InitContainers: []api.Container{{Name: "init-ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- },
- "AppArmor profile format must be valid": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- apparmor.ContainerAnnotationKeyPrefix + "ctr": "bad-name",
- },
- },
- Spec: validPodSpec,
- },
- "only default AppArmor profile may start with runtime/": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- apparmor.ContainerAnnotationKeyPrefix + "ctr": "runtime/foo",
- },
- },
- Spec: validPodSpec,
- },
- "invalid sysctl annotation": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.SysctlsPodAnnotationKey: "foo:",
- },
- },
- Spec: validPodSpec,
- },
- "invalid comma-separated sysctl annotation": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.SysctlsPodAnnotationKey: "kernel.msgmax,",
- },
- },
- Spec: validPodSpec,
- },
- "invalid unsafe sysctl annotation": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.SysctlsPodAnnotationKey: "foo:",
- },
- },
- Spec: validPodSpec,
- },
- "intersecting safe sysctls and unsafe sysctls annotations": {
- ObjectMeta: api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Annotations: map[string]string{
- api.SysctlsPodAnnotationKey: "kernel.shmmax=10000000",
- api.UnsafeSysctlsPodAnnotationKey: "kernel.shmmax=10000000",
- },
- },
- Spec: validPodSpec,
- },
- }
- for k, v := range errorCases {
- if errs := ValidatePod(&v); len(errs) == 0 {
- t.Errorf("expected failure for %q", k)
- }
- }
- }
- func TestValidatePodUpdate(t *testing.T) {
- var (
- activeDeadlineSecondsZero = int64(0)
- activeDeadlineSecondsNegative = int64(-30)
- activeDeadlineSecondsPositive = int64(30)
- activeDeadlineSecondsLarger = int64(31)
- now = unversioned.Now()
- grace = int64(30)
- grace2 = int64(31)
- )
- tests := []struct {
- a api.Pod
- b api.Pod
- isValid bool
- test string
- }{
- {api.Pod{}, api.Pod{}, true, "nothing"},
- {
- api.Pod{
- ObjectMeta: api.ObjectMeta{Name: "foo"},
- },
- api.Pod{
- ObjectMeta: api.ObjectMeta{Name: "bar"},
- },
- false,
- "ids",
- },
- {
- api.Pod{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Labels: map[string]string{
- "foo": "bar",
- },
- },
- },
- api.Pod{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Labels: map[string]string{
- "bar": "foo",
- },
- },
- },
- true,
- "labels",
- },
- {
- api.Pod{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Annotations: map[string]string{
- "foo": "bar",
- },
- },
- },
- api.Pod{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Annotations: map[string]string{
- "bar": "foo",
- },
- },
- },
- true,
- "annotations",
- },
- {
- api.Pod{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- },
- Spec: api.PodSpec{
- Containers: []api.Container{
- {
- Image: "foo:V1",
- },
- },
- },
- },
- api.Pod{
- ObjectMeta: api.ObjectMeta{Name: "foo"},
- Spec: api.PodSpec{
- Containers: []api.Container{
- {
- Image: "foo:V2",
- },
- {
- Image: "bar:V2",
- },
- },
- },
- },
- false,
- "more containers",
- },
- {
- api.Pod{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- },
- Spec: api.PodSpec{
- InitContainers: []api.Container{
- {
- Image: "foo:V1",
- },
- },
- },
- },
- api.Pod{
- ObjectMeta: api.ObjectMeta{Name: "foo"},
- Spec: api.PodSpec{
- InitContainers: []api.Container{
- {
- Image: "foo:V2",
- },
- {
- Image: "bar:V2",
- },
- },
- },
- },
- false,
- "more init containers",
- },
- {
- api.Pod{
- ObjectMeta: api.ObjectMeta{Name: "foo"},
- Spec: api.PodSpec{Containers: []api.Container{{Image: "foo:V1"}}},
- },
- api.Pod{
- ObjectMeta: api.ObjectMeta{Name: "foo", DeletionTimestamp: &now},
- Spec: api.PodSpec{Containers: []api.Container{{Image: "foo:V1"}}},
- },
- true,
- "deletion timestamp filled out",
- },
- {
- api.Pod{
- ObjectMeta: api.ObjectMeta{Name: "foo", DeletionTimestamp: &now, DeletionGracePeriodSeconds: &grace},
- Spec: api.PodSpec{Containers: []api.Container{{Image: "foo:V1"}}},
- },
- api.Pod{
- ObjectMeta: api.ObjectMeta{Name: "foo", DeletionTimestamp: &now, DeletionGracePeriodSeconds: &grace2},
- Spec: api.PodSpec{Containers: []api.Container{{Image: "foo:V1"}}},
- },
- false,
- "deletion grace period seconds cleared",
- },
- {
- api.Pod{
- ObjectMeta: api.ObjectMeta{Name: "foo"},
- Spec: api.PodSpec{
- Containers: []api.Container{
- {
- Image: "foo:V1",
- },
- },
- },
- },
- api.Pod{
- ObjectMeta: api.ObjectMeta{Name: "foo"},
- Spec: api.PodSpec{
- Containers: []api.Container{
- {
- Image: "foo:V2",
- },
- },
- },
- },
- true,
- "image change",
- },
- {
- api.Pod{
- ObjectMeta: api.ObjectMeta{Name: "foo"},
- Spec: api.PodSpec{
- InitContainers: []api.Container{
- {
- Image: "foo:V1",
- },
- },
- },
- },
- api.Pod{
- ObjectMeta: api.ObjectMeta{Name: "foo"},
- Spec: api.PodSpec{
- InitContainers: []api.Container{
- {
- Image: "foo:V2",
- },
- },
- },
- },
- true,
- "init container image change",
- },
- {
- api.Pod{
- ObjectMeta: api.ObjectMeta{Name: "foo"},
- Spec: api.PodSpec{
- Containers: []api.Container{
- {},
- },
- },
- },
- api.Pod{
- ObjectMeta: api.ObjectMeta{Name: "foo"},
- Spec: api.PodSpec{
- Containers: []api.Container{
- {
- Image: "foo:V2",
- },
- },
- },
- },
- false,
- "image change to empty",
- },
- {
- api.Pod{
- ObjectMeta: api.ObjectMeta{Name: "foo"},
- Spec: api.PodSpec{
- InitContainers: []api.Container{
- {},
- },
- },
- },
- api.Pod{
- ObjectMeta: api.ObjectMeta{Name: "foo"},
- Spec: api.PodSpec{
- InitContainers: []api.Container{
- {
- Image: "foo:V2",
- },
- },
- },
- },
- false,
- "init container image change to empty",
- },
- {
- api.Pod{
- Spec: api.PodSpec{},
- },
- api.Pod{
- Spec: api.PodSpec{},
- },
- true,
- "activeDeadlineSeconds no change, nil",
- },
- {
- api.Pod{
- Spec: api.PodSpec{
- ActiveDeadlineSeconds: &activeDeadlineSecondsPositive,
- },
- },
- api.Pod{
- Spec: api.PodSpec{
- ActiveDeadlineSeconds: &activeDeadlineSecondsPositive,
- },
- },
- true,
- "activeDeadlineSeconds no change, set",
- },
- {
- api.Pod{
- Spec: api.PodSpec{
- ActiveDeadlineSeconds: &activeDeadlineSecondsPositive,
- },
- },
- api.Pod{},
- true,
- "activeDeadlineSeconds change to positive from nil",
- },
- {
- api.Pod{
- Spec: api.PodSpec{
- ActiveDeadlineSeconds: &activeDeadlineSecondsPositive,
- },
- },
- api.Pod{
- Spec: api.PodSpec{
- ActiveDeadlineSeconds: &activeDeadlineSecondsLarger,
- },
- },
- true,
- "activeDeadlineSeconds change to smaller positive",
- },
- {
- api.Pod{
- Spec: api.PodSpec{
- ActiveDeadlineSeconds: &activeDeadlineSecondsLarger,
- },
- },
- api.Pod{
- Spec: api.PodSpec{
- ActiveDeadlineSeconds: &activeDeadlineSecondsPositive,
- },
- },
- false,
- "activeDeadlineSeconds change to larger positive",
- },
- {
- api.Pod{
- Spec: api.PodSpec{
- ActiveDeadlineSeconds: &activeDeadlineSecondsNegative,
- },
- },
- api.Pod{},
- false,
- "activeDeadlineSeconds change to negative from nil",
- },
- {
- api.Pod{
- Spec: api.PodSpec{
- ActiveDeadlineSeconds: &activeDeadlineSecondsNegative,
- },
- },
- api.Pod{
- Spec: api.PodSpec{
- ActiveDeadlineSeconds: &activeDeadlineSecondsPositive,
- },
- },
- false,
- "activeDeadlineSeconds change to negative from positive",
- },
- {
- api.Pod{
- Spec: api.PodSpec{
- ActiveDeadlineSeconds: &activeDeadlineSecondsZero,
- },
- },
- api.Pod{
- Spec: api.PodSpec{
- ActiveDeadlineSeconds: &activeDeadlineSecondsPositive,
- },
- },
- true,
- "activeDeadlineSeconds change to zero from positive",
- },
- {
- api.Pod{
- Spec: api.PodSpec{
- ActiveDeadlineSeconds: &activeDeadlineSecondsZero,
- },
- },
- api.Pod{},
- true,
- "activeDeadlineSeconds change to zero from nil",
- },
- {
- api.Pod{},
- api.Pod{
- Spec: api.PodSpec{
- ActiveDeadlineSeconds: &activeDeadlineSecondsPositive,
- },
- },
- false,
- "activeDeadlineSeconds change to nil from positive",
- },
- {
- api.Pod{
- ObjectMeta: api.ObjectMeta{Name: "foo"},
- Spec: api.PodSpec{
- Containers: []api.Container{
- {
- Image: "foo:V1",
- Resources: api.ResourceRequirements{
- Limits: getResourceLimits("100m", "0"),
- },
- },
- },
- },
- },
- api.Pod{
- ObjectMeta: api.ObjectMeta{Name: "foo"},
- Spec: api.PodSpec{
- Containers: []api.Container{
- {
- Image: "foo:V2",
- Resources: api.ResourceRequirements{
- Limits: getResourceLimits("1000m", "0"),
- },
- },
- },
- },
- },
- false,
- "cpu change",
- },
- {
- api.Pod{
- ObjectMeta: api.ObjectMeta{Name: "foo"},
- Spec: api.PodSpec{
- Containers: []api.Container{
- {
- Image: "foo:V1",
- Ports: []api.ContainerPort{
- {HostPort: 8080, ContainerPort: 80},
- },
- },
- },
- },
- },
- api.Pod{
- ObjectMeta: api.ObjectMeta{Name: "foo"},
- Spec: api.PodSpec{
- Containers: []api.Container{
- {
- Image: "foo:V2",
- Ports: []api.ContainerPort{
- {HostPort: 8000, ContainerPort: 80},
- },
- },
- },
- },
- },
- false,
- "port change",
- },
- {
- api.Pod{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Labels: map[string]string{
- "foo": "bar",
- },
- },
- },
- api.Pod{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Labels: map[string]string{
- "Bar": "foo",
- },
- },
- },
- true,
- "bad label change",
- },
- }
- for _, test := range tests {
- test.a.ObjectMeta.ResourceVersion = "1"
- test.b.ObjectMeta.ResourceVersion = "1"
- errs := ValidatePodUpdate(&test.a, &test.b)
- if test.isValid {
- if len(errs) != 0 {
- t.Errorf("unexpected invalid: %s (%+v)\nA: %+v\nB: %+v", test.test, errs, test.a, test.b)
- }
- } else {
- if len(errs) == 0 {
- t.Errorf("unexpected valid: %s\nA: %+v\nB: %+v", test.test, test.a, test.b)
- }
- }
- }
- }
- func makeValidService() api.Service {
- return api.Service{
- ObjectMeta: api.ObjectMeta{
- Name: "valid",
- Namespace: "valid",
- Labels: map[string]string{},
- Annotations: map[string]string{},
- ResourceVersion: "1",
- },
- Spec: api.ServiceSpec{
- Selector: map[string]string{"key": "val"},
- SessionAffinity: "None",
- Type: api.ServiceTypeClusterIP,
- Ports: []api.ServicePort{{Name: "p", Protocol: "TCP", Port: 8675, TargetPort: intstr.FromInt(8675)}},
- },
- }
- }
- func TestValidateService(t *testing.T) {
- testCases := []struct {
- name string
- tweakSvc func(svc *api.Service) // given a basic valid service, each test case can customize it
- numErrs int
- }{
- {
- name: "missing namespace",
- tweakSvc: func(s *api.Service) {
- s.Namespace = ""
- },
- numErrs: 1,
- },
- {
- name: "invalid namespace",
- tweakSvc: func(s *api.Service) {
- s.Namespace = "-123"
- },
- numErrs: 1,
- },
- {
- name: "missing name",
- tweakSvc: func(s *api.Service) {
- s.Name = ""
- },
- numErrs: 1,
- },
- {
- name: "invalid name",
- tweakSvc: func(s *api.Service) {
- s.Name = "-123"
- },
- numErrs: 1,
- },
- {
- name: "too long name",
- tweakSvc: func(s *api.Service) {
- s.Name = strings.Repeat("a", 64)
- },
- numErrs: 1,
- },
- {
- name: "invalid generateName",
- tweakSvc: func(s *api.Service) {
- s.GenerateName = "-123"
- },
- numErrs: 1,
- },
- {
- name: "too long generateName",
- tweakSvc: func(s *api.Service) {
- s.GenerateName = strings.Repeat("a", 64)
- },
- numErrs: 1,
- },
- {
- name: "invalid label",
- tweakSvc: func(s *api.Service) {
- s.Labels["NoUppercaseOrSpecialCharsLike=Equals"] = "bar"
- },
- numErrs: 1,
- },
- {
- name: "invalid annotation",
- tweakSvc: func(s *api.Service) {
- s.Annotations["NoSpecialCharsLike=Equals"] = "bar"
- },
- numErrs: 1,
- },
- {
- name: "nil selector",
- tweakSvc: func(s *api.Service) {
- s.Spec.Selector = nil
- },
- numErrs: 0,
- },
- {
- name: "invalid selector",
- tweakSvc: func(s *api.Service) {
- s.Spec.Selector["NoSpecialCharsLike=Equals"] = "bar"
- },
- numErrs: 1,
- },
- {
- name: "missing session affinity",
- tweakSvc: func(s *api.Service) {
- s.Spec.SessionAffinity = ""
- },
- numErrs: 1,
- },
- {
- name: "missing type",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = ""
- },
- numErrs: 1,
- },
- {
- name: "missing ports",
- tweakSvc: func(s *api.Service) {
- s.Spec.Ports = nil
- },
- numErrs: 1,
- },
- {
- name: "missing ports but headless",
- tweakSvc: func(s *api.Service) {
- s.Spec.Ports = nil
- s.Spec.ClusterIP = api.ClusterIPNone
- },
- numErrs: 0,
- },
- {
- name: "empty port[0] name",
- tweakSvc: func(s *api.Service) {
- s.Spec.Ports[0].Name = ""
- },
- numErrs: 0,
- },
- {
- name: "empty port[1] name",
- tweakSvc: func(s *api.Service) {
- s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "", Protocol: "TCP", Port: 12345, TargetPort: intstr.FromInt(12345)})
- },
- numErrs: 1,
- },
- {
- name: "empty multi-port port[0] name",
- tweakSvc: func(s *api.Service) {
- s.Spec.Ports[0].Name = ""
- s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "p", Protocol: "TCP", Port: 12345, TargetPort: intstr.FromInt(12345)})
- },
- numErrs: 1,
- },
- {
- name: "invalid port name",
- tweakSvc: func(s *api.Service) {
- s.Spec.Ports[0].Name = "INVALID"
- },
- numErrs: 1,
- },
- {
- name: "missing protocol",
- tweakSvc: func(s *api.Service) {
- s.Spec.Ports[0].Protocol = ""
- },
- numErrs: 1,
- },
- {
- name: "invalid protocol",
- tweakSvc: func(s *api.Service) {
- s.Spec.Ports[0].Protocol = "INVALID"
- },
- numErrs: 1,
- },
- {
- name: "invalid cluster ip",
- tweakSvc: func(s *api.Service) {
- s.Spec.ClusterIP = "invalid"
- },
- numErrs: 1,
- },
- {
- name: "missing port",
- tweakSvc: func(s *api.Service) {
- s.Spec.Ports[0].Port = 0
- },
- numErrs: 1,
- },
- {
- name: "invalid port",
- tweakSvc: func(s *api.Service) {
- s.Spec.Ports[0].Port = 65536
- },
- numErrs: 1,
- },
- {
- name: "invalid TargetPort int",
- tweakSvc: func(s *api.Service) {
- s.Spec.Ports[0].TargetPort = intstr.FromInt(65536)
- },
- numErrs: 1,
- },
- {
- name: "valid port headless",
- tweakSvc: func(s *api.Service) {
- s.Spec.Ports[0].Port = 11722
- s.Spec.Ports[0].TargetPort = intstr.FromInt(11722)
- s.Spec.ClusterIP = api.ClusterIPNone
- },
- numErrs: 0,
- },
- {
- name: "invalid port headless 1",
- tweakSvc: func(s *api.Service) {
- s.Spec.Ports[0].Port = 11722
- s.Spec.Ports[0].TargetPort = intstr.FromInt(11721)
- s.Spec.ClusterIP = api.ClusterIPNone
- },
- // in the v1 API, targetPorts on headless services were tolerated.
- // once we have version-specific validation, we can reject this on newer API versions, but until then, we have to tolerate it for compatibility.
- // numErrs: 1,
- numErrs: 0,
- },
- {
- name: "invalid port headless 2",
- tweakSvc: func(s *api.Service) {
- s.Spec.Ports[0].Port = 11722
- s.Spec.Ports[0].TargetPort = intstr.FromString("target")
- s.Spec.ClusterIP = api.ClusterIPNone
- },
- // in the v1 API, targetPorts on headless services were tolerated.
- // once we have version-specific validation, we can reject this on newer API versions, but until then, we have to tolerate it for compatibility.
- // numErrs: 1,
- numErrs: 0,
- },
- {
- name: "invalid publicIPs localhost",
- tweakSvc: func(s *api.Service) {
- s.Spec.ExternalIPs = []string{"127.0.0.1"}
- },
- numErrs: 1,
- },
- {
- name: "invalid publicIPs unspecified",
- tweakSvc: func(s *api.Service) {
- s.Spec.ExternalIPs = []string{"0.0.0.0"}
- },
- numErrs: 1,
- },
- {
- name: "invalid publicIPs loopback",
- tweakSvc: func(s *api.Service) {
- s.Spec.ExternalIPs = []string{"127.0.0.1"}
- },
- numErrs: 1,
- },
- {
- name: "invalid publicIPs host",
- tweakSvc: func(s *api.Service) {
- s.Spec.ExternalIPs = []string{"myhost.mydomain"}
- },
- numErrs: 1,
- },
- {
- name: "dup port name",
- tweakSvc: func(s *api.Service) {
- s.Spec.Ports[0].Name = "p"
- s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "p", Port: 12345, Protocol: "TCP", TargetPort: intstr.FromInt(12345)})
- },
- numErrs: 1,
- },
- {
- name: "valid load balancer protocol UDP 1",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeLoadBalancer
- s.Spec.Ports[0].Protocol = "UDP"
- },
- numErrs: 0,
- },
- {
- name: "valid load balancer protocol UDP 2",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeLoadBalancer
- s.Spec.Ports[0] = api.ServicePort{Name: "q", Port: 12345, Protocol: "UDP", TargetPort: intstr.FromInt(12345)}
- },
- numErrs: 0,
- },
- {
- name: "invalid load balancer with mix protocol",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeLoadBalancer
- s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "UDP", TargetPort: intstr.FromInt(12345)})
- },
- numErrs: 1,
- },
- {
- name: "valid 1",
- tweakSvc: func(s *api.Service) {
- // do nothing
- },
- numErrs: 0,
- },
- {
- name: "valid 2",
- tweakSvc: func(s *api.Service) {
- s.Spec.Ports[0].Protocol = "UDP"
- s.Spec.Ports[0].TargetPort = intstr.FromInt(12345)
- },
- numErrs: 0,
- },
- {
- name: "valid 3",
- tweakSvc: func(s *api.Service) {
- s.Spec.Ports[0].TargetPort = intstr.FromString("http")
- },
- numErrs: 0,
- },
- {
- name: "valid cluster ip - none ",
- tweakSvc: func(s *api.Service) {
- s.Spec.ClusterIP = "None"
- },
- numErrs: 0,
- },
- {
- name: "valid cluster ip - empty",
- tweakSvc: func(s *api.Service) {
- s.Spec.ClusterIP = ""
- s.Spec.Ports[0].TargetPort = intstr.FromString("http")
- },
- numErrs: 0,
- },
- {
- name: "valid type - cluster",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeClusterIP
- },
- numErrs: 0,
- },
- {
- name: "valid type - loadbalancer",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeLoadBalancer
- },
- numErrs: 0,
- },
- {
- name: "valid type loadbalancer 2 ports",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeLoadBalancer
- s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", TargetPort: intstr.FromInt(12345)})
- },
- numErrs: 0,
- },
- {
- name: "valid external load balancer 2 ports",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeLoadBalancer
- s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", TargetPort: intstr.FromInt(12345)})
- },
- numErrs: 0,
- },
- {
- name: "duplicate nodeports",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeNodePort
- s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 1, Protocol: "TCP", NodePort: 1, TargetPort: intstr.FromInt(1)})
- s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "r", Port: 2, Protocol: "TCP", NodePort: 1, TargetPort: intstr.FromInt(2)})
- },
- numErrs: 1,
- },
- {
- name: "duplicate nodeports (different protocols)",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeNodePort
- s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 1, Protocol: "TCP", NodePort: 1, TargetPort: intstr.FromInt(1)})
- s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "r", Port: 2, Protocol: "UDP", NodePort: 1, TargetPort: intstr.FromInt(2)})
- },
- numErrs: 0,
- },
- {
- name: "valid type - cluster",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeClusterIP
- },
- numErrs: 0,
- },
- {
- name: "valid type - nodeport",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeNodePort
- },
- numErrs: 0,
- },
- {
- name: "valid type - loadbalancer",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeLoadBalancer
- },
- numErrs: 0,
- },
- {
- name: "valid type loadbalancer 2 ports",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeLoadBalancer
- s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", TargetPort: intstr.FromInt(12345)})
- },
- numErrs: 0,
- },
- {
- name: "valid type loadbalancer with NodePort",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeLoadBalancer
- s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", NodePort: 12345, TargetPort: intstr.FromInt(12345)})
- },
- numErrs: 0,
- },
- {
- name: "valid type=NodePort service with NodePort",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeNodePort
- s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", NodePort: 12345, TargetPort: intstr.FromInt(12345)})
- },
- numErrs: 0,
- },
- {
- name: "valid type=NodePort service without NodePort",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeNodePort
- s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", TargetPort: intstr.FromInt(12345)})
- },
- numErrs: 0,
- },
- {
- name: "valid cluster service without NodePort",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeClusterIP
- s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", TargetPort: intstr.FromInt(12345)})
- },
- numErrs: 0,
- },
- {
- name: "invalid cluster service with NodePort",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeClusterIP
- s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", NodePort: 12345, TargetPort: intstr.FromInt(12345)})
- },
- numErrs: 1,
- },
- {
- name: "invalid public service with duplicate NodePort",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeNodePort
- s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "p1", Port: 1, Protocol: "TCP", NodePort: 1, TargetPort: intstr.FromInt(1)})
- s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "p2", Port: 2, Protocol: "TCP", NodePort: 1, TargetPort: intstr.FromInt(2)})
- },
- numErrs: 1,
- },
- {
- name: "valid type=LoadBalancer",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeLoadBalancer
- s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", TargetPort: intstr.FromInt(12345)})
- },
- numErrs: 0,
- },
- {
- // For now we open firewalls, and its insecure if we open 10250, remove this
- // when we have better protections in place.
- name: "invalid port type=LoadBalancer",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeLoadBalancer
- s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "kubelet", Port: 10250, Protocol: "TCP", TargetPort: intstr.FromInt(12345)})
- },
- numErrs: 1,
- },
- {
- name: "valid LoadBalancer source range annotation",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeLoadBalancer
- s.Annotations[service.AnnotationLoadBalancerSourceRangesKey] = "1.2.3.4/8, 5.6.7.8/16"
- },
- numErrs: 0,
- },
- {
- name: "empty LoadBalancer source range annotation",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeLoadBalancer
- s.Annotations[service.AnnotationLoadBalancerSourceRangesKey] = ""
- },
- numErrs: 0,
- },
- {
- name: "invalid LoadBalancer source range annotation (hostname)",
- tweakSvc: func(s *api.Service) {
- s.Annotations[service.AnnotationLoadBalancerSourceRangesKey] = "foo.bar"
- },
- numErrs: 2,
- },
- {
- name: "invalid LoadBalancer source range annotation (invalid CIDR)",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeLoadBalancer
- s.Annotations[service.AnnotationLoadBalancerSourceRangesKey] = "1.2.3.4/33"
- },
- numErrs: 1,
- },
- {
- name: "invalid source range for non LoadBalancer type service",
- tweakSvc: func(s *api.Service) {
- s.Spec.LoadBalancerSourceRanges = []string{"1.2.3.4/8", "5.6.7.8/16"}
- },
- numErrs: 1,
- },
- {
- name: "valid LoadBalancer source range",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeLoadBalancer
- s.Spec.LoadBalancerSourceRanges = []string{"1.2.3.4/8", "5.6.7.8/16"}
- },
- numErrs: 0,
- },
- {
- name: "empty LoadBalancer source range",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeLoadBalancer
- s.Spec.LoadBalancerSourceRanges = []string{" "}
- },
- numErrs: 1,
- },
- {
- name: "invalid LoadBalancer source range",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeLoadBalancer
- s.Spec.LoadBalancerSourceRanges = []string{"foo.bar"}
- },
- numErrs: 1,
- },
- {
- name: "valid ExternalName",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeExternalName
- s.Spec.ClusterIP = ""
- s.Spec.ExternalName = "foo.bar.example.com"
- },
- numErrs: 0,
- },
- {
- name: "invalid ExternalName clusterIP (valid IP)",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeExternalName
- s.Spec.ClusterIP = "1.2.3.4"
- s.Spec.ExternalName = "foo.bar.example.com"
- },
- numErrs: 1,
- },
- {
- name: "invalid ExternalName clusterIP (None)",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeExternalName
- s.Spec.ClusterIP = "None"
- s.Spec.ExternalName = "foo.bar.example.com"
- },
- numErrs: 1,
- },
- {
- name: "invalid ExternalName (not a DNS name)",
- tweakSvc: func(s *api.Service) {
- s.Spec.Type = api.ServiceTypeExternalName
- s.Spec.ClusterIP = ""
- s.Spec.ExternalName = "-123"
- },
- numErrs: 1,
- },
- }
- for _, tc := range testCases {
- svc := makeValidService()
- tc.tweakSvc(&svc)
- errs := ValidateService(&svc)
- if len(errs) != tc.numErrs {
- t.Errorf("Unexpected error list for case %q: %v", tc.name, errs.ToAggregate())
- }
- }
- }
- func TestValidateReplicationControllerStatusUpdate(t *testing.T) {
- validSelector := map[string]string{"a": "b"}
- validPodTemplate := api.PodTemplate{
- Template: api.PodTemplateSpec{
- ObjectMeta: api.ObjectMeta{
- Labels: validSelector,
- },
- Spec: api.PodSpec{
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- },
- },
- }
- type rcUpdateTest struct {
- old api.ReplicationController
- update api.ReplicationController
- }
- successCases := []rcUpdateTest{
- {
- old: api.ReplicationController{
- ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- Status: api.ReplicationControllerStatus{
- Replicas: 2,
- },
- },
- update: api.ReplicationController{
- ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Replicas: 3,
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- Status: api.ReplicationControllerStatus{
- Replicas: 4,
- },
- },
- },
- }
- for _, successCase := range successCases {
- successCase.old.ObjectMeta.ResourceVersion = "1"
- successCase.update.ObjectMeta.ResourceVersion = "1"
- if errs := ValidateReplicationControllerStatusUpdate(&successCase.update, &successCase.old); len(errs) != 0 {
- t.Errorf("expected success: %v", errs)
- }
- }
- errorCases := map[string]rcUpdateTest{
- "negative replicas": {
- old: api.ReplicationController{
- ObjectMeta: api.ObjectMeta{Name: "", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- Status: api.ReplicationControllerStatus{
- Replicas: 3,
- },
- },
- update: api.ReplicationController{
- ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Replicas: 2,
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- Status: api.ReplicationControllerStatus{
- Replicas: -3,
- },
- },
- },
- }
- for testName, errorCase := range errorCases {
- if errs := ValidateReplicationControllerStatusUpdate(&errorCase.update, &errorCase.old); len(errs) == 0 {
- t.Errorf("expected failure: %s", testName)
- }
- }
- }
- func TestValidateReplicationControllerUpdate(t *testing.T) {
- validSelector := map[string]string{"a": "b"}
- validPodTemplate := api.PodTemplate{
- Template: api.PodTemplateSpec{
- ObjectMeta: api.ObjectMeta{
- Labels: validSelector,
- },
- Spec: api.PodSpec{
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- },
- },
- }
- readWriteVolumePodTemplate := api.PodTemplate{
- Template: api.PodTemplateSpec{
- ObjectMeta: api.ObjectMeta{
- Labels: validSelector,
- },
- Spec: api.PodSpec{
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- Volumes: []api.Volume{{Name: "gcepd", VolumeSource: api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{PDName: "my-PD", FSType: "ext4", Partition: 1, ReadOnly: false}}}},
- },
- },
- }
- invalidSelector := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"}
- invalidPodTemplate := api.PodTemplate{
- Template: api.PodTemplateSpec{
- Spec: api.PodSpec{
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- ObjectMeta: api.ObjectMeta{
- Labels: invalidSelector,
- },
- },
- }
- type rcUpdateTest struct {
- old api.ReplicationController
- update api.ReplicationController
- }
- successCases := []rcUpdateTest{
- {
- old: api.ReplicationController{
- ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
- update: api.ReplicationController{
- ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Replicas: 3,
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
- },
- {
- old: api.ReplicationController{
- ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
- update: api.ReplicationController{
- ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Replicas: 1,
- Selector: validSelector,
- Template: &readWriteVolumePodTemplate.Template,
- },
- },
- },
- }
- for _, successCase := range successCases {
- successCase.old.ObjectMeta.ResourceVersion = "1"
- successCase.update.ObjectMeta.ResourceVersion = "1"
- if errs := ValidateReplicationControllerUpdate(&successCase.update, &successCase.old); len(errs) != 0 {
- t.Errorf("expected success: %v", errs)
- }
- }
- errorCases := map[string]rcUpdateTest{
- "more than one read/write": {
- old: api.ReplicationController{
- ObjectMeta: api.ObjectMeta{Name: "", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
- update: api.ReplicationController{
- ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Replicas: 2,
- Selector: validSelector,
- Template: &readWriteVolumePodTemplate.Template,
- },
- },
- },
- "invalid selector": {
- old: api.ReplicationController{
- ObjectMeta: api.ObjectMeta{Name: "", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
- update: api.ReplicationController{
- ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Replicas: 2,
- Selector: invalidSelector,
- Template: &validPodTemplate.Template,
- },
- },
- },
- "invalid pod": {
- old: api.ReplicationController{
- ObjectMeta: api.ObjectMeta{Name: "", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
- update: api.ReplicationController{
- ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Replicas: 2,
- Selector: validSelector,
- Template: &invalidPodTemplate.Template,
- },
- },
- },
- "negative replicas": {
- old: api.ReplicationController{
- ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
- update: api.ReplicationController{
- ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Replicas: -1,
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
- },
- }
- for testName, errorCase := range errorCases {
- if errs := ValidateReplicationControllerUpdate(&errorCase.update, &errorCase.old); len(errs) == 0 {
- t.Errorf("expected failure: %s", testName)
- }
- }
- }
- func TestValidateReplicationController(t *testing.T) {
- validSelector := map[string]string{"a": "b"}
- validPodTemplate := api.PodTemplate{
- Template: api.PodTemplateSpec{
- ObjectMeta: api.ObjectMeta{
- Labels: validSelector,
- },
- Spec: api.PodSpec{
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- },
- },
- }
- readWriteVolumePodTemplate := api.PodTemplate{
- Template: api.PodTemplateSpec{
- ObjectMeta: api.ObjectMeta{
- Labels: validSelector,
- },
- Spec: api.PodSpec{
- Volumes: []api.Volume{{Name: "gcepd", VolumeSource: api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{PDName: "my-PD", FSType: "ext4", Partition: 1, ReadOnly: false}}}},
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- },
- },
- }
- invalidSelector := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"}
- invalidPodTemplate := api.PodTemplate{
- Template: api.PodTemplateSpec{
- Spec: api.PodSpec{
- RestartPolicy: api.RestartPolicyAlways,
- DNSPolicy: api.DNSClusterFirst,
- },
- ObjectMeta: api.ObjectMeta{
- Labels: invalidSelector,
- },
- },
- }
- successCases := []api.ReplicationController{
- {
- ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
- {
- ObjectMeta: api.ObjectMeta{Name: "abc-123", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
- {
- ObjectMeta: api.ObjectMeta{Name: "abc-123", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Replicas: 1,
- Selector: validSelector,
- Template: &readWriteVolumePodTemplate.Template,
- },
- },
- }
- for _, successCase := range successCases {
- if errs := ValidateReplicationController(&successCase); len(errs) != 0 {
- t.Errorf("expected success: %v", errs)
- }
- }
- errorCases := map[string]api.ReplicationController{
- "zero-length ID": {
- ObjectMeta: api.ObjectMeta{Name: "", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
- "missing-namespace": {
- ObjectMeta: api.ObjectMeta{Name: "abc-123"},
- Spec: api.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
- "empty selector": {
- ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Template: &validPodTemplate.Template,
- },
- },
- "selector_doesnt_match": {
- ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Selector: map[string]string{"foo": "bar"},
- Template: &validPodTemplate.Template,
- },
- },
- "invalid manifest": {
- ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Selector: validSelector,
- },
- },
- "read-write persistent disk with > 1 pod": {
- ObjectMeta: api.ObjectMeta{Name: "abc"},
- Spec: api.ReplicationControllerSpec{
- Replicas: 2,
- Selector: validSelector,
- Template: &readWriteVolumePodTemplate.Template,
- },
- },
- "negative_replicas": {
- ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault},
- Spec: api.ReplicationControllerSpec{
- Replicas: -1,
- Selector: validSelector,
- },
- },
- "invalid_label": {
- ObjectMeta: api.ObjectMeta{
- Name: "abc-123",
- Namespace: api.NamespaceDefault,
- Labels: map[string]string{
- "NoUppercaseOrSpecialCharsLike=Equals": "bar",
- },
- },
- Spec: api.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
- "invalid_label 2": {
- ObjectMeta: api.ObjectMeta{
- Name: "abc-123",
- Namespace: api.NamespaceDefault,
- Labels: map[string]string{
- "NoUppercaseOrSpecialCharsLike=Equals": "bar",
- },
- },
- Spec: api.ReplicationControllerSpec{
- Template: &invalidPodTemplate.Template,
- },
- },
- "invalid_annotation": {
- ObjectMeta: api.ObjectMeta{
- Name: "abc-123",
- Namespace: api.NamespaceDefault,
- Annotations: map[string]string{
- "NoUppercaseOrSpecialCharsLike=Equals": "bar",
- },
- },
- Spec: api.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
- "invalid restart policy 1": {
- ObjectMeta: api.ObjectMeta{
- Name: "abc-123",
- Namespace: api.NamespaceDefault,
- },
- Spec: api.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &api.PodTemplateSpec{
- Spec: api.PodSpec{
- RestartPolicy: api.RestartPolicyOnFailure,
- DNSPolicy: api.DNSClusterFirst,
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- },
- ObjectMeta: api.ObjectMeta{
- Labels: validSelector,
- },
- },
- },
- },
- "invalid restart policy 2": {
- ObjectMeta: api.ObjectMeta{
- Name: "abc-123",
- Namespace: api.NamespaceDefault,
- },
- Spec: api.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &api.PodTemplateSpec{
- Spec: api.PodSpec{
- RestartPolicy: api.RestartPolicyNever,
- DNSPolicy: api.DNSClusterFirst,
- Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
- },
- ObjectMeta: api.ObjectMeta{
- Labels: validSelector,
- },
- },
- },
- },
- }
- for k, v := range errorCases {
- errs := ValidateReplicationController(&v)
- if len(errs) == 0 {
- t.Errorf("expected failure for %s", k)
- }
- for i := range errs {
- field := errs[i].Field
- if !strings.HasPrefix(field, "spec.template.") &&
- field != "metadata.name" &&
- field != "metadata.namespace" &&
- field != "spec.selector" &&
- field != "spec.template" &&
- field != "GCEPersistentDisk.ReadOnly" &&
- field != "spec.replicas" &&
- field != "spec.template.labels" &&
- field != "metadata.annotations" &&
- field != "metadata.labels" &&
- field != "status.replicas" {
- t.Errorf("%s: missing prefix for: %v", k, errs[i])
- }
- }
- }
- }
- func TestValidateNode(t *testing.T) {
- validSelector := map[string]string{"a": "b"}
- invalidSelector := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"}
- successCases := []api.Node{
- {
- ObjectMeta: api.ObjectMeta{
- Name: "abc",
- Labels: validSelector,
- },
- Status: api.NodeStatus{
- Addresses: []api.NodeAddress{
- {Type: api.NodeLegacyHostIP, Address: "something"},
- },
- Capacity: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
- api.ResourceName("my.org/gpu"): resource.MustParse("10"),
- },
- },
- Spec: api.NodeSpec{
- ExternalID: "external",
- },
- },
- {
- ObjectMeta: api.ObjectMeta{
- Name: "abc",
- },
- Status: api.NodeStatus{
- Addresses: []api.NodeAddress{
- {Type: api.NodeLegacyHostIP, Address: "something"},
- },
- Capacity: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("0"),
- },
- },
- Spec: api.NodeSpec{
- ExternalID: "external",
- },
- },
- {
- ObjectMeta: api.ObjectMeta{
- Name: "dedicated-node1",
- // Add a valid taint to a node
- Annotations: map[string]string{
- api.TaintsAnnotationKey: `
- [{
- "key": "GPU",
- "value": "true",
- "effect": "NoSchedule"
- }]`,
- },
- },
- Status: api.NodeStatus{
- Addresses: []api.NodeAddress{
- {Type: api.NodeLegacyHostIP, Address: "something"},
- },
- Capacity: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("0"),
- },
- },
- Spec: api.NodeSpec{
- ExternalID: "external",
- },
- },
- {
- ObjectMeta: api.ObjectMeta{
- Name: "abc",
- Annotations: map[string]string{
- api.PreferAvoidPodsAnnotationKey: `
- {
- "preferAvoidPods": [
- {
- "podSignature": {
- "podController": {
- "apiVersion": "v1",
- "kind": "ReplicationController",
- "name": "foo",
- "uid": "abcdef123456",
- "controller": true
- }
- },
- "reason": "some reason",
- "message": "some message"
- }
- ]
- }`,
- },
- },
- Status: api.NodeStatus{
- Addresses: []api.NodeAddress{
- {Type: api.NodeLegacyHostIP, Address: "something"},
- },
- Capacity: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("0"),
- },
- },
- Spec: api.NodeSpec{
- ExternalID: "external",
- },
- },
- }
- for _, successCase := range successCases {
- if errs := ValidateNode(&successCase); len(errs) != 0 {
- t.Errorf("expected success: %v", errs)
- }
- }
- errorCases := map[string]api.Node{
- "zero-length Name": {
- ObjectMeta: api.ObjectMeta{
- Name: "",
- Labels: validSelector,
- },
- Status: api.NodeStatus{
- Addresses: []api.NodeAddress{},
- Capacity: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
- },
- },
- Spec: api.NodeSpec{
- ExternalID: "external",
- },
- },
- "invalid-labels": {
- ObjectMeta: api.ObjectMeta{
- Name: "abc-123",
- Labels: invalidSelector,
- },
- Status: api.NodeStatus{
- Capacity: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
- },
- },
- Spec: api.NodeSpec{
- ExternalID: "external",
- },
- },
- "missing-external-id": {
- ObjectMeta: api.ObjectMeta{
- Name: "abc-123",
- Labels: validSelector,
- },
- Status: api.NodeStatus{
- Capacity: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
- },
- },
- },
- "missing-taint-key": {
- ObjectMeta: api.ObjectMeta{
- Name: "dedicated-node1",
- // Add a taint with an empty key to a node
- Annotations: map[string]string{
- api.TaintsAnnotationKey: `
- [{
- "key": "",
- "value": "special-user-1",
- "effect": "NoSchedule"
- }]`,
- },
- },
- Spec: api.NodeSpec{
- ExternalID: "external",
- },
- },
- "bad-taint-key": {
- ObjectMeta: api.ObjectMeta{
- Name: "dedicated-node1",
- // Add a taint with an empty key to a node
- Annotations: map[string]string{
- api.TaintsAnnotationKey: `
- [{
- "key": "NoUppercaseOrSpecialCharsLike=Equals",
- "value": "special-user-1",
- "effect": "NoSchedule"
- }]`,
- },
- },
- Spec: api.NodeSpec{
- ExternalID: "external",
- },
- },
- "bad-taint-value": {
- ObjectMeta: api.ObjectMeta{
- Name: "dedicated-node2",
- Annotations: map[string]string{
- api.TaintsAnnotationKey: `
- [{
- "key": "dedicated",
- "value": "some\\bad\\value",
- "effect": "NoSchedule"
- }]`,
- },
- },
- Status: api.NodeStatus{
- Addresses: []api.NodeAddress{
- {Type: api.NodeLegacyHostIP, Address: "something"},
- },
- Capacity: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("0"),
- },
- },
- // Add a taint with an empty value to a node
- Spec: api.NodeSpec{
- ExternalID: "external",
- },
- },
- "missing-taint-effect": {
- ObjectMeta: api.ObjectMeta{
- Name: "dedicated-node3",
- // Add a taint with an empty effect to a node
- Annotations: map[string]string{
- api.TaintsAnnotationKey: `
- [{
- "key": "dedicated",
- "value": "special-user-3",
- "effect": ""
- }]`,
- },
- },
- Status: api.NodeStatus{
- Addresses: []api.NodeAddress{
- {Type: api.NodeLegacyHostIP, Address: "something"},
- },
- Capacity: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("0"),
- },
- },
- Spec: api.NodeSpec{
- ExternalID: "external",
- },
- },
- "invalide-taint-effect": {
- ObjectMeta: api.ObjectMeta{
- Name: "dedicated-node3",
- // Add a taint with an empty effect to a node
- Annotations: map[string]string{
- api.TaintsAnnotationKey: `
- [{
- "key": "dedicated",
- "value": "special-user-3",
- "effect": "NoExecute"
- }]`,
- },
- },
- Status: api.NodeStatus{
- Addresses: []api.NodeAddress{
- {Type: api.NodeLegacyHostIP, Address: "something"},
- },
- Capacity: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("0"),
- },
- },
- Spec: api.NodeSpec{
- ExternalID: "external",
- },
- },
- "duplicated-taints-with-same-key-effect": {
- ObjectMeta: api.ObjectMeta{
- Name: "dedicated-node1",
- // Add two taints to the node with the same key and effect; should be rejected.
- Annotations: map[string]string{
- api.TaintsAnnotationKey: `
- [{
- "key": "dedicated",
- "value": "special-user-1",
- "effect": "NoSchedule"
- }, {
- "key": "dedicated",
- "value": "special-user-2",
- "effect": "NoSchedule"
- }]`,
- },
- },
- Spec: api.NodeSpec{
- ExternalID: "external",
- },
- },
- "missing-podSignature": {
- ObjectMeta: api.ObjectMeta{
- Name: "abc-123",
- Annotations: map[string]string{
- api.PreferAvoidPodsAnnotationKey: `
- {
- "preferAvoidPods": [
- {
- "reason": "some reason",
- "message": "some message"
- }
- ]
- }`,
- },
- },
- Status: api.NodeStatus{
- Addresses: []api.NodeAddress{},
- Capacity: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("0"),
- },
- },
- Spec: api.NodeSpec{
- ExternalID: "external",
- },
- },
- "invalid-podController": {
- ObjectMeta: api.ObjectMeta{
- Name: "abc-123",
- Annotations: map[string]string{
- api.PreferAvoidPodsAnnotationKey: `
- {
- "preferAvoidPods": [
- {
- "podSignature": {
- "podController": {
- "apiVersion": "v1",
- "kind": "ReplicationController",
- "name": "foo",
- "uid": "abcdef123456",
- "controller": false
- }
- },
- "reason": "some reason",
- "message": "some message"
- }
- ]
- }`,
- },
- },
- Status: api.NodeStatus{
- Addresses: []api.NodeAddress{},
- Capacity: api.ResourceList{
- api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
- api.ResourceName(api.ResourceMemory): resource.MustParse("0"),
- },
- },
- Spec: api.NodeSpec{
- ExternalID: "external",
- },
- },
- }
- for k, v := range errorCases {
- errs := ValidateNode(&v)
- if len(errs) == 0 {
- t.Errorf("expected failure for %s", k)
- }
- for i := range errs {
- field := errs[i].Field
- expectedFields := map[string]bool{
- "metadata.name": true,
- "metadata.labels": true,
- "metadata.annotations": true,
- "metadata.namespace": true,
- "spec.externalID": true,
- "metadata.annotations.scheduler.alpha.kubernetes.io/taints[0].key": true,
- "metadata.annotations.scheduler.alpha.kubernetes.io/taints[0].value": true,
- "metadata.annotations.scheduler.alpha.kubernetes.io/taints[0].effect": true,
- "metadata.annotations.scheduler.alpha.kubernetes.io/preferAvoidPods[0].PodSignature": true,
- "metadata.annotations.scheduler.alpha.kubernetes.io/preferAvoidPods[0].PodSignature.PodController.Controller": true,
- }
- if val, ok := expectedFields[field]; ok {
- if !val {
- t.Errorf("%s: missing prefix for: %v", k, errs[i])
- }
- }
- }
- }
- }
- func TestValidateNodeUpdate(t *testing.T) {
- tests := []struct {
- oldNode api.Node
- node api.Node
- valid bool
- }{
- {api.Node{}, api.Node{}, true},
- {api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo"}},
- api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "bar"},
- }, false},
- {api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Labels: map[string]string{"foo": "bar"},
- },
- }, api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Labels: map[string]string{"foo": "baz"},
- },
- }, true},
- {api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- },
- }, api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Labels: map[string]string{"foo": "baz"},
- },
- }, true},
- {api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Labels: map[string]string{"bar": "foo"},
- },
- }, api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Labels: map[string]string{"foo": "baz"},
- },
- }, true},
- {api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- },
- Spec: api.NodeSpec{
- PodCIDR: "",
- },
- }, api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- },
- Spec: api.NodeSpec{
- PodCIDR: "192.168.0.0/16",
- },
- }, true},
- {api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- },
- Spec: api.NodeSpec{
- PodCIDR: "192.123.0.0/16",
- },
- }, api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- },
- Spec: api.NodeSpec{
- PodCIDR: "192.168.0.0/16",
- },
- }, false},
- {api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- },
- Status: api.NodeStatus{
- Capacity: api.ResourceList{
- api.ResourceCPU: resource.MustParse("10000"),
- api.ResourceMemory: resource.MustParse("100"),
- },
- },
- }, api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- },
- Status: api.NodeStatus{
- Capacity: api.ResourceList{
- api.ResourceCPU: resource.MustParse("100"),
- api.ResourceMemory: resource.MustParse("10000"),
- },
- },
- }, true},
- {api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Labels: map[string]string{"bar": "foo"},
- },
- Status: api.NodeStatus{
- Capacity: api.ResourceList{
- api.ResourceCPU: resource.MustParse("10000"),
- api.ResourceMemory: resource.MustParse("100"),
- },
- },
- }, api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Labels: map[string]string{"bar": "fooobaz"},
- },
- Status: api.NodeStatus{
- Capacity: api.ResourceList{
- api.ResourceCPU: resource.MustParse("100"),
- api.ResourceMemory: resource.MustParse("10000"),
- },
- },
- }, true},
- {api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Labels: map[string]string{"bar": "foo"},
- },
- Status: api.NodeStatus{
- Addresses: []api.NodeAddress{
- {Type: api.NodeLegacyHostIP, Address: "1.2.3.4"},
- },
- },
- }, api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Labels: map[string]string{"bar": "fooobaz"},
- },
- }, true},
- {api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Labels: map[string]string{"foo": "baz"},
- },
- }, api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Labels: map[string]string{"Foo": "baz"},
- },
- }, true},
- {api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- },
- Spec: api.NodeSpec{
- Unschedulable: false,
- },
- }, api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- },
- Spec: api.NodeSpec{
- Unschedulable: true,
- },
- }, true},
- {api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- },
- Spec: api.NodeSpec{
- Unschedulable: false,
- },
- }, api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- },
- Status: api.NodeStatus{
- Addresses: []api.NodeAddress{
- {Type: api.NodeExternalIP, Address: "1.1.1.1"},
- {Type: api.NodeExternalIP, Address: "1.1.1.1"},
- },
- },
- }, false},
- {api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- },
- Spec: api.NodeSpec{
- Unschedulable: false,
- },
- }, api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- },
- Status: api.NodeStatus{
- Addresses: []api.NodeAddress{
- {Type: api.NodeExternalIP, Address: "1.1.1.1"},
- {Type: api.NodeInternalIP, Address: "10.1.1.1"},
- },
- },
- }, true},
- {api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- },
- }, api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Annotations: map[string]string{
- api.PreferAvoidPodsAnnotationKey: `
- {
- "preferAvoidPods": [
- {
- "podSignature": {
- "podController": {
- "apiVersion": "v1",
- "kind": "ReplicationController",
- "name": "foo",
- "uid": "abcdef123456",
- "controller": true
- }
- },
- "reason": "some reason",
- "message": "some message"
- }
- ]
- }`,
- },
- },
- Spec: api.NodeSpec{
- Unschedulable: false,
- },
- }, true},
- {api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- },
- }, api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Annotations: map[string]string{
- api.PreferAvoidPodsAnnotationKey: `
- {
- "preferAvoidPods": [
- {
- "reason": "some reason",
- "message": "some message"
- }
- ]
- }`,
- },
- },
- }, false},
- {api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- },
- }, api.Node{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Annotations: map[string]string{
- api.PreferAvoidPodsAnnotationKey: `
- {
- "preferAvoidPods": [
- {
- "podSignature": {
- "podController": {
- "apiVersion": "v1",
- "kind": "ReplicationController",
- "name": "foo",
- "uid": "abcdef123456",
- "controller": false
- }
- },
- "reason": "some reason",
- "message": "some message"
- }
- ]
- }`,
- },
- },
- }, false},
- }
- for i, test := range tests {
- test.oldNode.ObjectMeta.ResourceVersion = "1"
- test.node.ObjectMeta.ResourceVersion = "1"
- errs := ValidateNodeUpdate(&test.node, &test.oldNode)
- if test.valid && len(errs) > 0 {
- t.Errorf("%d: Unexpected error: %v", i, errs)
- t.Logf("%#v vs %#v", test.oldNode.ObjectMeta, test.node.ObjectMeta)
- }
- if !test.valid && len(errs) == 0 {
- t.Errorf("%d: Unexpected non-error", i)
- }
- }
- }
- func TestValidateServiceUpdate(t *testing.T) {
- testCases := []struct {
- name string
- tweakSvc func(oldSvc, newSvc *api.Service) // given basic valid services, each test case can customize them
- numErrs int
- }{
- {
- name: "no change",
- tweakSvc: func(oldSvc, newSvc *api.Service) {
- // do nothing
- },
- numErrs: 0,
- },
- {
- name: "change name",
- tweakSvc: func(oldSvc, newSvc *api.Service) {
- newSvc.Name += "2"
- },
- numErrs: 1,
- },
- {
- name: "change namespace",
- tweakSvc: func(oldSvc, newSvc *api.Service) {
- newSvc.Namespace += "2"
- },
- numErrs: 1,
- },
- {
- name: "change label valid",
- tweakSvc: func(oldSvc, newSvc *api.Service) {
- newSvc.Labels["key"] = "other-value"
- },
- numErrs: 0,
- },
- {
- name: "add label",
- tweakSvc: func(oldSvc, newSvc *api.Service) {
- newSvc.Labels["key2"] = "value2"
- },
- numErrs: 0,
- },
- {
- name: "change cluster IP",
- tweakSvc: func(oldSvc, newSvc *api.Service) {
- oldSvc.Spec.ClusterIP = "1.2.3.4"
- newSvc.Spec.ClusterIP = "8.6.7.5"
- },
- numErrs: 1,
- },
- {
- name: "remove cluster IP",
- tweakSvc: func(oldSvc, newSvc *api.Service) {
- oldSvc.Spec.ClusterIP = "1.2.3.4"
- newSvc.Spec.ClusterIP = ""
- },
- numErrs: 1,
- },
- {
- name: "change affinity",
- tweakSvc: func(oldSvc, newSvc *api.Service) {
- newSvc.Spec.SessionAffinity = "ClientIP"
- },
- numErrs: 0,
- },
- {
- name: "remove affinity",
- tweakSvc: func(oldSvc, newSvc *api.Service) {
- newSvc.Spec.SessionAffinity = ""
- },
- numErrs: 1,
- },
- {
- name: "change type",
- tweakSvc: func(oldSvc, newSvc *api.Service) {
- newSvc.Spec.Type = api.ServiceTypeLoadBalancer
- },
- numErrs: 0,
- },
- {
- name: "remove type",
- tweakSvc: func(oldSvc, newSvc *api.Service) {
- newSvc.Spec.Type = ""
- },
- numErrs: 1,
- },
- {
- name: "change type -> nodeport",
- tweakSvc: func(oldSvc, newSvc *api.Service) {
- newSvc.Spec.Type = api.ServiceTypeNodePort
- },
- numErrs: 0,
- },
- }
- for _, tc := range testCases {
- oldSvc := makeValidService()
- newSvc := makeValidService()
- tc.tweakSvc(&oldSvc, &newSvc)
- errs := ValidateServiceUpdate(&newSvc, &oldSvc)
- if len(errs) != tc.numErrs {
- t.Errorf("Unexpected error list for case %q: %v", tc.name, errs.ToAggregate())
- }
- }
- }
- func TestValidateResourceNames(t *testing.T) {
- table := []struct {
- input string
- success bool
- expect string
- }{
- {"memory", true, ""},
- {"cpu", true, ""},
- {"network", false, ""},
- {"disk", false, ""},
- {"", false, ""},
- {".", false, ""},
- {"..", false, ""},
- {"my.favorite.app.co/12345", true, ""},
- {"my.favorite.app.co/_12345", false, ""},
- {"my.favorite.app.co/12345_", false, ""},
- {"kubernetes.io/..", false, ""},
- {"kubernetes.io/" + strings.Repeat("a", 63), true, ""},
- {"kubernetes.io/" + strings.Repeat("a", 64), false, ""},
- {"kubernetes.io//", false, ""},
- {"kubernetes.io", false, ""},
- {"kubernetes.io/will/not/work/", false, ""},
- }
- for k, item := range table {
- err := validateResourceName(item.input, field.NewPath("field"))
- if len(err) != 0 && item.success {
- t.Errorf("expected no failure for input %q", item.input)
- } else if len(err) == 0 && !item.success {
- t.Errorf("expected failure for input %q", item.input)
- for i := range err {
- detail := err[i].Detail
- if detail != "" && !strings.Contains(detail, item.expect) {
- t.Errorf("%d: expected error detail either empty or %s, got %s", k, item.expect, detail)
- }
- }
- }
- }
- }
- func getResourceList(cpu, memory string) api.ResourceList {
- res := api.ResourceList{}
- if cpu != "" {
- res[api.ResourceCPU] = resource.MustParse(cpu)
- }
- if memory != "" {
- res[api.ResourceMemory] = resource.MustParse(memory)
- }
- return res
- }
- func getStorageResourceList(storage string) api.ResourceList {
- res := api.ResourceList{}
- if storage != "" {
- res[api.ResourceStorage] = resource.MustParse(storage)
- }
- return res
- }
- func TestValidateLimitRange(t *testing.T) {
- successCases := []struct {
- name string
- spec api.LimitRangeSpec
- }{
- {
- name: "all-fields-valid",
- spec: api.LimitRangeSpec{
- Limits: []api.LimitRangeItem{
- {
- Type: api.LimitTypePod,
- Max: getResourceList("100m", "10000Mi"),
- Min: getResourceList("5m", "100Mi"),
- MaxLimitRequestRatio: getResourceList("10", ""),
- },
- {
- Type: api.LimitTypeContainer,
- Max: getResourceList("100m", "10000Mi"),
- Min: getResourceList("5m", "100Mi"),
- Default: getResourceList("50m", "500Mi"),
- DefaultRequest: getResourceList("10m", "200Mi"),
- MaxLimitRequestRatio: getResourceList("10", ""),
- },
- },
- },
- },
- {
- name: "all-fields-valid-big-numbers",
- spec: api.LimitRangeSpec{
- Limits: []api.LimitRangeItem{
- {
- Type: api.LimitTypeContainer,
- Max: getResourceList("100m", "10000T"),
- Min: getResourceList("5m", "100Mi"),
- Default: getResourceList("50m", "500Mi"),
- DefaultRequest: getResourceList("10m", "200Mi"),
- MaxLimitRequestRatio: getResourceList("10", ""),
- },
- },
- },
- },
- {
- name: "thirdparty-fields-all-valid-standard-container-resources",
- spec: api.LimitRangeSpec{
- Limits: []api.LimitRangeItem{
- {
- Type: "thirdparty.com/foo",
- Max: getResourceList("100m", "10000T"),
- Min: getResourceList("5m", "100Mi"),
- Default: getResourceList("50m", "500Mi"),
- DefaultRequest: getResourceList("10m", "200Mi"),
- MaxLimitRequestRatio: getResourceList("10", ""),
- },
- },
- },
- },
- {
- name: "thirdparty-fields-all-valid-storage-resources",
- spec: api.LimitRangeSpec{
- Limits: []api.LimitRangeItem{
- {
- Type: "thirdparty.com/foo",
- Max: getStorageResourceList("10000T"),
- Min: getStorageResourceList("100Mi"),
- Default: getStorageResourceList("500Mi"),
- DefaultRequest: getStorageResourceList("200Mi"),
- MaxLimitRequestRatio: getStorageResourceList(""),
- },
- },
- },
- },
- }
- for _, successCase := range successCases {
- limitRange := &api.LimitRange{ObjectMeta: api.ObjectMeta{Name: successCase.name, Namespace: "foo"}, Spec: successCase.spec}
- if errs := ValidateLimitRange(limitRange); len(errs) != 0 {
- t.Errorf("Case %v, unexpected error: %v", successCase.name, errs)
- }
- }
- errorCases := map[string]struct {
- R api.LimitRange
- D string
- }{
- "zero-length-name": {
- api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "", Namespace: "foo"}, Spec: api.LimitRangeSpec{}},
- "name or generateName is required",
- },
- "zero-length-namespace": {
- api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: ""}, Spec: api.LimitRangeSpec{}},
- "",
- },
- "invalid-name": {
- api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "^Invalid", Namespace: "foo"}, Spec: api.LimitRangeSpec{}},
- "must match the regex",
- },
- "invalid-namespace": {
- api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "^Invalid"}, Spec: api.LimitRangeSpec{}},
- "must match the regex",
- },
- "duplicate-limit-type": {
- api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: api.LimitRangeSpec{
- Limits: []api.LimitRangeItem{
- {
- Type: api.LimitTypePod,
- Max: getResourceList("100m", "10000m"),
- Min: getResourceList("0m", "100m"),
- },
- {
- Type: api.LimitTypePod,
- Min: getResourceList("0m", "100m"),
- },
- },
- }},
- "",
- },
- "default-limit-type-pod": {
- api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: api.LimitRangeSpec{
- Limits: []api.LimitRangeItem{
- {
- Type: api.LimitTypePod,
- Max: getResourceList("100m", "10000m"),
- Min: getResourceList("0m", "100m"),
- Default: getResourceList("10m", "100m"),
- },
- },
- }},
- "may not be specified when `type` is 'Pod'",
- },
- "default-request-limit-type-pod": {
- api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: api.LimitRangeSpec{
- Limits: []api.LimitRangeItem{
- {
- Type: api.LimitTypePod,
- Max: getResourceList("100m", "10000m"),
- Min: getResourceList("0m", "100m"),
- DefaultRequest: getResourceList("10m", "100m"),
- },
- },
- }},
- "may not be specified when `type` is 'Pod'",
- },
- "min value 100m is greater than max value 10m": {
- api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: api.LimitRangeSpec{
- Limits: []api.LimitRangeItem{
- {
- Type: api.LimitTypePod,
- Max: getResourceList("10m", ""),
- Min: getResourceList("100m", ""),
- },
- },
- }},
- "min value 100m is greater than max value 10m",
- },
- "invalid spec default outside range": {
- api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: api.LimitRangeSpec{
- Limits: []api.LimitRangeItem{
- {
- Type: api.LimitTypeContainer,
- Max: getResourceList("1", ""),
- Min: getResourceList("100m", ""),
- Default: getResourceList("2000m", ""),
- },
- },
- }},
- "default value 2 is greater than max value 1",
- },
- "invalid spec defaultrequest outside range": {
- api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: api.LimitRangeSpec{
- Limits: []api.LimitRangeItem{
- {
- Type: api.LimitTypeContainer,
- Max: getResourceList("1", ""),
- Min: getResourceList("100m", ""),
- DefaultRequest: getResourceList("2000m", ""),
- },
- },
- }},
- "default request value 2 is greater than max value 1",
- },
- "invalid spec defaultrequest more than default": {
- api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: api.LimitRangeSpec{
- Limits: []api.LimitRangeItem{
- {
- Type: api.LimitTypeContainer,
- Max: getResourceList("2", ""),
- Min: getResourceList("100m", ""),
- Default: getResourceList("500m", ""),
- DefaultRequest: getResourceList("800m", ""),
- },
- },
- }},
- "default request value 800m is greater than default limit value 500m",
- },
- "invalid spec maxLimitRequestRatio less than 1": {
- api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: api.LimitRangeSpec{
- Limits: []api.LimitRangeItem{
- {
- Type: api.LimitTypePod,
- MaxLimitRequestRatio: getResourceList("800m", ""),
- },
- },
- }},
- "ratio 800m is less than 1",
- },
- "invalid spec maxLimitRequestRatio greater than max/min": {
- api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: api.LimitRangeSpec{
- Limits: []api.LimitRangeItem{
- {
- Type: api.LimitTypeContainer,
- Max: getResourceList("", "2Gi"),
- Min: getResourceList("", "512Mi"),
- MaxLimitRequestRatio: getResourceList("", "10"),
- },
- },
- }},
- "ratio 10 is greater than max/min = 4.000000",
- },
- "invalid non standard limit type": {
- api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: api.LimitRangeSpec{
- Limits: []api.LimitRangeItem{
- {
- Type: "foo",
- Max: getStorageResourceList("10000T"),
- Min: getStorageResourceList("100Mi"),
- Default: getStorageResourceList("500Mi"),
- DefaultRequest: getStorageResourceList("200Mi"),
- MaxLimitRequestRatio: getStorageResourceList(""),
- },
- },
- }},
- "must be a standard limit type or fully qualified",
- },
- }
- for k, v := range errorCases {
- errs := ValidateLimitRange(&v.R)
- if len(errs) == 0 {
- t.Errorf("expected failure for %s", k)
- }
- for i := range errs {
- detail := errs[i].Detail
- if !strings.Contains(detail, v.D) {
- t.Errorf("[%s]: expected error detail either empty or %q, got %q", k, v.D, detail)
- }
- }
- }
- }
- func TestValidateResourceQuota(t *testing.T) {
- spec := api.ResourceQuotaSpec{
- Hard: api.ResourceList{
- api.ResourceCPU: resource.MustParse("100"),
- api.ResourceMemory: resource.MustParse("10000"),
- api.ResourceRequestsCPU: resource.MustParse("100"),
- api.ResourceRequestsMemory: resource.MustParse("10000"),
- api.ResourceLimitsCPU: resource.MustParse("100"),
- api.ResourceLimitsMemory: resource.MustParse("10000"),
- api.ResourcePods: resource.MustParse("10"),
- api.ResourceServices: resource.MustParse("0"),
- api.ResourceReplicationControllers: resource.MustParse("10"),
- api.ResourceQuotas: resource.MustParse("10"),
- api.ResourceConfigMaps: resource.MustParse("10"),
- api.ResourceSecrets: resource.MustParse("10"),
- },
- }
- terminatingSpec := api.ResourceQuotaSpec{
- Hard: api.ResourceList{
- api.ResourceCPU: resource.MustParse("100"),
- api.ResourceLimitsCPU: resource.MustParse("200"),
- },
- Scopes: []api.ResourceQuotaScope{api.ResourceQuotaScopeTerminating},
- }
- nonTerminatingSpec := api.ResourceQuotaSpec{
- Hard: api.ResourceList{
- api.ResourceCPU: resource.MustParse("100"),
- },
- Scopes: []api.ResourceQuotaScope{api.ResourceQuotaScopeNotTerminating},
- }
- bestEffortSpec := api.ResourceQuotaSpec{
- Hard: api.ResourceList{
- api.ResourcePods: resource.MustParse("100"),
- },
- Scopes: []api.ResourceQuotaScope{api.ResourceQuotaScopeBestEffort},
- }
- nonBestEffortSpec := api.ResourceQuotaSpec{
- Hard: api.ResourceList{
- api.ResourceCPU: resource.MustParse("100"),
- },
- Scopes: []api.ResourceQuotaScope{api.ResourceQuotaScopeNotBestEffort},
- }
- // storage is not yet supported as a quota tracked resource
- invalidQuotaResourceSpec := api.ResourceQuotaSpec{
- Hard: api.ResourceList{
- api.ResourceStorage: resource.MustParse("10"),
- },
- }
- negativeSpec := api.ResourceQuotaSpec{
- Hard: api.ResourceList{
- api.ResourceCPU: resource.MustParse("-100"),
- api.ResourceMemory: resource.MustParse("-10000"),
- api.ResourcePods: resource.MustParse("-10"),
- api.ResourceServices: resource.MustParse("-10"),
- api.ResourceReplicationControllers: resource.MustParse("-10"),
- api.ResourceQuotas: resource.MustParse("-10"),
- api.ResourceConfigMaps: resource.MustParse("-10"),
- api.ResourceSecrets: resource.MustParse("-10"),
- },
- }
- fractionalComputeSpec := api.ResourceQuotaSpec{
- Hard: api.ResourceList{
- api.ResourceCPU: resource.MustParse("100m"),
- },
- }
- fractionalPodSpec := api.ResourceQuotaSpec{
- Hard: api.ResourceList{
- api.ResourcePods: resource.MustParse(".1"),
- api.ResourceServices: resource.MustParse(".5"),
- api.ResourceReplicationControllers: resource.MustParse("1.25"),
- api.ResourceQuotas: resource.MustParse("2.5"),
- },
- }
- invalidTerminatingScopePairsSpec := api.ResourceQuotaSpec{
- Hard: api.ResourceList{
- api.ResourceCPU: resource.MustParse("100"),
- },
- Scopes: []api.ResourceQuotaScope{api.ResourceQuotaScopeTerminating, api.ResourceQuotaScopeNotTerminating},
- }
- invalidBestEffortScopePairsSpec := api.ResourceQuotaSpec{
- Hard: api.ResourceList{
- api.ResourcePods: resource.MustParse("100"),
- },
- Scopes: []api.ResourceQuotaScope{api.ResourceQuotaScopeBestEffort, api.ResourceQuotaScopeNotBestEffort},
- }
- invalidScopeNameSpec := api.ResourceQuotaSpec{
- Hard: api.ResourceList{
- api.ResourceCPU: resource.MustParse("100"),
- },
- Scopes: []api.ResourceQuotaScope{api.ResourceQuotaScope("foo")},
- }
- successCases := []api.ResourceQuota{
- {
- ObjectMeta: api.ObjectMeta{
- Name: "abc",
- Namespace: "foo",
- },
- Spec: spec,
- },
- {
- ObjectMeta: api.ObjectMeta{
- Name: "abc",
- Namespace: "foo",
- },
- Spec: fractionalComputeSpec,
- },
- {
- ObjectMeta: api.ObjectMeta{
- Name: "abc",
- Namespace: "foo",
- },
- Spec: terminatingSpec,
- },
- {
- ObjectMeta: api.ObjectMeta{
- Name: "abc",
- Namespace: "foo",
- },
- Spec: nonTerminatingSpec,
- },
- {
- ObjectMeta: api.ObjectMeta{
- Name: "abc",
- Namespace: "foo",
- },
- Spec: bestEffortSpec,
- },
- {
- ObjectMeta: api.ObjectMeta{
- Name: "abc",
- Namespace: "foo",
- },
- Spec: nonBestEffortSpec,
- },
- }
- for _, successCase := range successCases {
- if errs := ValidateResourceQuota(&successCase); len(errs) != 0 {
- t.Errorf("expected success: %v", errs)
- }
- }
- errorCases := map[string]struct {
- R api.ResourceQuota
- D string
- }{
- "zero-length Name": {
- api.ResourceQuota{ObjectMeta: api.ObjectMeta{Name: "", Namespace: "foo"}, Spec: spec},
- "name or generateName is required",
- },
- "zero-length Namespace": {
- api.ResourceQuota{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: ""}, Spec: spec},
- "",
- },
- "invalid Name": {
- api.ResourceQuota{ObjectMeta: api.ObjectMeta{Name: "^Invalid", Namespace: "foo"}, Spec: spec},
- "must match the regex",
- },
- "invalid Namespace": {
- api.ResourceQuota{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "^Invalid"}, Spec: spec},
- "must match the regex",
- },
- "negative-limits": {
- api.ResourceQuota{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: negativeSpec},
- isNegativeErrorMsg,
- },
- "fractional-api-resource": {
- api.ResourceQuota{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: fractionalPodSpec},
- isNotIntegerErrorMsg,
- },
- "invalid-quota-resource": {
- api.ResourceQuota{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: invalidQuotaResourceSpec},
- isInvalidQuotaResource,
- },
- "invalid-quota-terminating-pair": {
- api.ResourceQuota{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: invalidTerminatingScopePairsSpec},
- "conflicting scopes",
- },
- "invalid-quota-besteffort-pair": {
- api.ResourceQuota{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: invalidBestEffortScopePairsSpec},
- "conflicting scopes",
- },
- "invalid-quota-scope-name": {
- api.ResourceQuota{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: invalidScopeNameSpec},
- "unsupported scope",
- },
- }
- for k, v := range errorCases {
- errs := ValidateResourceQuota(&v.R)
- if len(errs) == 0 {
- t.Errorf("expected failure for %s", k)
- }
- for i := range errs {
- if !strings.Contains(errs[i].Detail, v.D) {
- t.Errorf("[%s]: expected error detail either empty or %s, got %s", k, v.D, errs[i].Detail)
- }
- }
- }
- }
- func TestValidateNamespace(t *testing.T) {
- validLabels := map[string]string{"a": "b"}
- invalidLabels := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"}
- successCases := []api.Namespace{
- {
- ObjectMeta: api.ObjectMeta{Name: "abc", Labels: validLabels},
- },
- {
- ObjectMeta: api.ObjectMeta{Name: "abc-123"},
- Spec: api.NamespaceSpec{
- Finalizers: []api.FinalizerName{"example.com/something", "example.com/other"},
- },
- },
- }
- for _, successCase := range successCases {
- if errs := ValidateNamespace(&successCase); len(errs) != 0 {
- t.Errorf("expected success: %v", errs)
- }
- }
- errorCases := map[string]struct {
- R api.Namespace
- D string
- }{
- "zero-length name": {
- api.Namespace{ObjectMeta: api.ObjectMeta{Name: ""}},
- "",
- },
- "defined-namespace": {
- api.Namespace{ObjectMeta: api.ObjectMeta{Name: "abc-123", Namespace: "makesnosense"}},
- "",
- },
- "invalid-labels": {
- api.Namespace{ObjectMeta: api.ObjectMeta{Name: "abc", Labels: invalidLabels}},
- "",
- },
- }
- for k, v := range errorCases {
- errs := ValidateNamespace(&v.R)
- if len(errs) == 0 {
- t.Errorf("expected failure for %s", k)
- }
- }
- }
- func TestValidateNamespaceFinalizeUpdate(t *testing.T) {
- tests := []struct {
- oldNamespace api.Namespace
- namespace api.Namespace
- valid bool
- }{
- {api.Namespace{}, api.Namespace{}, true},
- {api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo"}},
- api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo"},
- Spec: api.NamespaceSpec{
- Finalizers: []api.FinalizerName{"Foo"},
- },
- }, false},
- {api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo"},
- Spec: api.NamespaceSpec{
- Finalizers: []api.FinalizerName{"foo.com/bar"},
- },
- },
- api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo"},
- Spec: api.NamespaceSpec{
- Finalizers: []api.FinalizerName{"foo.com/bar", "what.com/bar"},
- },
- }, true},
- {api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "fooemptyfinalizer"},
- Spec: api.NamespaceSpec{
- Finalizers: []api.FinalizerName{"foo.com/bar"},
- },
- },
- api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "fooemptyfinalizer"},
- Spec: api.NamespaceSpec{
- Finalizers: []api.FinalizerName{"", "foo.com/bar", "what.com/bar"},
- },
- }, false},
- }
- for i, test := range tests {
- test.namespace.ObjectMeta.ResourceVersion = "1"
- test.oldNamespace.ObjectMeta.ResourceVersion = "1"
- errs := ValidateNamespaceFinalizeUpdate(&test.namespace, &test.oldNamespace)
- if test.valid && len(errs) > 0 {
- t.Errorf("%d: Unexpected error: %v", i, errs)
- t.Logf("%#v vs %#v", test.oldNamespace, test.namespace)
- }
- if !test.valid && len(errs) == 0 {
- t.Errorf("%d: Unexpected non-error", i)
- }
- }
- }
- func TestValidateNamespaceStatusUpdate(t *testing.T) {
- now := unversioned.Now()
- tests := []struct {
- oldNamespace api.Namespace
- namespace api.Namespace
- valid bool
- }{
- {api.Namespace{}, api.Namespace{
- Status: api.NamespaceStatus{
- Phase: api.NamespaceActive,
- },
- }, true},
- // Cannot set deletionTimestamp via status update
- {api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo"}},
- api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- DeletionTimestamp: &now},
- Status: api.NamespaceStatus{
- Phase: api.NamespaceTerminating,
- },
- }, false},
- // Can update phase via status update
- {api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- DeletionTimestamp: &now}},
- api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- DeletionTimestamp: &now},
- Status: api.NamespaceStatus{
- Phase: api.NamespaceTerminating,
- },
- }, true},
- {api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo"}},
- api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo"},
- Status: api.NamespaceStatus{
- Phase: api.NamespaceTerminating,
- },
- }, false},
- {api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo"}},
- api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "bar"},
- Status: api.NamespaceStatus{
- Phase: api.NamespaceTerminating,
- },
- }, false},
- }
- for i, test := range tests {
- test.namespace.ObjectMeta.ResourceVersion = "1"
- test.oldNamespace.ObjectMeta.ResourceVersion = "1"
- errs := ValidateNamespaceStatusUpdate(&test.namespace, &test.oldNamespace)
- if test.valid && len(errs) > 0 {
- t.Errorf("%d: Unexpected error: %v", i, errs)
- t.Logf("%#v vs %#v", test.oldNamespace.ObjectMeta, test.namespace.ObjectMeta)
- }
- if !test.valid && len(errs) == 0 {
- t.Errorf("%d: Unexpected non-error", i)
- }
- }
- }
- func TestValidateNamespaceUpdate(t *testing.T) {
- tests := []struct {
- oldNamespace api.Namespace
- namespace api.Namespace
- valid bool
- }{
- {api.Namespace{}, api.Namespace{}, true},
- {api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo1"}},
- api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "bar1"},
- }, false},
- {api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo2",
- Labels: map[string]string{"foo": "bar"},
- },
- }, api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo2",
- Labels: map[string]string{"foo": "baz"},
- },
- }, true},
- {api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo3",
- },
- }, api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo3",
- Labels: map[string]string{"foo": "baz"},
- },
- }, true},
- {api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo4",
- Labels: map[string]string{"bar": "foo"},
- },
- }, api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo4",
- Labels: map[string]string{"foo": "baz"},
- },
- }, true},
- {api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo5",
- Labels: map[string]string{"foo": "baz"},
- },
- }, api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo5",
- Labels: map[string]string{"Foo": "baz"},
- },
- }, true},
- {api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo6",
- Labels: map[string]string{"foo": "baz"},
- },
- }, api.Namespace{
- ObjectMeta: api.ObjectMeta{
- Name: "foo6",
- Labels: map[string]string{"Foo": "baz"},
- },
- Spec: api.NamespaceSpec{
- Finalizers: []api.FinalizerName{"kubernetes"},
- },
- Status: api.NamespaceStatus{
- Phase: api.NamespaceTerminating,
- },
- }, true},
- }
- for i, test := range tests {
- test.namespace.ObjectMeta.ResourceVersion = "1"
- test.oldNamespace.ObjectMeta.ResourceVersion = "1"
- errs := ValidateNamespaceUpdate(&test.namespace, &test.oldNamespace)
- if test.valid && len(errs) > 0 {
- t.Errorf("%d: Unexpected error: %v", i, errs)
- t.Logf("%#v vs %#v", test.oldNamespace.ObjectMeta, test.namespace.ObjectMeta)
- }
- if !test.valid && len(errs) == 0 {
- t.Errorf("%d: Unexpected non-error", i)
- }
- }
- }
- func TestValidateSecret(t *testing.T) {
- // Opaque secret validation
- validSecret := func() api.Secret {
- return api.Secret{
- ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "bar"},
- Data: map[string][]byte{
- "data-1": []byte("bar"),
- },
- }
- }
- var (
- emptyName = validSecret()
- invalidName = validSecret()
- emptyNs = validSecret()
- invalidNs = validSecret()
- overMaxSize = validSecret()
- invalidKey = validSecret()
- leadingDotKey = validSecret()
- dotKey = validSecret()
- doubleDotKey = validSecret()
- )
- emptyName.Name = ""
- invalidName.Name = "NoUppercaseOrSpecialCharsLike=Equals"
- emptyNs.Namespace = ""
- invalidNs.Namespace = "NoUppercaseOrSpecialCharsLike=Equals"
- overMaxSize.Data = map[string][]byte{
- "over": make([]byte, api.MaxSecretSize+1),
- }
- invalidKey.Data["a*b"] = []byte("whoops")
- leadingDotKey.Data[".key"] = []byte("bar")
- dotKey.Data["."] = []byte("bar")
- doubleDotKey.Data[".."] = []byte("bar")
- // kubernetes.io/service-account-token secret validation
- validServiceAccountTokenSecret := func() api.Secret {
- return api.Secret{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Namespace: "bar",
- Annotations: map[string]string{
- api.ServiceAccountNameKey: "foo",
- },
- },
- Type: api.SecretTypeServiceAccountToken,
- Data: map[string][]byte{
- "data-1": []byte("bar"),
- },
- }
- }
- var (
- emptyTokenAnnotation = validServiceAccountTokenSecret()
- missingTokenAnnotation = validServiceAccountTokenSecret()
- missingTokenAnnotations = validServiceAccountTokenSecret()
- )
- emptyTokenAnnotation.Annotations[api.ServiceAccountNameKey] = ""
- delete(missingTokenAnnotation.Annotations, api.ServiceAccountNameKey)
- missingTokenAnnotations.Annotations = nil
- tests := map[string]struct {
- secret api.Secret
- valid bool
- }{
- "valid": {validSecret(), true},
- "empty name": {emptyName, false},
- "invalid name": {invalidName, false},
- "empty namespace": {emptyNs, false},
- "invalid namespace": {invalidNs, false},
- "over max size": {overMaxSize, false},
- "invalid key": {invalidKey, false},
- "valid service-account-token secret": {validServiceAccountTokenSecret(), true},
- "empty service-account-token annotation": {emptyTokenAnnotation, false},
- "missing service-account-token annotation": {missingTokenAnnotation, false},
- "missing service-account-token annotations": {missingTokenAnnotations, false},
- "leading dot key": {leadingDotKey, true},
- "dot key": {dotKey, false},
- "double dot key": {doubleDotKey, false},
- }
- for name, tc := range tests {
- errs := ValidateSecret(&tc.secret)
- if tc.valid && len(errs) > 0 {
- t.Errorf("%v: Unexpected error: %v", name, errs)
- }
- if !tc.valid && len(errs) == 0 {
- t.Errorf("%v: Unexpected non-error", name)
- }
- }
- }
- func TestValidateDockerConfigSecret(t *testing.T) {
- validDockerSecret := func() api.Secret {
- return api.Secret{
- ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "bar"},
- Type: api.SecretTypeDockercfg,
- Data: map[string][]byte{
- api.DockerConfigKey: []byte(`{"https://index.docker.io/v1/": {"auth": "Y2x1ZWRyb29sZXIwMDAxOnBhc3N3b3Jk","email": "fake@example.com"}}`),
- },
- }
- }
- validDockerSecret2 := func() api.Secret {
- return api.Secret{
- ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "bar"},
- Type: api.SecretTypeDockerConfigJson,
- Data: map[string][]byte{
- api.DockerConfigJsonKey: []byte(`{"auths":{"https://index.docker.io/v1/": {"auth": "Y2x1ZWRyb29sZXIwMDAxOnBhc3N3b3Jk","email": "fake@example.com"}}}`),
- },
- }
- }
- var (
- missingDockerConfigKey = validDockerSecret()
- emptyDockerConfigKey = validDockerSecret()
- invalidDockerConfigKey = validDockerSecret()
- missingDockerConfigKey2 = validDockerSecret2()
- emptyDockerConfigKey2 = validDockerSecret2()
- invalidDockerConfigKey2 = validDockerSecret2()
- )
- delete(missingDockerConfigKey.Data, api.DockerConfigKey)
- emptyDockerConfigKey.Data[api.DockerConfigKey] = []byte("")
- invalidDockerConfigKey.Data[api.DockerConfigKey] = []byte("bad")
- delete(missingDockerConfigKey2.Data, api.DockerConfigJsonKey)
- emptyDockerConfigKey2.Data[api.DockerConfigJsonKey] = []byte("")
- invalidDockerConfigKey2.Data[api.DockerConfigJsonKey] = []byte("bad")
- tests := map[string]struct {
- secret api.Secret
- valid bool
- }{
- "valid dockercfg": {validDockerSecret(), true},
- "missing dockercfg": {missingDockerConfigKey, false},
- "empty dockercfg": {emptyDockerConfigKey, false},
- "invalid dockercfg": {invalidDockerConfigKey, false},
- "valid config.json": {validDockerSecret2(), true},
- "missing config.json": {missingDockerConfigKey2, false},
- "empty config.json": {emptyDockerConfigKey2, false},
- "invalid config.json": {invalidDockerConfigKey2, false},
- }
- for name, tc := range tests {
- errs := ValidateSecret(&tc.secret)
- if tc.valid && len(errs) > 0 {
- t.Errorf("%v: Unexpected error: %v", name, errs)
- }
- if !tc.valid && len(errs) == 0 {
- t.Errorf("%v: Unexpected non-error", name)
- }
- }
- }
- func TestValidateBasicAuthSecret(t *testing.T) {
- validBasicAuthSecret := func() api.Secret {
- return api.Secret{
- ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "bar"},
- Type: api.SecretTypeBasicAuth,
- Data: map[string][]byte{
- api.BasicAuthUsernameKey: []byte("username"),
- api.BasicAuthPasswordKey: []byte("password"),
- },
- }
- }
- var (
- missingBasicAuthUsernamePasswordKeys = validBasicAuthSecret()
- // invalidBasicAuthUsernamePasswordKey = validBasicAuthSecret()
- // emptyBasicAuthUsernameKey = validBasicAuthSecret()
- // emptyBasicAuthPasswordKey = validBasicAuthSecret()
- )
- delete(missingBasicAuthUsernamePasswordKeys.Data, api.BasicAuthUsernameKey)
- delete(missingBasicAuthUsernamePasswordKeys.Data, api.BasicAuthPasswordKey)
- // invalidBasicAuthUsernamePasswordKey.Data[api.BasicAuthUsernameKey] = []byte("bad")
- // invalidBasicAuthUsernamePasswordKey.Data[api.BasicAuthPasswordKey] = []byte("bad")
- // emptyBasicAuthUsernameKey.Data[api.BasicAuthUsernameKey] = []byte("")
- // emptyBasicAuthPasswordKey.Data[api.BasicAuthPasswordKey] = []byte("")
- tests := map[string]struct {
- secret api.Secret
- valid bool
- }{
- "valid": {validBasicAuthSecret(), true},
- "missing username and password": {missingBasicAuthUsernamePasswordKeys, false},
- // "invalid username and password": {invalidBasicAuthUsernamePasswordKey, false},
- // "empty username": {emptyBasicAuthUsernameKey, false},
- // "empty password": {emptyBasicAuthPasswordKey, false},
- }
- for name, tc := range tests {
- errs := ValidateSecret(&tc.secret)
- if tc.valid && len(errs) > 0 {
- t.Errorf("%v: Unexpected error: %v", name, errs)
- }
- if !tc.valid && len(errs) == 0 {
- t.Errorf("%v: Unexpected non-error", name)
- }
- }
- }
- func TestValidateSSHAuthSecret(t *testing.T) {
- validSSHAuthSecret := func() api.Secret {
- return api.Secret{
- ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "bar"},
- Type: api.SecretTypeSSHAuth,
- Data: map[string][]byte{
- api.SSHAuthPrivateKey: []byte("foo-bar-baz"),
- },
- }
- }
- missingSSHAuthPrivateKey := validSSHAuthSecret()
- delete(missingSSHAuthPrivateKey.Data, api.SSHAuthPrivateKey)
- tests := map[string]struct {
- secret api.Secret
- valid bool
- }{
- "valid": {validSSHAuthSecret(), true},
- "missing private key": {missingSSHAuthPrivateKey, false},
- }
- for name, tc := range tests {
- errs := ValidateSecret(&tc.secret)
- if tc.valid && len(errs) > 0 {
- t.Errorf("%v: Unexpected error: %v", name, errs)
- }
- if !tc.valid && len(errs) == 0 {
- t.Errorf("%v: Unexpected non-error", name)
- }
- }
- }
- func TestValidateEndpoints(t *testing.T) {
- successCases := map[string]api.Endpoints{
- "simple endpoint": {
- ObjectMeta: api.ObjectMeta{Name: "mysvc", Namespace: "namespace"},
- Subsets: []api.EndpointSubset{
- {
- Addresses: []api.EndpointAddress{{IP: "10.10.1.1"}, {IP: "10.10.2.2"}},
- Ports: []api.EndpointPort{{Name: "a", Port: 8675, Protocol: "TCP"}, {Name: "b", Port: 309, Protocol: "TCP"}},
- },
- {
- Addresses: []api.EndpointAddress{{IP: "10.10.3.3"}},
- Ports: []api.EndpointPort{{Name: "a", Port: 93, Protocol: "TCP"}, {Name: "b", Port: 76, Protocol: "TCP"}},
- },
- },
- },
- "empty subsets": {
- ObjectMeta: api.ObjectMeta{Name: "mysvc", Namespace: "namespace"},
- },
- "no name required for singleton port": {
- ObjectMeta: api.ObjectMeta{Name: "mysvc", Namespace: "namespace"},
- Subsets: []api.EndpointSubset{
- {
- Addresses: []api.EndpointAddress{{IP: "10.10.1.1"}},
- Ports: []api.EndpointPort{{Port: 8675, Protocol: "TCP"}},
- },
- },
- },
- }
- for k, v := range successCases {
- if errs := ValidateEndpoints(&v); len(errs) != 0 {
- t.Errorf("Expected success for %s, got %v", k, errs)
- }
- }
- errorCases := map[string]struct {
- endpoints api.Endpoints
- errorType field.ErrorType
- errorDetail string
- }{
- "missing namespace": {
- endpoints: api.Endpoints{ObjectMeta: api.ObjectMeta{Name: "mysvc"}},
- errorType: "FieldValueRequired",
- },
- "missing name": {
- endpoints: api.Endpoints{ObjectMeta: api.ObjectMeta{Namespace: "namespace"}},
- errorType: "FieldValueRequired",
- },
- "invalid namespace": {
- endpoints: api.Endpoints{ObjectMeta: api.ObjectMeta{Name: "mysvc", Namespace: "no@#invalid.;chars\"allowed"}},
- errorType: "FieldValueInvalid",
- errorDetail: "must match the regex",
- },
- "invalid name": {
- endpoints: api.Endpoints{ObjectMeta: api.ObjectMeta{Name: "-_Invliad^&Characters", Namespace: "namespace"}},
- errorType: "FieldValueInvalid",
- errorDetail: "must match the regex",
- },
- "empty addresses": {
- endpoints: api.Endpoints{
- ObjectMeta: api.ObjectMeta{Name: "mysvc", Namespace: "namespace"},
- Subsets: []api.EndpointSubset{
- {
- Ports: []api.EndpointPort{{Name: "a", Port: 93, Protocol: "TCP"}},
- },
- },
- },
- errorType: "FieldValueRequired",
- },
- "empty ports": {
- endpoints: api.Endpoints{
- ObjectMeta: api.ObjectMeta{Name: "mysvc", Namespace: "namespace"},
- Subsets: []api.EndpointSubset{
- {
- Addresses: []api.EndpointAddress{{IP: "10.10.3.3"}},
- },
- },
- },
- errorType: "FieldValueRequired",
- },
- "invalid IP": {
- endpoints: api.Endpoints{
- ObjectMeta: api.ObjectMeta{Name: "mysvc", Namespace: "namespace"},
- Subsets: []api.EndpointSubset{
- {
- Addresses: []api.EndpointAddress{{IP: "[2001:0db8:85a3:0042:1000:8a2e:0370:7334]"}},
- Ports: []api.EndpointPort{{Name: "a", Port: 93, Protocol: "TCP"}},
- },
- },
- },
- errorType: "FieldValueInvalid",
- errorDetail: "must be a valid IP address",
- },
- "Multiple ports, one without name": {
- endpoints: api.Endpoints{
- ObjectMeta: api.ObjectMeta{Name: "mysvc", Namespace: "namespace"},
- Subsets: []api.EndpointSubset{
- {
- Addresses: []api.EndpointAddress{{IP: "10.10.1.1"}},
- Ports: []api.EndpointPort{{Port: 8675, Protocol: "TCP"}, {Name: "b", Port: 309, Protocol: "TCP"}},
- },
- },
- },
- errorType: "FieldValueRequired",
- },
- "Invalid port number": {
- endpoints: api.Endpoints{
- ObjectMeta: api.ObjectMeta{Name: "mysvc", Namespace: "namespace"},
- Subsets: []api.EndpointSubset{
- {
- Addresses: []api.EndpointAddress{{IP: "10.10.1.1"}},
- Ports: []api.EndpointPort{{Name: "a", Port: 66000, Protocol: "TCP"}},
- },
- },
- },
- errorType: "FieldValueInvalid",
- errorDetail: "between",
- },
- "Invalid protocol": {
- endpoints: api.Endpoints{
- ObjectMeta: api.ObjectMeta{Name: "mysvc", Namespace: "namespace"},
- Subsets: []api.EndpointSubset{
- {
- Addresses: []api.EndpointAddress{{IP: "10.10.1.1"}},
- Ports: []api.EndpointPort{{Name: "a", Port: 93, Protocol: "Protocol"}},
- },
- },
- },
- errorType: "FieldValueNotSupported",
- },
- "Address missing IP": {
- endpoints: api.Endpoints{
- ObjectMeta: api.ObjectMeta{Name: "mysvc", Namespace: "namespace"},
- Subsets: []api.EndpointSubset{
- {
- Addresses: []api.EndpointAddress{{}},
- Ports: []api.EndpointPort{{Name: "a", Port: 93, Protocol: "TCP"}},
- },
- },
- },
- errorType: "FieldValueInvalid",
- errorDetail: "must be a valid IP address",
- },
- "Port missing number": {
- endpoints: api.Endpoints{
- ObjectMeta: api.ObjectMeta{Name: "mysvc", Namespace: "namespace"},
- Subsets: []api.EndpointSubset{
- {
- Addresses: []api.EndpointAddress{{IP: "10.10.1.1"}},
- Ports: []api.EndpointPort{{Name: "a", Protocol: "TCP"}},
- },
- },
- },
- errorType: "FieldValueInvalid",
- errorDetail: "between",
- },
- "Port missing protocol": {
- endpoints: api.Endpoints{
- ObjectMeta: api.ObjectMeta{Name: "mysvc", Namespace: "namespace"},
- Subsets: []api.EndpointSubset{
- {
- Addresses: []api.EndpointAddress{{IP: "10.10.1.1"}},
- Ports: []api.EndpointPort{{Name: "a", Port: 93}},
- },
- },
- },
- errorType: "FieldValueRequired",
- },
- "Address is loopback": {
- endpoints: api.Endpoints{
- ObjectMeta: api.ObjectMeta{Name: "mysvc", Namespace: "namespace"},
- Subsets: []api.EndpointSubset{
- {
- Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}},
- Ports: []api.EndpointPort{{Name: "p", Port: 93, Protocol: "TCP"}},
- },
- },
- },
- errorType: "FieldValueInvalid",
- errorDetail: "loopback",
- },
- "Address is link-local": {
- endpoints: api.Endpoints{
- ObjectMeta: api.ObjectMeta{Name: "mysvc", Namespace: "namespace"},
- Subsets: []api.EndpointSubset{
- {
- Addresses: []api.EndpointAddress{{IP: "169.254.169.254"}},
- Ports: []api.EndpointPort{{Name: "p", Port: 93, Protocol: "TCP"}},
- },
- },
- },
- errorType: "FieldValueInvalid",
- errorDetail: "link-local",
- },
- "Address is link-local multicast": {
- endpoints: api.Endpoints{
- ObjectMeta: api.ObjectMeta{Name: "mysvc", Namespace: "namespace"},
- Subsets: []api.EndpointSubset{
- {
- Addresses: []api.EndpointAddress{{IP: "224.0.0.1"}},
- Ports: []api.EndpointPort{{Name: "p", Port: 93, Protocol: "TCP"}},
- },
- },
- },
- errorType: "FieldValueInvalid",
- errorDetail: "link-local multicast",
- },
- }
- for k, v := range errorCases {
- if errs := ValidateEndpoints(&v.endpoints); len(errs) == 0 || errs[0].Type != v.errorType || !strings.Contains(errs[0].Detail, v.errorDetail) {
- t.Errorf("[%s] Expected error type %s with detail %q, got %v", k, v.errorType, v.errorDetail, errs)
- }
- }
- }
- func TestValidateTLSSecret(t *testing.T) {
- successCases := map[string]api.Secret{
- "emtpy certificate chain": {
- ObjectMeta: api.ObjectMeta{Name: "tls-cert", Namespace: "namespace"},
- Data: map[string][]byte{
- api.TLSCertKey: []byte("public key"),
- api.TLSPrivateKeyKey: []byte("private key"),
- },
- },
- }
- for k, v := range successCases {
- if errs := ValidateSecret(&v); len(errs) != 0 {
- t.Errorf("Expected success for %s, got %v", k, errs)
- }
- }
- errorCases := map[string]struct {
- secrets api.Secret
- errorType field.ErrorType
- errorDetail string
- }{
- "missing public key": {
- secrets: api.Secret{
- ObjectMeta: api.ObjectMeta{Name: "tls-cert"},
- Data: map[string][]byte{
- api.TLSCertKey: []byte("public key"),
- },
- },
- errorType: "FieldValueRequired",
- },
- "missing private key": {
- secrets: api.Secret{
- ObjectMeta: api.ObjectMeta{Name: "tls-cert"},
- Data: map[string][]byte{
- api.TLSCertKey: []byte("public key"),
- },
- },
- errorType: "FieldValueRequired",
- },
- }
- for k, v := range errorCases {
- if errs := ValidateSecret(&v.secrets); len(errs) == 0 || errs[0].Type != v.errorType || !strings.Contains(errs[0].Detail, v.errorDetail) {
- t.Errorf("[%s] Expected error type %s with detail %q, got %v", k, v.errorType, v.errorDetail, errs)
- }
- }
- }
- func TestValidateSecurityContext(t *testing.T) {
- priv := false
- var runAsUser int64 = 1
- fullValidSC := func() *api.SecurityContext {
- return &api.SecurityContext{
- Privileged: &priv,
- Capabilities: &api.Capabilities{
- Add: []api.Capability{"foo"},
- Drop: []api.Capability{"bar"},
- },
- SELinuxOptions: &api.SELinuxOptions{
- User: "user",
- Role: "role",
- Type: "type",
- Level: "level",
- },
- RunAsUser: &runAsUser,
- }
- }
- //setup data
- allSettings := fullValidSC()
- noCaps := fullValidSC()
- noCaps.Capabilities = nil
- noSELinux := fullValidSC()
- noSELinux.SELinuxOptions = nil
- noPrivRequest := fullValidSC()
- noPrivRequest.Privileged = nil
- noRunAsUser := fullValidSC()
- noRunAsUser.RunAsUser = nil
- successCases := map[string]struct {
- sc *api.SecurityContext
- }{
- "all settings": {allSettings},
- "no capabilities": {noCaps},
- "no selinux": {noSELinux},
- "no priv request": {noPrivRequest},
- "no run as user": {noRunAsUser},
- }
- for k, v := range successCases {
- if errs := ValidateSecurityContext(v.sc, field.NewPath("field")); len(errs) != 0 {
- t.Errorf("[%s] Expected success, got %v", k, errs)
- }
- }
- privRequestWithGlobalDeny := fullValidSC()
- requestPrivileged := true
- privRequestWithGlobalDeny.Privileged = &requestPrivileged
- negativeRunAsUser := fullValidSC()
- var negativeUser int64 = -1
- negativeRunAsUser.RunAsUser = &negativeUser
- errorCases := map[string]struct {
- sc *api.SecurityContext
- errorType field.ErrorType
- errorDetail string
- }{
- "request privileged when capabilities forbids": {
- sc: privRequestWithGlobalDeny,
- errorType: "FieldValueForbidden",
- errorDetail: "disallowed by policy",
- },
- "negative RunAsUser": {
- sc: negativeRunAsUser,
- errorType: "FieldValueInvalid",
- errorDetail: isNegativeErrorMsg,
- },
- }
- for k, v := range errorCases {
- if errs := ValidateSecurityContext(v.sc, field.NewPath("field")); len(errs) == 0 || errs[0].Type != v.errorType || !strings.Contains(errs[0].Detail, v.errorDetail) {
- t.Errorf("[%s] Expected error type %q with detail %q, got %v", k, v.errorType, v.errorDetail, errs)
- }
- }
- }
- func fakeValidSecurityContext(priv bool) *api.SecurityContext {
- return &api.SecurityContext{
- Privileged: &priv,
- }
- }
- func TestValidPodLogOptions(t *testing.T) {
- now := unversioned.Now()
- negative := int64(-1)
- zero := int64(0)
- positive := int64(1)
- tests := []struct {
- opt api.PodLogOptions
- errs int
- }{
- {api.PodLogOptions{}, 0},
- {api.PodLogOptions{Previous: true}, 0},
- {api.PodLogOptions{Follow: true}, 0},
- {api.PodLogOptions{TailLines: &zero}, 0},
- {api.PodLogOptions{TailLines: &negative}, 1},
- {api.PodLogOptions{TailLines: &positive}, 0},
- {api.PodLogOptions{LimitBytes: &zero}, 1},
- {api.PodLogOptions{LimitBytes: &negative}, 1},
- {api.PodLogOptions{LimitBytes: &positive}, 0},
- {api.PodLogOptions{SinceSeconds: &negative}, 1},
- {api.PodLogOptions{SinceSeconds: &positive}, 0},
- {api.PodLogOptions{SinceSeconds: &zero}, 1},
- {api.PodLogOptions{SinceTime: &now}, 0},
- }
- for i, test := range tests {
- errs := ValidatePodLogOptions(&test.opt)
- if test.errs != len(errs) {
- t.Errorf("%d: Unexpected errors: %v", i, errs)
- }
- }
- }
- func TestValidateConfigMap(t *testing.T) {
- newConfigMap := func(name, namespace string, data map[string]string) api.ConfigMap {
- return api.ConfigMap{
- ObjectMeta: api.ObjectMeta{
- Name: name,
- Namespace: namespace,
- },
- Data: data,
- }
- }
- var (
- validConfigMap = newConfigMap("validname", "validns", map[string]string{"key": "value"})
- maxKeyLength = newConfigMap("validname", "validns", map[string]string{strings.Repeat("a", 253): "value"})
- emptyName = newConfigMap("", "validns", nil)
- invalidName = newConfigMap("NoUppercaseOrSpecialCharsLike=Equals", "validns", nil)
- emptyNs = newConfigMap("validname", "", nil)
- invalidNs = newConfigMap("validname", "NoUppercaseOrSpecialCharsLike=Equals", nil)
- invalidKey = newConfigMap("validname", "validns", map[string]string{"a*b": "value"})
- leadingDotKey = newConfigMap("validname", "validns", map[string]string{".ab": "value"})
- dotKey = newConfigMap("validname", "validns", map[string]string{".": "value"})
- doubleDotKey = newConfigMap("validname", "validns", map[string]string{"..": "value"})
- overMaxKeyLength = newConfigMap("validname", "validns", map[string]string{strings.Repeat("a", 254): "value"})
- overMaxSize = newConfigMap("validname", "validns", map[string]string{"key": strings.Repeat("a", api.MaxSecretSize+1)})
- )
- tests := map[string]struct {
- cfg api.ConfigMap
- isValid bool
- }{
- "valid": {validConfigMap, true},
- "max key length": {maxKeyLength, true},
- "leading dot key": {leadingDotKey, true},
- "empty name": {emptyName, false},
- "invalid name": {invalidName, false},
- "invalid key": {invalidKey, false},
- "empty namespace": {emptyNs, false},
- "invalid namespace": {invalidNs, false},
- "dot key": {dotKey, false},
- "double dot key": {doubleDotKey, false},
- "over max key length": {overMaxKeyLength, false},
- "over max size": {overMaxSize, false},
- }
- for name, tc := range tests {
- errs := ValidateConfigMap(&tc.cfg)
- if tc.isValid && len(errs) > 0 {
- t.Errorf("%v: unexpected error: %v", name, errs)
- }
- if !tc.isValid && len(errs) == 0 {
- t.Errorf("%v: unexpected non-error", name)
- }
- }
- }
- func TestValidateConfigMapUpdate(t *testing.T) {
- newConfigMap := func(version, name, namespace string, data map[string]string) api.ConfigMap {
- return api.ConfigMap{
- ObjectMeta: api.ObjectMeta{
- Name: name,
- Namespace: namespace,
- ResourceVersion: version,
- },
- Data: data,
- }
- }
- var (
- validConfigMap = newConfigMap("1", "validname", "validns", map[string]string{"key": "value"})
- noVersion = newConfigMap("", "validname", "validns", map[string]string{"key": "value"})
- )
- cases := []struct {
- name string
- newCfg api.ConfigMap
- oldCfg api.ConfigMap
- isValid bool
- }{
- {
- name: "valid",
- newCfg: validConfigMap,
- oldCfg: validConfigMap,
- isValid: true,
- },
- {
- name: "invalid",
- newCfg: noVersion,
- oldCfg: validConfigMap,
- isValid: false,
- },
- }
- for _, tc := range cases {
- errs := ValidateConfigMapUpdate(&tc.newCfg, &tc.oldCfg)
- if tc.isValid && len(errs) > 0 {
- t.Errorf("%v: unexpected error: %v", tc.name, errs)
- }
- if !tc.isValid && len(errs) == 0 {
- t.Errorf("%v: unexpected non-error", tc.name)
- }
- }
- }
- func TestValidateHasLabel(t *testing.T) {
- successCase := api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Labels: map[string]string{
- "other": "blah",
- "foo": "bar",
- },
- }
- if errs := ValidateHasLabel(successCase, field.NewPath("field"), "foo", "bar"); len(errs) != 0 {
- t.Errorf("expected success: %v", errs)
- }
- missingCase := api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Labels: map[string]string{
- "other": "blah",
- },
- }
- if errs := ValidateHasLabel(missingCase, field.NewPath("field"), "foo", "bar"); len(errs) == 0 {
- t.Errorf("expected failure")
- }
- wrongValueCase := api.ObjectMeta{
- Name: "123",
- Namespace: "ns",
- Labels: map[string]string{
- "other": "blah",
- "foo": "notbar",
- },
- }
- if errs := ValidateHasLabel(wrongValueCase, field.NewPath("field"), "foo", "bar"); len(errs) == 0 {
- t.Errorf("expected failure")
- }
- }
- func TestIsValidSysctlName(t *testing.T) {
- valid := []string{
- "a.b.c.d",
- "a",
- "a_b",
- "a-b",
- "abc",
- "abc.def",
- }
- invalid := []string{
- "",
- "*",
- "ä",
- "a_",
- "_",
- "__",
- "_a",
- "_a._b",
- "-",
- ".",
- "a.",
- ".a",
- "a.b.",
- "a*.b",
- "a*b",
- "*a",
- "a.*",
- "*",
- "abc*",
- "a.abc*",
- "a.b.*",
- "Abc",
- func(n int) string {
- x := make([]byte, n)
- for i := range x {
- x[i] = byte('a')
- }
- return string(x)
- }(256),
- }
- for _, s := range valid {
- if !IsValidSysctlName(s) {
- t.Errorf("%q expected to be a valid sysctl name", s)
- }
- }
- for _, s := range invalid {
- if IsValidSysctlName(s) {
- t.Errorf("%q expected to be an invalid sysctl name", s)
- }
- }
- }
- func TestValidateSysctls(t *testing.T) {
- valid := []string{
- "net.foo.bar",
- "kernel.shmmax",
- }
- invalid := []string{
- "i..nvalid",
- "_invalid",
- }
- sysctls := make([]api.Sysctl, len(valid))
- for i, sysctl := range valid {
- sysctls[i].Name = sysctl
- }
- errs := validateSysctls(sysctls, field.NewPath("foo"))
- if len(errs) != 0 {
- t.Errorf("unexpected validation errors: %v", errs)
- }
- sysctls = make([]api.Sysctl, len(invalid))
- for i, sysctl := range invalid {
- sysctls[i].Name = sysctl
- }
- errs = validateSysctls(sysctls, field.NewPath("foo"))
- if len(errs) != 2 {
- t.Errorf("expected 2 validation errors. Got: %v", errs)
- } else {
- if got, expected := errs[0].Error(), "foo"; !strings.Contains(got, expected) {
- t.Errorf("unexpected errors: expected=%q, got=%q", expected, got)
- }
- if got, expected := errs[1].Error(), "foo"; !strings.Contains(got, expected) {
- t.Errorf("unexpected errors: expected=%q, got=%q", expected, got)
- }
- }
- }
- func newNodeNameEndpoint(nodeName string) *api.Endpoints {
- ep := &api.Endpoints{
- ObjectMeta: api.ObjectMeta{
- Name: "foo",
- Namespace: api.NamespaceDefault,
- ResourceVersion: "1",
- },
- Subsets: []api.EndpointSubset{
- {
- NotReadyAddresses: []api.EndpointAddress{},
- Ports: []api.EndpointPort{{Name: "https", Port: 443, Protocol: "TCP"}},
- Addresses: []api.EndpointAddress{
- {
- IP: "8.8.8.8",
- Hostname: "zookeeper1",
- NodeName: &nodeName}}}}}
- return ep
- }
- func TestEndpointAddressNodeNameUpdateRestrictions(t *testing.T) {
- oldEndpoint := newNodeNameEndpoint("kubernetes-minion-setup-by-backend")
- updatedEndpoint := newNodeNameEndpoint("kubernetes-changed-nodename")
- // Check that NodeName cannot be changed during update (if already set)
- errList := ValidateEndpoints(updatedEndpoint)
- errList = append(errList, ValidateEndpointsUpdate(updatedEndpoint, oldEndpoint)...)
- if len(errList) == 0 {
- t.Error("Endpoint should not allow changing of Subset.Addresses.NodeName on update")
- }
- }
- func TestEndpointAddressNodeNameInvalidDNS1123(t *testing.T) {
- // Check NodeName DNS validation
- endpoint := newNodeNameEndpoint("illegal.nodename")
- errList := ValidateEndpoints(endpoint)
- if len(errList) == 0 {
- t.Error("Endpoint should reject invalid NodeName")
- }
- }
|