From e12b9d123e13392d737b7e88c9ba1a1cfdb43dad Mon Sep 17 00:00:00 2001 From: Tom Gardham-Pallister Date: Mon, 4 Dec 2017 09:03:49 +0000 Subject: [PATCH 01/13] images for docs --- docs/images/OcelotBasic.jpg | Bin 0 -> 37518 bytes docs/images/OcelotIndentityServer.jpg | Bin 0 -> 66753 bytes docs/images/OcelotMultipleInstances.jpg | Bin 0 -> 52168 bytes docs/images/OcelotMultipleInstancesConsul.jpg | Bin 0 -> 63115 bytes docs/index.rst | 24 +----------- docs/introduction/bigpicture.rst | 36 +++++++++++++++++- 6 files changed, 36 insertions(+), 24 deletions(-) create mode 100644 docs/images/OcelotBasic.jpg create mode 100644 docs/images/OcelotIndentityServer.jpg create mode 100644 docs/images/OcelotMultipleInstances.jpg create mode 100644 docs/images/OcelotMultipleInstancesConsul.jpg diff --git a/docs/images/OcelotBasic.jpg b/docs/images/OcelotBasic.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bf42a496536be8a984fe4af01a3de30773105505 GIT binary patch literal 37518 zcmeFYcUV(R+bPLlklpxOZj+JSB)fB)oczx1J9o(MJ-A1H_x{~G zcPJ<+?mu|=kn$n~AaM z6U#|M9Aa*miosz{5%4Q_kA$R;+06?-t}17^GWy?)|HCu@aO375WVfzgyM2`_O?j33 zw`sCJZr!^1--=xOgYwEY6^jtr3+N+OVGHEj$7N4IF!aZ-KWm&RrD>~z&Iy$!}{k`sgW3_Iu92IrgGWLg#FpAx8*H98H z4i>?1`Y)q_(pWTOxu};)jc=D5`K`=h3g!L4&aoV)C3_+^*BkV**_qr&6h^+T#Q0A>)jvTOURje!f)3J0)PZFd*4_ z%9PWki2&))(e~=jtv3vi_<4Qd`O2yTNYWWwhjJ$b>n^nkpY9B+t%jm0vG8^qE3A!? z53ibIWe-%gN+sH^_?671xah;-gD6#JHjPdZ4?Oo}q5hz7-tA7JoE|Ov#Sd3|cN~vd z=(-j%){?yH3q$!^M zk{2VLTMlVtY^SNyh0A_hfMxH*pC-HQ7^Bg#z#YTbMIAWr&K9{yR7A^!Uf4RnzmMrH z^YtB!u=Z>U%DBk##Bml zovE$2GP$+$FC!WQJ&9#-&-v!lqob-`0gYN1wJJO$&bYu4ddz;+YuH%()$M`%0>f(; zbgA3?pOXbSzNOu?iLNErD4>?SXFV!;M>yO#b%lsm=Xe?LlS(%znMYE6itSeaR@HWq zR0<7BTafo7HlkB2W3rZq`gsToJv!c@z^S=3%-&hurP&|jJ?wZs6~j+>@Bb%!=T=Li z-k}DIlmEo>-6+#YFnUPb?|!Kdqn(1#mT*}6WR5KdM%XyT3n&v z`o6AWaFl9(GoAab>ZW9K*)&IvZ#6C|SlTCU7u=)!>M139Vh~~1xKPN8!HDy2KX{Vf zQ4$gl$;G7TQ;5uSBh7XA8P2U)#*4bQ@pwP-TVt#Eg%q zOYF*o(EHcyry$0R#7z{W!qhOz*+AL{SyZQ&FMRSSIjy`mC+-!yu;aQI^Q;nPKgE*y z2{Xa74KcdAnpNC)K&-HD@1~}a@SWMh~%tiIoKkq&_ zHx$n`JY*fP{$Vtj$yOl5X2(KoBGW=W$;?4as)1W%REOR>m*sS@=t4Kd7(PbNsq2Ud zzl(6D>bk2iT#5VQj56)ho6>HAzHV(_g!s~T+gN8SZu5QHw5$FKG183}RAgh>rn{cC zZVKdo)9vkxK$z0Wxg=>OGHIn5SwBv?--)VbX;^%yKOIw6j`Q{@J|GSf&mO}Kaltdi zRjJAT&06AAg%Oj5;}ac>oet4dJ2pZ$?|0~)?huCv-RTggq-0}7+-4Uim9i}%O6*Uv z64wLz=Pe#RoH@m@&fUV&j-ZyFE*&nO^=~tdw((#|H?x3yqN?zr7jn7PNQWhbm&D%9 zUk@r;TpO)B_>i)~!x4EO*KHEgKVlQb$hI3(ZAURP@cY=@W(c=d8fIbxNJ2JdxQ2zZ z)Y4!xMWd$|Pk+r|m9i3gJwM|s;BAkw6di5~Q!8qkTypUO`L$8iOTzpEpJFOX&eSYh#Fc4kGugo|7g`t z;fDL8_n}Y&T5_GCWX67j#b=mAUhH@Y#J(10chcW#%XP+od-#pCPUYCmx3RzrhuP26 zHYrBp4@n)^jfp^PhemwYXd;gZjp}HL70;2A+76tescYXzL!7a^M;P+?eqlCmJcsH! zp+BArQlPf{f-bR)@)$yUX-WH{kIz8k$h1$ak8ON#=|EWAB15wsi*8@I-l1`lc(2drLJFkMZwcYtZX=lo_Fko!o~U; zh1|^&mDj6;tx;k%nq9_9Cv6yg+j+5tFmc;CfsTA;RAdqJ4GpEel7<6y_1lj5nmaDE z9-TiElzRI5=KP5}pzBJMd7cwyA$@s0BJbMHUZqrphU7V?b~$}c5|k?D5utrUvN|Y_ zDSAFqAN`Cp=|7AMWci-@mNV>lJLNsX+&+DjudcT93qM2LC{Kcj4f7Ol>h#ke(Lsc2 zmV%f9DF*|H^L~v*{g~Htc3IWkm*KdAjt7=o3Ao|FKiHle zBU_Wu@a@BCmuK~rz6f-n_0nxh>h5RS+=Yk>gF>eIu>90#DvJC_k~HOC?J-9{e;M!q>ZnZzo_mG&;;A`q8Q=3jAI69p#0tp=WD< zQs{ouu+6R3v1oG4HEcE2bkJMGhG?_i%Rf4xGu{xRs&&rGkxZn*N)%UCrQKHU7+N;c zt9t$4&4mA37kDo^OhtYJ$hwn*jdOU@Jo?`FspLQ{#%N4$m)4v+)ogrk^PxB>;d=++ z>QW&=x2s*L0pvwHNkgTBeR}7d-A$plX87~Wb5e_rwG#vHUvv1Y@bfxC>KDU^dHI{7 zQ&)e|BL>l^F(4`cr?&V^+I-B~X$OMlEwN_QJ;8=5^^`8e%(PwsY`yXWml;XZbBDP!F2H^w`M zF(>}JF;lt8Z>w0FPW)0~<7rk63(~#E>rC#nv8lvq7)Rhb$rco=;`$dd=6%?X?5W+b z{h51$;gXnVqk^Ibtq@}>XVT?{RfB^#1CZ7&=fiFT0h2K>!+3LECnj2ILHjoVn5QJ8 zBOQ~7TEjQ?!hCM`IY!N+QMbdQhTpRqdCIjb_43q|28%a{tM7@V43pY@iX>Zodwt(5 z3LW^Zxf~u5x+DpW!1C_^(V*}DlRp06ynOSYpLJUyPqF)c-mrYph4^ud@*gArrjl!V zu0yAF)n{M&e*=68Hw*vO6YlC>;RYY}?j9!FXGUu#M_FvuXD*?25p{ij>&`d4Zudst zR}+TPgQUp{93pv9-n32@4c*nDUn3#GEM_R6v$Gpj#b={mUS!84GV|P7O?Ulf(k;v?r@P}v8R$~Vc&1;I*t*oh zukZy~*-mq^E>OF^=0maYvl^>?4{ z;s5kr%-b!SHauWy-X5=BnX&>>Vi{u>yk)j|3C_$r{kS1Up9NM{_sz`4iW6@)XIb2+ zsBf#?@(ka6Q)~w10*A&MXPN3Ur`r7qk)$Gscko$)UC*@NZ-B~-;g(%j8EteT14DRJ z$M9Y2HM1lul~g-XgKkHfp0!yC8!-YyxBTN!Dw7#GUGDn@5FNNE`zTIqQbk$fkA*DG zVP%4M;AllWGwk_0V>LIIx{K33!Q689gU=+Q+Q33(s@`;$Esu=Kys2y=f8F?Ml2jju zyM6I-rO*CPR8;H9PsqgXD$GjTps6&;UyjfNxRy=$PbYkG+34_#>HWHxQlWSOr*Up% zACtH^cXvVsyKq}9lXof4@{yYy8*l(yT@0-r>rC?^DWJ``o~V{^*{A)J zT}PBllSN{ltY&-YELIekf(ge@m;=bDmO2L(6%G+lVEy4hLrupSk39Ly@yJ#Y)HPg1 z(UAr0Ud3z*#KSrVNy{tHodcUX)t;t~vY1>+$Kb|W6E21@0cMTV9~(Y7j&b!=K3-GK zZBrKg+lCz?6Ir<50L@JnsFmsmG;D~pSVb+Vh@4FrT1B#(gb+yYr+fUn7h+BdbA|tcnQyg{uvcNjg8vm ze-?YNkLzr_4bQu$FJk1^VL(ZTU>a?#b8KH&ipI@pgQwkdH@fJv1lzxYt<6VR4_QLV z7ttRpMux`i`}`A>a#DR(9L0*w8ZB8_tr%IEQO6YswSGcH-%y@bO}g62_rs+n;at;&$HTTJAB-e>ZPgTcw}u_)rC+)0dUa zTNz`i!VR5-ep|B6vl{Qs>2{5E*?1%Ex%OkEL8**1zPnAwT-2bnd01)3?A$daYwWNN zo@?;PDh2?!NPqof2t1y9GOaxLqX#$SUfU}!HmP<&0d!05S;*yz>PZ{ReaB|h+j!S< zC!6UUK`c2is-qK1GeSOWKPkzuJ>I?-Q@;nwxsZfa!waz7D)B(F8a*hLwss2Gk6Dw@ zGc$MQDDok;tSF%6MMWuoK-XovyGZ#kJM*rfl%|TbSyfQSiQF`yJeQQn?BX?dd2kx- z+o!HC6yCdcw>2|ev)j&r(W-pUaE$7k>m(*eAg!cz*+Ba0K0FtB(G@`PI{j#pc=g&* zM~!atk1$DDExe^6S?-MZSWSiij4tcRSRgqsg&YKRoByY7w)WhUS}fKLFH3*OdGE(% z&z^BK_q2JXhIVb1+(fGiJPn22CUqFY^iM=)$=8mB~sS!YKo1P zo7vav$nMj&co`X%m>dcVpSHdhrjQ%9TPR@n6dY2uL&yOw3BiX;!Xdl8xJ{Q z+b)YRjyQ{XBJ$qI(hQ!^YXxfA_e{6qZYi_{HkX>?!eI13R#1>#Zx9}A57U3o25)JZTFG4?kNxYbg;CKlITQ zk%^?M4uu=MCYgr|(|4hgr{v$oRbtV}C-?qZqPbhwRm|{sX}rF!AjWKD{XQPu70%=r zbP^0#XVoAcvx*S7jUF%fymS`JHsH8OLoJ7nC$zX_H%#?5yhB#gL%De&NlV$nTh=yn z$br+{n@wb+5pCILo&)?jl?zCrp?*#rfmok1JqCK3vislzfL!FmKb-P^b=G^ex~!ca zXmIMX%@AdjUDzSdXkPk;4jNwbGi(!H+X1QE&04VefjcWdAzAXHJHCWQio|?N0;(?R{pE4 z#3Hmw-VlVccJ9E9s#*{>9Ylyp^r9!o@*Y=ZbM6Y=6Ge#NCS^VrF)M$TdikRfx=8c# zvHQ+8PJ31-;^ab_dk;3MvOet58GGPympI3;3!r=S-r+LH`Yf67)J9pys=!cYigPxL zQh+Ot)OQ0>d}8dd$pbwAg8g`1wq@-HHQ1ul`X3VnJ(_g6zpqC*oWDzcnU|^&Z*yY< zpVNTiN+=h_`u}LUU7trXZ(d3BXOX-gY^6U*TXd*fqdqHA^bF#Yjmk8<0^u!n^{~!+ zQrR;q6G}f$X|xvVA(hTmymdo(LV?<9s#Q{Dq-6#h?l8R7V_jA^l@86+Uy%o*d?cGw z+%J_YgK`yYS&3OQ-6>peV7Pwo$wK z1%j^aIIu=1)D*78-{OB`5X}Lyn`nVtQofN5_(Lx{)$lOMXq|(_CO35`cFYn3UzFCQ zTulX*J^ZFT`B;TgrIyojC+G6usrg<-Gp{*GbY(RwFmuK#Np^ZsGUhkHO*U(R!#`?~ zv5cwlN7i0&o`eB~RN@DzDSQpVS%@qis7a9~S_W%oiu!p#%bScCMtHZTCe~)jyKk|* z@f{Nk72r8+is-dSRCefBvzm=(tlNm~4)r1K)(wUu+crE(;^D3u@bcBl3QP{Lm-vtn(kFBkp zY|7@wf~XKAHpFUKet&DonPnI_nw0p_T1^e(heluAiwX$2b8+O9L@X1`wz1&5VR z>quz3>tB?~S#xU$ajT~?iC7`h1=O*Vl=kov&OM}fIiXX` znUSMt4K7it3)tgoBwciT-v0G$k^*Gv?N?^U6jW=-#I2w7Lw)=NM+>L`T#JJI1yk&RiP_#+24@ehE#?*nOi52%R98%vfz3AyZjZdBn;l&*Z}&t2z+)3hE+Kxmi>o9#cUnbi8}F`d923+gV?~ z{84t`_h_sBQ_e3)Gm#%VwDZe8*M|*(ZXBwa>lF)A&}yJDn>Cf#Owa`^@DV-tyS?}A zO_alwtF9*>ybsk@U}5q5cWZIE2HaebIw#z;eOPVm57i7|HvVJzkcN#XE>8)_j^K_i zw|h$4CZ4tThvXUW@Eqsts|h2l5onig?WC1>c^|ox0@Zn=3y0LnNx$t8pX}rlpCXW9 z*gAYUyj8_#V8^P|v~sippC{3aYCZaia`HN7K2$kMOAp)x{|0=e`whsGxv^e?YV*up z3%C7Ag)|{2g8C*Zkr!&q5@9R3)6kr_a2}I);~15b^4;;1abq){)A{)gp{YIEmNv5& zk?q2J;wi&1IyPi)8g}TYHy%y*ID3ewe4aX?Qy8G3(XBBnEM-*Rt3cQ`dg?7>{5Wo! z?QrEh4`f0WDt3YR!zu9e&N(N-^Fj-ZeWJErl-?;3qa#&_SJ+Bh-+(?#U+rQQpEuK5 z{qg4ROzw{!J({YXHBcFx30>H$S?p$8Z%p5mF`y}|)R4aOCc=bxXC>!j3+xWJfV&|< zn)>2%)+2%PXvQ<1Lps~cb^e&*rI*8PnQw@RH}A9ARKFS!hxnf->p2?z1_*uR=N)!= zI3#0LpSu|`$?a^9Qvz)Y&7CgWv{bHYD{xrRZk~Su(3SrK`naAhqtYzL6@Zoz|FbyB zWg&&nb7`Vo)PK80Oh@=?zWtRg1G-h+;-Yo#;q9iv(Q*2&{!9ramG#2y)O*G9#I&_L zb3@YL7M{^xBsI;WTI4V)GTDtC>ul^KwtE|{RCPjEW=|->I~!{|^e#v?oP)cY!i=Bb zktu1)R%1)!1U*6|;R@fRzLlF)W{8bmj}fb1e`)HJG>I#YhJYaF_3i?<5V7SC4vs6U zuCMLQ%fHN(wqetZR}x(W84nAm2W1Q#2*-=5H?25@L|D-Ub|_9;*o^my z)J@KWC1rialffalSOjt0{Wl=w9&=RWFP#yoUk`>I3QaOUe;G{d#w^0Y9T)1$z3+AI zvI%6H$#rSZ)3_*1fClIA;p`6jO;$IQrhAu!u13P02H*Ok-3kVUgkm>szS+T1R!$J* z(@oicUD>>fXu%PU0kfJ|efit%Lv1vJ9v;W{5_;cXvozFe@N@sTu?U+$kOkhikK_=O zn?1?1;_XrhSzXUMj(#v7`DBs% zjqK7yQuGF1OfWBzw8d^D^!nV|HaDACa39SbdM}>Do;*+Ek>z}z+4JjxriNDhYHVp% zvmh&+!nxEd9~$R4-uS>qy!Pz)qNSr%njG%>Ye^Z8crp^H&n8(sg)fqo@?8+_fftF5 z4y_BNE(CaQUj+jI*Ln?J|Cb=s$xV=Q_@bvA+_Y@QW$0+zJ4eZ<%(EzGFx4B;bA? z#~~rRDchFuP-BiI7C75FX0YyrkRaPC>)K2Hw$UJaJ2ENXc0-gNdRjWHHKt$i$;wg_ zkYtTPe15mWVZ6cykhG+lQ(H&)>kt%29%!l;`nW;MKI`EmKKrqEzdLO&Pe`ASGgM67`FYs(bGU($J(9holua9l z0o&qbCOFels&Ryhd+*opFX}M8i#VBVqe}|0YBAiM=VmJ{VCj(0){+7feo3Yy=rdcn z)g2mKU|o0H3s#IAVi3Ny(6zFB%`!a$$~WyPU2y)L>I7RgcA|Ai8pNGjT@=q~_+9+{ zN{wB!SyPXZ#nu&M3TGlAs~&~FWr)Q-oPv7F#@a|^Z{g9Dv=}Nom!^!^U{4_g-}x&- zLEfWTjvyLn-C=>33}LAa&AC<>i$UHdGI%YS!@^pSnLb?&%wm7u`u=5bRPXM z)q-M(1No+8SuV$D2UWmz7`@KAoJ~D#F#6+g{&_)0ZMbootu0*5m*5BNafjU6UknX` zmRAwTm`M6siw@xivqoh9{BqU&ve}D>Gyg?VFZimM9HMi;?eyV{d_%({r1;QGoL_n&VCX5AB$B~ zY!shbZmT0iCLvr3w~6ey$TCVwC$QogK*VhG&;RrS_t>cTv=$nABmXulAyYHiIAD#q z8eBRHgz$#n5k3Ni5r^N&N2JO&-Ev#|UUGB*M#`~>G>>VNA9CN*Wc6hyl9ab-?WZUG z>CbSzojqp|7{))ra`i2eLXNS1Wh`oamLoa#7lIV%6<98|sf?=JGmnt!7^VAI>JuaW zvB3pyN3~xwoZJ8UYf!*td}%-Jog>u7>K{FwmKoJjWaUO zrwavUT>dmRB-sR}A%12GzP)^;^cz4q;QuE{J^ERX9bEkJz%pe#Rl%CK-V{|bM+r-O z!y1Z$PB)LW_ymZrP@CG|>Z=m8fK>{eh&bbs)#$5Oh)6YpA>L@z*+NELx(M+R05Xjgg9NVhRw_x>j^XW$M zx9QnQ;!v*+D6`Q{4tX&-&>Z;0?a;tL)Jy!+V0y`VdST~Mf27Go!G?`@!F@VrwindD z0dHXbJ6hXW$JCXOW9

Id~eoXLwGVm!wcFYX8AGOF=m|ftot|sZe%#t*6`Aeh|;~?*^Yn z%F#hM!MH)blgJ;VmJJn;(vR1Lt{nX!x9U)AFS5fhGoD#;Gl}`7#M`%dZ9msCK?rf2w0Lp6^-P$swaD_Upl?(DlUyBp~Y&H_9FY@=PfqKBu!x{gtAwXF5| zxri_Z3}hXzv0uznwHi zBjSOjEKmk(IYC&3Mct0R!A^U^5S5N+f^bP}!yL>XA!qKQ%?I`hhs@WBSDyQ8Yk=SU z1z`B;{Gaas?^EPO3>O7Q4>qo162EfLk-Gna>D(;*H_CtfGi-BLSIl}h><+21CQF>AOo%sw<_2<7RT?Ow1xw?fe1er4umh}fWc^r+z@oN6YES0-u zfp9DqO{MM*#ZppY(SR=-{de#_mrvwMU*)eCBWtz=bn%mPFNeV&IUKw*L2TBfjLXwR z_Ri`Z#f*Vp)Doj*{TV56XB)2|3vF&5Tx{UI6-cG>irup~A%d_)s- z|CUXWDF+RGzYg!L$`!fKrHq=NlPXt^dTU*b4<%RnyV;F5{Tb{pOO*3`j*{Ij-=)7` zM*Ifg#1z4oQJOrtRXD z8M2!2u&jx&(!H?oOF;@b=4w)UoA1S06F)H|E-6*vWe_Eh8{!03`oYR_2B4d?|1bUf zZ~p%A&j_jI$P<0ZpE?`NBbW5&Dr{FV_1pR&LY+|RlGegg=MoFAvEoM&o+ag%>;;H; zyNZxpW7koWyzvu(j=gK5*OrQjh4m5c>%|M&QCjs3vl08IDtdX3r}9^>NX9I!{N#cW zx`~uq*4DDG|Gps^Vp3v4BFR~x*z(KUJ9v2x-8{??9}zR7ZZvG2fQzifX0~3xT0H?| zFx!Pi%P-!_^|QmpoC{?*%zU^^@?ewcFgi%)CRA*4)H!|Pm`cO@tO#LNnPDHRU%k!g9LnIh00$# z%p(o?8(6`Yi6jw+C#*5sgU|oNqsyr|`I^hPQ(c`+09=E@?CDMV2roSZa_PY|K9Wf@!*~ zsTgnzz3H2Bj4og|0z0%jYIR0axZnjp4=KMx+5cVoJ+d5%b~0I?bGBsq^uG>&|J|X0 zo2CUuHgOrN`-GeWR=rgyRuPt?21A>p+f=@{rgbaBp_EurAiJaW!1qeJG`(y9OPx)@ zVI5Wnc=~C1czQ(U<<43$m1>%vAfhi1qik($v&+(o{n$#&O%7#Ru;%>*9^w`!LMyiJ zcSSJl<1?ewpAa2l<>W3p$o$~rvr#$Ioma1HJhQZ|O0U}adT!yL12N4J-GnVa!H**1 zKC$Yq2^XTg0V>9ZPTKA6R$_B6+=|0GyLi>U#ijrC-&*h=hr*hSZJh9vU&a^6z-6!7 zzX5*(@(Y2)=vPH {T{0ABCV%fJ2$oKH!iK61}Ryq`O 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⪻Ja$A!t!A|1-x^_)>1bd>_3~X6r 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 literal 0 HcmV?d00001 diff --git a/docs/images/OcelotIndentityServer.jpg b/docs/images/OcelotIndentityServer.jpg new file mode 100644 index 0000000000000000000000000000000000000000..730af85b7bdf7a950a498e22fc04419ddaa2aa80 GIT binary patch literal 66753 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

UfyHZkn_W_k)<~vE{~*$WRRYhb{Q0XDJr^t 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 z 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 Date: Mon, 4 Dec 2017 09:05:15 +0000 Subject: [PATCH 02/13] correct path for images --- docs/introduction/bigpicture.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/introduction/bigpicture.rst b/docs/introduction/bigpicture.rst index a86bbf78..18ea1b12 100644 --- a/docs/introduction/bigpicture.rst +++ b/docs/introduction/bigpicture.rst @@ -23,16 +23,16 @@ The following are configuration that you use when deploying Ocelot. Basic Implementation ^^^^^^^^^^^^^^^^^^^^ -.. image:: images/OcelotBasic.jpg +.. image:: ../images/OcelotBasic.jpg With IdentityServer ^^^^^^^^^^^^^^^^^^^ -.. image:: images/OcelotIndentityServer.jpg +.. image:: ../images/OcelotIndentityServer.jpg Multiple Instances ^^^^^^^^^^^^^^^^^^ -.. image:: images/OcelotMultipleInstances.jpg +.. image:: ../images/OcelotMultipleInstances.jpg With Consul ^^^^^^^^^^^ -.. image:: images/OcelotMultipleInstancesConsul.jpg +.. image:: ../images/OcelotMultipleInstancesConsul.jpg From 189471cabade01f4352207b2548be10a9469b4be Mon Sep 17 00:00:00 2001 From: Andrew Warren-Love Date: Mon, 4 Dec 2017 10:11:56 -0500 Subject: [PATCH 03/13] demonstrate issue #169 --- .../DependencyInjection/OcelotBuilderTests.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs index 138ea496..834879c5 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs @@ -5,12 +5,15 @@ using System.Net.Http; using System.Text; using CacheManager.Core; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Internal; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Ocelot.Cache; using Ocelot.Configuration; using Ocelot.Configuration.File; +using Ocelot.Configuration.Setter; using Ocelot.DependencyInjection; +using Ocelot.Logging; using Shouldly; using TestStack.BDDfy; using Xunit; @@ -20,6 +23,7 @@ namespace Ocelot.UnitTests.DependencyInjection public class OcelotBuilderTests { private IServiceCollection _services; + private IServiceProvider _serviceProvider; private IConfigurationRoot _configRoot; private IOcelotBuilder _ocelotBuilder; private int _maxRetries; @@ -30,6 +34,7 @@ namespace Ocelot.UnitTests.DependencyInjection _configRoot = new ConfigurationRoot(new List()); _services = new ServiceCollection(); _services.AddSingleton(builder); + _services.AddSingleton(); _maxRetries = 100; } private Exception _ex; @@ -70,6 +75,16 @@ namespace Ocelot.UnitTests.DependencyInjection .BDDfy(); } + [Fact] + public void should_use_logger_factory() + { + this.Given(x => WhenISetUpOcelotServices()) + .When(x => WhenIValidateScopes()) + .When(x => WhenIAccessLoggerFactory()) + .Then(x => ThenAnExceptionIsntThrown()) + .BDDfy(); + } + private void OnlyOneVersionOfEachCacheIsRegistered() { var outputCache = _services.Single(x => x.ServiceType == typeof(IOcelotCache)); @@ -127,6 +142,30 @@ namespace Ocelot.UnitTests.DependencyInjection } } + private void WhenIAccessLoggerFactory() + { + try + { + var logger = _serviceProvider.GetService(); + } + catch (Exception e) + { + _ex = e; + } + } + + private void WhenIValidateScopes() + { + try + { + _serviceProvider = _services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); + } + catch (Exception e) + { + _ex = e; + } + } + private void ThenAnExceptionIsntThrown() { _ex.ShouldBeNull(); From 4f27a50503a1bf9182f480aeb720621117ac0714 Mon Sep 17 00:00:00 2001 From: Eilyyyy Date: Tue, 5 Dec 2017 12:29:44 -0600 Subject: [PATCH 04/13] =?UTF-8?q?add=20file=20configuration=20fluent=20val?= =?UTF-8?q?idation=20and=20change=20default=20configura=E2=80=A6=20(#168)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add file configuration fluent validation and change default configuration validator to fluent validator * add file validation failed error code * change authentication schemes check to async * beautify the code ^_^ * clean file validation and fix test failure. --- .../Creator/FileOcelotConfigurationCreator.cs | 2 +- .../File/FileAuthenticationOptions.cs | 10 + .../Configuration/File/FileRateLimitRule.cs | 16 ++ .../DownstreamPathTemplateAlreadyUsedError.cs | 11 - ...wnstreamPathTemplateContainsSchemeError.cs | 12 - ...PathTemplateDoesntStartWithForwardSlash.cs | 12 - .../FileConfigurationFluentValidator.cs | 50 ++++ .../Validator/FileConfigurationValidator.cs | 223 ------------------ .../Validator/FileValidationFailedError.cs | 15 ++ .../RateLimitOptionsValidationError.cs | 15 -- .../Validator/ReRouteFluentValidator.cs | 54 +++++ .../UnsupportedAuthenticationProviderError.cs | 12 - .../DependencyInjection/OcelotBuilder.cs | 2 +- src/Ocelot/Errors/OcelotErrorCode.cs | 3 +- src/Ocelot/Ocelot.csproj | 1 + ... => ConfigurationFluentValidationTests.cs} | 15 +- .../FileConfigurationCreatorTests.cs | 2 +- .../ErrorsToHttpStatusCodeMapperTests.cs | 3 +- 18 files changed, 160 insertions(+), 298 deletions(-) delete mode 100644 src/Ocelot/Configuration/Validator/DownstreamPathTemplateAlreadyUsedError.cs delete mode 100644 src/Ocelot/Configuration/Validator/DownstreamPathTemplateContainsSchemeError.cs delete mode 100644 src/Ocelot/Configuration/Validator/DownstreamPathTemplateDoesntStartWithForwardSlash.cs create mode 100644 src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs delete mode 100644 src/Ocelot/Configuration/Validator/FileConfigurationValidator.cs create mode 100644 src/Ocelot/Configuration/Validator/FileValidationFailedError.cs delete mode 100644 src/Ocelot/Configuration/Validator/RateLimitOptionsValidationError.cs create mode 100644 src/Ocelot/Configuration/Validator/ReRouteFluentValidator.cs delete mode 100644 src/Ocelot/Configuration/Validator/UnsupportedAuthenticationProviderError.cs rename test/Ocelot.UnitTests/Configuration/{ConfigurationValidationTests.cs => ConfigurationFluentValidationTests.cs} (94%) diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs index f8e2d506..5e3f4b44 100644 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs @@ -162,4 +162,4 @@ namespace Ocelot.Configuration.Creator return loadBalancerKey; } } -} \ No newline at end of file +} diff --git a/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs b/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs index 81fc9d28..2b99dc56 100644 --- a/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs +++ b/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text; namespace Ocelot.Configuration.File { @@ -11,5 +12,14 @@ namespace Ocelot.Configuration.File public string AuthenticationProviderKey {get; set;} public List AllowedScopes { get; set; } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append($"{nameof(AuthenticationProviderKey)}:{AuthenticationProviderKey},{nameof(AllowedScopes)}:["); + sb.AppendJoin(',', AllowedScopes); + sb.Append("]"); + return sb.ToString(); + } } } diff --git a/src/Ocelot/Configuration/File/FileRateLimitRule.cs b/src/Ocelot/Configuration/File/FileRateLimitRule.cs index 727a9e82..5a79c3c0 100644 --- a/src/Ocelot/Configuration/File/FileRateLimitRule.cs +++ b/src/Ocelot/Configuration/File/FileRateLimitRule.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading.Tasks; namespace Ocelot.Configuration.File @@ -30,5 +31,20 @@ namespace Ocelot.Configuration.File /// Maximum number of requests that a client can make in a defined period /// public long Limit { get; set; } + + public override string ToString() + { + if (!EnableRateLimiting) + { + return string.Empty; + } + var sb = new StringBuilder(); + sb.Append( + $"{nameof(Period)}:{Period},{nameof(PeriodTimespan)}:{PeriodTimespan:F},{nameof(Limit)}:{Limit},{nameof(ClientWhitelist)}:["); + + sb.AppendJoin(',', ClientWhitelist); + sb.Append(']'); + return sb.ToString(); + } } } diff --git a/src/Ocelot/Configuration/Validator/DownstreamPathTemplateAlreadyUsedError.cs b/src/Ocelot/Configuration/Validator/DownstreamPathTemplateAlreadyUsedError.cs deleted file mode 100644 index e350753c..00000000 --- a/src/Ocelot/Configuration/Validator/DownstreamPathTemplateAlreadyUsedError.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Ocelot.Errors; - -namespace Ocelot.Configuration.Validator -{ - public class DownstreamPathTemplateAlreadyUsedError : Error - { - public DownstreamPathTemplateAlreadyUsedError(string message) : base(message, OcelotErrorCode.DownstreampathTemplateAlreadyUsedError) - { - } - } -} diff --git a/src/Ocelot/Configuration/Validator/DownstreamPathTemplateContainsSchemeError.cs b/src/Ocelot/Configuration/Validator/DownstreamPathTemplateContainsSchemeError.cs deleted file mode 100644 index a3dfa309..00000000 --- a/src/Ocelot/Configuration/Validator/DownstreamPathTemplateContainsSchemeError.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Ocelot.Errors; - -namespace Ocelot.Configuration.Validator -{ - public class DownstreamPathTemplateContainsSchemeError : Error - { - public DownstreamPathTemplateContainsSchemeError(string message) - : base(message, OcelotErrorCode.DownstreamPathTemplateContainsSchemeError) - { - } - } -} diff --git a/src/Ocelot/Configuration/Validator/DownstreamPathTemplateDoesntStartWithForwardSlash.cs b/src/Ocelot/Configuration/Validator/DownstreamPathTemplateDoesntStartWithForwardSlash.cs deleted file mode 100644 index 2f09dbfb..00000000 --- a/src/Ocelot/Configuration/Validator/DownstreamPathTemplateDoesntStartWithForwardSlash.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Ocelot.Errors; - -namespace Ocelot.Configuration.Validator -{ - public class PathTemplateDoesntStartWithForwardSlash : Error - { - public PathTemplateDoesntStartWithForwardSlash(string message) - : base(message, OcelotErrorCode.PathTemplateDoesntStartWithForwardSlash) - { - } - } -} diff --git a/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs b/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs new file mode 100644 index 00000000..5cc4967b --- /dev/null +++ b/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs @@ -0,0 +1,50 @@ +using FluentValidation; +using Microsoft.AspNetCore.Authentication; +using Ocelot.Configuration.File; +using Ocelot.Errors; +using Ocelot.Responses; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ocelot.Configuration.Validator +{ + public class FileConfigurationFluentValidator : AbstractValidator, IConfigurationValidator + { + public FileConfigurationFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider) + { + RuleFor(configuration => configuration.ReRoutes) + .SetCollectionValidator(new ReRouteFluentValidator(authenticationSchemeProvider)); + RuleForEach(configuration => configuration.ReRoutes) + .Must((config, reRoute) => IsNotDuplicateIn(reRoute, config.ReRoutes)) + .WithMessage((config, reRoute) => $"duplicate downstreampath {reRoute.UpstreamPathTemplate}"); + } + + public async Task> IsValid(FileConfiguration configuration) + { + var validateResult = await ValidateAsync(configuration); + if (validateResult.IsValid) + { + return new OkResponse(new ConfigurationValidationResult(false)); + } + var errors = validateResult.Errors.Select(failure => new FileValidationFailedError(failure.ErrorMessage)); + var result = new ConfigurationValidationResult(true, errors.Cast().ToList()); + return new OkResponse(result); + } + + private static bool IsNotDuplicateIn(FileReRoute reRoute, List routes) + { + var reRoutesWithUpstreamPathTemplate = routes.Where(r => r.UpstreamPathTemplate == reRoute.UpstreamPathTemplate).ToList(); + var hasEmptyListToAllowAllHttpVerbs = reRoutesWithUpstreamPathTemplate.Any(x => x.UpstreamHttpMethod.Count == 0); + var hasDuplicateEmptyListToAllowAllHttpVerbs = reRoutesWithUpstreamPathTemplate.Count(x => x.UpstreamHttpMethod.Count == 0) > 1; + + var hasSpecificHttpVerbs = reRoutesWithUpstreamPathTemplate.Any(x => x.UpstreamHttpMethod.Count != 0); + var hasDuplicateSpecificHttpVerbs = reRoutesWithUpstreamPathTemplate.SelectMany(x => x.UpstreamHttpMethod).GroupBy(x => x.ToLower()).SelectMany(x => x.Skip(1)).Any(); + if (hasDuplicateEmptyListToAllowAllHttpVerbs || hasDuplicateSpecificHttpVerbs || (hasEmptyListToAllowAllHttpVerbs && hasSpecificHttpVerbs)) + { + return false; + } + return true; + } + } +} diff --git a/src/Ocelot/Configuration/Validator/FileConfigurationValidator.cs b/src/Ocelot/Configuration/Validator/FileConfigurationValidator.cs deleted file mode 100644 index 7b4f6dbe..00000000 --- a/src/Ocelot/Configuration/Validator/FileConfigurationValidator.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication; -using Ocelot.Configuration.File; -using Ocelot.Errors; -using Ocelot.Responses; - -namespace Ocelot.Configuration.Validator -{ - public class FileConfigurationValidator : IConfigurationValidator - { - private readonly IAuthenticationSchemeProvider _provider; - - public FileConfigurationValidator(IAuthenticationSchemeProvider provider) - { - _provider = provider; - } - - public async Task> IsValid(FileConfiguration configuration) - { - var result = CheckForDuplicateReRoutes(configuration); - - if (result.IsError) - { - return new OkResponse(result); - } - - result = CheckDownstreamTemplatePathBeingsWithForwardSlash(configuration); - - if (result.IsError) - { - return new OkResponse(result); - } - - result = CheckUpstreamTemplatePathBeingsWithForwardSlash(configuration); - - if (result.IsError) - { - return new OkResponse(result); - } - - result = await CheckForUnsupportedAuthenticationProviders(configuration); - - if (result.IsError) - { - return new OkResponse(result); - } - - result = CheckForReRoutesContainingDownstreamSchemeInDownstreamPathTemplate(configuration); - - if (result.IsError) - { - return new OkResponse(result); - } - result = CheckForReRoutesRateLimitOptions(configuration); - - if (result.IsError) - { - return new OkResponse(result); - } - - return new OkResponse(result); - } - - private ConfigurationValidationResult CheckDownstreamTemplatePathBeingsWithForwardSlash(FileConfiguration configuration) - { - var errors = new List(); - - foreach(var reRoute in configuration.ReRoutes) - { - if(!reRoute.DownstreamPathTemplate.StartsWith("/")) - { - errors.Add(new PathTemplateDoesntStartWithForwardSlash($"{reRoute.DownstreamPathTemplate} doesnt start with forward slash")); - } - } - - if(errors.Any()) - { - return new ConfigurationValidationResult(true, errors); - } - - return new ConfigurationValidationResult(false, errors); - } - - private ConfigurationValidationResult CheckUpstreamTemplatePathBeingsWithForwardSlash(FileConfiguration configuration) - { - var errors = new List(); - - foreach(var reRoute in configuration.ReRoutes) - { - if(!reRoute.UpstreamPathTemplate.StartsWith("/")) - { - errors.Add(new PathTemplateDoesntStartWithForwardSlash($"{reRoute.DownstreamPathTemplate} doesnt start with forward slash")); - } - } - - if(errors.Any()) - { - return new ConfigurationValidationResult(true, errors); - } - - return new ConfigurationValidationResult(false, errors); - } - - private async Task CheckForUnsupportedAuthenticationProviders(FileConfiguration configuration) - { - var errors = new List(); - - foreach (var reRoute in configuration.ReRoutes) - { - var isAuthenticated = !string.IsNullOrEmpty(reRoute.AuthenticationOptions.AuthenticationProviderKey); - - if (!isAuthenticated) - { - continue; - } - - var data = await _provider.GetAllSchemesAsync(); - var schemes = data.ToList(); - if (schemes.Any(x => x.Name == reRoute.AuthenticationOptions.AuthenticationProviderKey)) - { - continue; - } - - var error = new UnsupportedAuthenticationProviderError($"{reRoute.AuthenticationOptions.AuthenticationProviderKey} is unsupported authentication provider, upstream template is {reRoute.UpstreamPathTemplate}, upstream method is {reRoute.UpstreamHttpMethod}"); - errors.Add(error); - } - - return errors.Count > 0 - ? new ConfigurationValidationResult(true, errors) - : new ConfigurationValidationResult(false); - } - - private ConfigurationValidationResult CheckForReRoutesContainingDownstreamSchemeInDownstreamPathTemplate(FileConfiguration configuration) - { - var errors = new List(); - - foreach(var reRoute in configuration.ReRoutes) - { - if(reRoute.DownstreamPathTemplate.Contains("https://") - || reRoute.DownstreamPathTemplate.Contains("http://")) - { - errors.Add(new DownstreamPathTemplateContainsSchemeError($"{reRoute.DownstreamPathTemplate} contains scheme")); - } - } - - if(errors.Any()) - { - return new ConfigurationValidationResult(true, errors); - } - - return new ConfigurationValidationResult(false, errors); - } - - private ConfigurationValidationResult CheckForDuplicateReRoutes(FileConfiguration configuration) - { - var duplicatedUpstreamPathTemplates = new List(); - - var distinctUpstreamPathTemplates = configuration.ReRoutes.Select(x => x.UpstreamPathTemplate).Distinct(); - - foreach (string upstreamPathTemplate in distinctUpstreamPathTemplates) - { - var reRoutesWithUpstreamPathTemplate = configuration.ReRoutes.Where(x => x.UpstreamPathTemplate == upstreamPathTemplate); - - var hasEmptyListToAllowAllHttpVerbs = reRoutesWithUpstreamPathTemplate.Where(x => x.UpstreamHttpMethod.Count() == 0).Any(); - var hasDuplicateEmptyListToAllowAllHttpVerbs = reRoutesWithUpstreamPathTemplate.Where(x => x.UpstreamHttpMethod.Count() == 0).Count() > 1; - var hasSpecificHttpVerbs = reRoutesWithUpstreamPathTemplate.Where(x => x.UpstreamHttpMethod.Count() > 0).Any(); - var hasDuplicateSpecificHttpVerbs = reRoutesWithUpstreamPathTemplate.SelectMany(x => x.UpstreamHttpMethod).GroupBy(x => x.ToLower()).SelectMany(x => x.Skip(1)).Any(); - - if (hasDuplicateEmptyListToAllowAllHttpVerbs || hasDuplicateSpecificHttpVerbs || (hasEmptyListToAllowAllHttpVerbs && hasSpecificHttpVerbs)) - { - duplicatedUpstreamPathTemplates.Add(upstreamPathTemplate); - } - } - - if (duplicatedUpstreamPathTemplates.Count() == 0) - { - return new ConfigurationValidationResult(false); - } - else - { - var errors = duplicatedUpstreamPathTemplates - .Select(d => new DownstreamPathTemplateAlreadyUsedError(string.Format("Duplicate DownstreamPath: {0}", d))) - .Cast() - .ToList(); - - return new ConfigurationValidationResult(true, errors); - } - - } - - private ConfigurationValidationResult CheckForReRoutesRateLimitOptions(FileConfiguration configuration) - { - var errors = new List(); - - foreach (var reRoute in configuration.ReRoutes) - { - if (reRoute.RateLimitOptions.EnableRateLimiting) - { - if (!IsValidPeriod(reRoute)) - { - errors.Add(new RateLimitOptionsValidationError($"{reRoute.RateLimitOptions.Period} not contains scheme")); - } - } - } - - if (errors.Any()) - { - return new ConfigurationValidationResult(true, errors); - } - - return new ConfigurationValidationResult(false, errors); - } - - private static bool IsValidPeriod(FileReRoute reRoute) - { - string period = reRoute.RateLimitOptions.Period; - - return period.Contains("s") || period.Contains("m") || period.Contains("h") || period.Contains("d"); - } - } -} diff --git a/src/Ocelot/Configuration/Validator/FileValidationFailedError.cs b/src/Ocelot/Configuration/Validator/FileValidationFailedError.cs new file mode 100644 index 00000000..02255b5a --- /dev/null +++ b/src/Ocelot/Configuration/Validator/FileValidationFailedError.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Ocelot.Errors; + +namespace Ocelot.Configuration.Validator +{ + public class FileValidationFailedError : Error + { + public FileValidationFailedError(string message) : base(message, OcelotErrorCode.FileValidationFailedError) + { + + } + } +} diff --git a/src/Ocelot/Configuration/Validator/RateLimitOptionsValidationError.cs b/src/Ocelot/Configuration/Validator/RateLimitOptionsValidationError.cs deleted file mode 100644 index e467a486..00000000 --- a/src/Ocelot/Configuration/Validator/RateLimitOptionsValidationError.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Ocelot.Errors; -using System; -using System.Collections.Generic; -using System.Text; - -namespace Ocelot.Configuration.Validator -{ - public class RateLimitOptionsValidationError : Error - { - public RateLimitOptionsValidationError(string message) - : base(message, OcelotErrorCode.RateLimitOptionsError) - { - } - } -} diff --git a/src/Ocelot/Configuration/Validator/ReRouteFluentValidator.cs b/src/Ocelot/Configuration/Validator/ReRouteFluentValidator.cs new file mode 100644 index 00000000..915be18b --- /dev/null +++ b/src/Ocelot/Configuration/Validator/ReRouteFluentValidator.cs @@ -0,0 +1,54 @@ +using FluentValidation; +using Microsoft.AspNetCore.Authentication; +using Ocelot.Configuration.File; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Ocelot.Configuration.Validator +{ + public class ReRouteFluentValidator : AbstractValidator + { + private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider; + + public ReRouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider) + { + _authenticationSchemeProvider = authenticationSchemeProvider; + + RuleFor(reRoute => reRoute.DownstreamPathTemplate) + .Must(path => path.StartsWith("/")) + .WithMessage("downstream path {PropertyValue} doesnt start with forward slash"); + RuleFor(reRoute => reRoute.UpstreamPathTemplate) + .Must(path => path.StartsWith("/")) + .WithMessage("upstream path {PropertyValue} doesnt start with forward slash"); + RuleFor(reRoute => reRoute.DownstreamPathTemplate) + .Must(path => !path.Contains("https://") && !path.Contains("http://")) + .WithMessage("downstream path {PropertyValue} contains scheme"); + RuleFor(reRoute => reRoute.RateLimitOptions) + .Must(IsValidPeriod) + .WithMessage("rate limit period {PropertyValue} not contains (s,m,h,d)"); + RuleFor(reRoute => reRoute.AuthenticationOptions) + .MustAsync(IsSupportedAuthenticationProviders) + .WithMessage("{PropertyValue} is unsupported authentication provider"); + } + + private async Task IsSupportedAuthenticationProviders(FileAuthenticationOptions authenticationOptions, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(authenticationOptions.AuthenticationProviderKey)) + { + return true; + } + var schemes = await _authenticationSchemeProvider.GetAllSchemesAsync(); + var supportedSchemes = schemes.Select(scheme => scheme.Name).ToList(); + + return supportedSchemes.Contains(authenticationOptions.AuthenticationProviderKey); + } + + private static bool IsValidPeriod(FileRateLimitRule rateLimitOptions) + { + string period = rateLimitOptions.Period; + + return !rateLimitOptions.EnableRateLimiting || period.Contains("s") || period.Contains("m") || period.Contains("h") || period.Contains("d"); + } + } +} diff --git a/src/Ocelot/Configuration/Validator/UnsupportedAuthenticationProviderError.cs b/src/Ocelot/Configuration/Validator/UnsupportedAuthenticationProviderError.cs deleted file mode 100644 index e4f441bf..00000000 --- a/src/Ocelot/Configuration/Validator/UnsupportedAuthenticationProviderError.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Ocelot.Errors; - -namespace Ocelot.Configuration.Validator -{ - public class UnsupportedAuthenticationProviderError : Error - { - public UnsupportedAuthenticationProviderError(string message) - : base(message, OcelotErrorCode.UnsupportedAuthenticationProviderError) - { - } - } -} diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 9eb6821e..3b54303a 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -72,7 +72,7 @@ namespace Ocelot.DependencyInjection _services.Configure(configurationRoot); _services.TryAddSingleton(); _services.TryAddSingleton(); - _services.TryAddSingleton(); + _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); diff --git a/src/Ocelot/Errors/OcelotErrorCode.cs b/src/Ocelot/Errors/OcelotErrorCode.cs index 6f85df58..3b65a3fd 100644 --- a/src/Ocelot/Errors/OcelotErrorCode.cs +++ b/src/Ocelot/Errors/OcelotErrorCode.cs @@ -32,6 +32,7 @@ UnableToSetConfigInConsulError, UnmappableRequestError, RateLimitOptionsError, - PathTemplateDoesntStartWithForwardSlash + PathTemplateDoesntStartWithForwardSlash, + FileValidationFailedError } } diff --git a/src/Ocelot/Ocelot.csproj b/src/Ocelot/Ocelot.csproj index 243953b5..ca664327 100644 --- a/src/Ocelot/Ocelot.csproj +++ b/src/Ocelot/Ocelot.csproj @@ -26,6 +26,7 @@ + diff --git a/test/Ocelot.UnitTests/Configuration/ConfigurationValidationTests.cs b/test/Ocelot.UnitTests/Configuration/ConfigurationFluentValidationTests.cs similarity index 94% rename from test/Ocelot.UnitTests/Configuration/ConfigurationValidationTests.cs rename to test/Ocelot.UnitTests/Configuration/ConfigurationFluentValidationTests.cs index 77d1e278..6a1bac80 100644 --- a/test/Ocelot.UnitTests/Configuration/ConfigurationValidationTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ConfigurationFluentValidationTests.cs @@ -15,17 +15,17 @@ using Xunit; namespace Ocelot.UnitTests.Configuration { - public class ConfigurationValidationTests + public class ConfigurationFluentValidationTests { private readonly IConfigurationValidator _configurationValidator; private FileConfiguration _fileConfiguration; private Response _result; private Mock _provider; - public ConfigurationValidationTests() + public ConfigurationFluentValidationTests() { - _provider = new Mock(); - _configurationValidator = new FileConfigurationValidator(_provider.Object); + _provider = new Mock(); + _configurationValidator = new FileConfigurationFluentValidator(_provider.Object); } [Fact] @@ -44,6 +44,7 @@ namespace Ocelot.UnitTests.Configuration })) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) + .Then(x => x.ThenTheErrorIs()) .BDDfy(); } @@ -147,7 +148,6 @@ namespace Ocelot.UnitTests.Configuration })) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorIs()) .BDDfy(); } @@ -172,10 +172,10 @@ namespace Ocelot.UnitTests.Configuration })) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorIs()) .BDDfy(); } + private void GivenAConfiguration(FileConfiguration fileConfiguration) { _fileConfiguration = fileConfiguration; @@ -225,6 +225,5 @@ namespace Ocelot.UnitTests.Configuration return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name))); } } - } -} \ No newline at end of file +} diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs index 975f71da..aef99941 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs @@ -503,7 +503,7 @@ namespace Ocelot.UnitTests.Configuration [Fact] public void should_return_validation_errors() { - var errors = new List {new PathTemplateDoesntStartWithForwardSlash("some message")}; + var errors = new List {new FileValidationFailedError("some message")}; this.Given(x => x.GivenTheConfigIs(new FileConfiguration())) .And(x => x.GivenTheConfigIsInvalid(errors)) diff --git a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs index c1165bc4..23809fd8 100644 --- a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs +++ b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs @@ -54,6 +54,7 @@ namespace Ocelot.UnitTests.Responder [InlineData(OcelotErrorCode.DownstreampathTemplateAlreadyUsedError)] [InlineData(OcelotErrorCode.DownstreamPathTemplateContainsSchemeError)] [InlineData(OcelotErrorCode.DownstreamSchemeNullOrEmptyError)] + [InlineData(OcelotErrorCode.FileValidationFailedError)] [InlineData(OcelotErrorCode.InstructionNotForClaimsError)] [InlineData(OcelotErrorCode.NoInstructionsError)] [InlineData(OcelotErrorCode.ParsingConfigurationHeaderError)] @@ -120,7 +121,7 @@ namespace Ocelot.UnitTests.Responder // If this test fails then it's because the number of error codes has changed. // You should make the appropriate changes to the test cases here to ensure // they cover all the error codes, and then modify this assertion. - Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(31, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?"); + Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(32, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?"); } private void ShouldMapErrorToStatusCode(OcelotErrorCode errorCode, HttpStatusCode expectedHttpStatusCode) From 67a421cb69095473fa823ccd3f3ad606d523a865 Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Wed, 6 Dec 2017 08:24:11 +0000 Subject: [PATCH 05/13] Feature/fix build always reporting green (#173) * added last exit code to wrapper scripts * try force fail * ooops missed ; * trying again * fail build with bad test * removed exception * removed using --- build-and-release-unstable.ps1 | 3 ++- build-and-run-tests.ps1 | 3 ++- release.ps1 | 3 ++- run-acceptance-tests.ps1 | 3 ++- run-benchmarks.ps1 | 3 ++- run-unit-tests.ps1 | 3 ++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/build-and-release-unstable.ps1 b/build-and-release-unstable.ps1 index 51c6f0d5..d6849dc8 100644 --- a/build-and-release-unstable.ps1 +++ b/build-and-release-unstable.ps1 @@ -1 +1,2 @@ -./build.ps1 -target BuildAndReleaseUnstable \ No newline at end of file +./build.ps1 -target BuildAndReleaseUnstable +exit $LASTEXITCODE \ No newline at end of file diff --git a/build-and-run-tests.ps1 b/build-and-run-tests.ps1 index f82502e5..cc9fd4bb 100644 --- a/build-and-run-tests.ps1 +++ b/build-and-run-tests.ps1 @@ -1 +1,2 @@ -./build.ps1 -target RunTests \ No newline at end of file +./build.ps1 -target RunTests +exit $LASTEXITCODE \ No newline at end of file diff --git a/release.ps1 b/release.ps1 index 6cf4c66b..683cb58c 100644 --- a/release.ps1 +++ b/release.ps1 @@ -1 +1,2 @@ -./build.ps1 -target Release \ No newline at end of file +./build.ps1 -target Release +exit $LASTEXITCODE \ No newline at end of file diff --git a/run-acceptance-tests.ps1 b/run-acceptance-tests.ps1 index 480e1d4c..6c6ade10 100644 --- a/run-acceptance-tests.ps1 +++ b/run-acceptance-tests.ps1 @@ -1 +1,2 @@ -./build -target RunAcceptanceTests \ No newline at end of file +./build -target RunAcceptanceTests +exit $LASTEXITCODE \ No newline at end of file diff --git a/run-benchmarks.ps1 b/run-benchmarks.ps1 index e05490fd..cb4e9b61 100644 --- a/run-benchmarks.ps1 +++ b/run-benchmarks.ps1 @@ -1 +1,2 @@ -./build.ps1 -target RunBenchmarkTests \ No newline at end of file +./build.ps1 -target RunBenchmarkTests +exit $LASTEXITCODE \ No newline at end of file diff --git a/run-unit-tests.ps1 b/run-unit-tests.ps1 index 0e6a91bd..c3c36832 100644 --- a/run-unit-tests.ps1 +++ b/run-unit-tests.ps1 @@ -1 +1,2 @@ -./build.ps1 -target RunUnitTests \ No newline at end of file +./build.ps1 -target RunUnitTests +exit $LASTEXITCODE \ No newline at end of file From 5855a14935abaa318b66dbd0fc1b9a14c39ad7e8 Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Sat, 9 Dec 2017 14:41:35 +0000 Subject: [PATCH 06/13] Feature/more validation (#174) * added message assertion for validation test * another message assertion * more validation tests --- .../FileConfigurationFluentValidator.cs | 31 ++- .../Validator/ReRouteFluentValidator.cs | 25 +- .../ConfigurationFluentValidationTests.cs | 234 +++++++++++++++++- 3 files changed, 272 insertions(+), 18 deletions(-) diff --git a/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs b/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs index 5cc4967b..08ab574a 100644 --- a/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs +++ b/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs @@ -15,35 +15,50 @@ namespace Ocelot.Configuration.Validator { RuleFor(configuration => configuration.ReRoutes) .SetCollectionValidator(new ReRouteFluentValidator(authenticationSchemeProvider)); + RuleForEach(configuration => configuration.ReRoutes) .Must((config, reRoute) => IsNotDuplicateIn(reRoute, config.ReRoutes)) - .WithMessage((config, reRoute) => $"duplicate downstreampath {reRoute.UpstreamPathTemplate}"); + .WithMessage((config, reRoute) => $"{nameof(reRoute)} {reRoute.UpstreamPathTemplate} has duplicate"); } public async Task> IsValid(FileConfiguration configuration) { var validateResult = await ValidateAsync(configuration); + if (validateResult.IsValid) { return new OkResponse(new ConfigurationValidationResult(false)); } + var errors = validateResult.Errors.Select(failure => new FileValidationFailedError(failure.ErrorMessage)); + var result = new ConfigurationValidationResult(true, errors.Cast().ToList()); + return new OkResponse(result); } - private static bool IsNotDuplicateIn(FileReRoute reRoute, List routes) + private static bool IsNotDuplicateIn(FileReRoute reRoute, List reRoutes) { - var reRoutesWithUpstreamPathTemplate = routes.Where(r => r.UpstreamPathTemplate == reRoute.UpstreamPathTemplate).ToList(); - var hasEmptyListToAllowAllHttpVerbs = reRoutesWithUpstreamPathTemplate.Any(x => x.UpstreamHttpMethod.Count == 0); - var hasDuplicateEmptyListToAllowAllHttpVerbs = reRoutesWithUpstreamPathTemplate.Count(x => x.UpstreamHttpMethod.Count == 0) > 1; + var matchingReRoutes = reRoutes.Where(r => r.UpstreamPathTemplate == reRoute.UpstreamPathTemplate).ToList(); - var hasSpecificHttpVerbs = reRoutesWithUpstreamPathTemplate.Any(x => x.UpstreamHttpMethod.Count != 0); - var hasDuplicateSpecificHttpVerbs = reRoutesWithUpstreamPathTemplate.SelectMany(x => x.UpstreamHttpMethod).GroupBy(x => x.ToLower()).SelectMany(x => x.Skip(1)).Any(); - if (hasDuplicateEmptyListToAllowAllHttpVerbs || hasDuplicateSpecificHttpVerbs || (hasEmptyListToAllowAllHttpVerbs && hasSpecificHttpVerbs)) + if(matchingReRoutes.Count == 1) + { + return true; + } + + var allowAllVerbs = matchingReRoutes.Any(x => x.UpstreamHttpMethod.Count == 0); + + var duplicateAllowAllVerbs = matchingReRoutes.Count(x => x.UpstreamHttpMethod.Count == 0) > 1; + + var specificVerbs = matchingReRoutes.Any(x => x.UpstreamHttpMethod.Count != 0); + + var duplicateSpecificVerbs = matchingReRoutes.SelectMany(x => x.UpstreamHttpMethod).GroupBy(x => x.ToLower()).SelectMany(x => x.Skip(1)).Any(); + + if (duplicateAllowAllVerbs || duplicateSpecificVerbs || (allowAllVerbs && specificVerbs)) { return false; } + return true; } } diff --git a/src/Ocelot/Configuration/Validator/ReRouteFluentValidator.cs b/src/Ocelot/Configuration/Validator/ReRouteFluentValidator.cs index 915be18b..b386890f 100644 --- a/src/Ocelot/Configuration/Validator/ReRouteFluentValidator.cs +++ b/src/Ocelot/Configuration/Validator/ReRouteFluentValidator.cs @@ -17,19 +17,35 @@ namespace Ocelot.Configuration.Validator RuleFor(reRoute => reRoute.DownstreamPathTemplate) .Must(path => path.StartsWith("/")) - .WithMessage("downstream path {PropertyValue} doesnt start with forward slash"); + .WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash"); + RuleFor(reRoute => reRoute.UpstreamPathTemplate) .Must(path => path.StartsWith("/")) - .WithMessage("upstream path {PropertyValue} doesnt start with forward slash"); + .WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash"); + RuleFor(reRoute => reRoute.DownstreamPathTemplate) .Must(path => !path.Contains("https://") && !path.Contains("http://")) - .WithMessage("downstream path {PropertyValue} contains scheme"); + .WithMessage("{PropertyName} {PropertyValue} contains scheme"); + + RuleFor(reRoute => reRoute.UpstreamPathTemplate) + .Must(path => !path.Contains("https://") && !path.Contains("http://")) + .WithMessage("{PropertyName} {PropertyValue} contains scheme"); + RuleFor(reRoute => reRoute.RateLimitOptions) .Must(IsValidPeriod) - .WithMessage("rate limit period {PropertyValue} not contains (s,m,h,d)"); + .WithMessage("RateLimitOptions.Period does not contains (s,m,h,d)"); + RuleFor(reRoute => reRoute.AuthenticationOptions) .MustAsync(IsSupportedAuthenticationProviders) .WithMessage("{PropertyValue} is unsupported authentication provider"); + + When(reRoute => reRoute.UseServiceDiscovery, () => { + RuleFor(r => r.ServiceName).NotEmpty().WithMessage("ServiceName cannot be empty or null when using service discovery or Ocelot cannot look up your service!"); + }); + + When(reRoute => !reRoute.UseServiceDiscovery, () => { + RuleFor(r => r.DownstreamHost).NotEmpty().WithMessage("When not using service discover DownstreamHost must be set or Ocelot cannot find your service!"); + }); } private async Task IsSupportedAuthenticationProviders(FileAuthenticationOptions authenticationOptions, CancellationToken cancellationToken) @@ -39,6 +55,7 @@ namespace Ocelot.Configuration.Validator return true; } var schemes = await _authenticationSchemeProvider.GetAllSchemesAsync(); + var supportedSchemes = schemes.Select(scheme => scheme.Name).ToList(); return supportedSchemes.Contains(authenticationOptions.AuthenticationProviderKey); diff --git a/test/Ocelot.UnitTests/Configuration/ConfigurationFluentValidationTests.cs b/test/Ocelot.UnitTests/Configuration/ConfigurationFluentValidationTests.cs index 6a1bac80..90475a27 100644 --- a/test/Ocelot.UnitTests/Configuration/ConfigurationFluentValidationTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ConfigurationFluentValidationTests.cs @@ -29,7 +29,7 @@ namespace Ocelot.UnitTests.Configuration } [Fact] - public void configuration_is_invalid_if_scheme_in_downstream_template() + public void configuration_is_invalid_if_scheme_in_downstream_or_upstream_template() { this.Given(x => x.GivenAConfiguration(new FileConfiguration { @@ -45,6 +45,10 @@ namespace Ocelot.UnitTests.Configuration .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) .Then(x => x.ThenTheErrorIs()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Downstream Path Template http://www.bbc.co.uk/api/products/{productId} doesnt start with forward slash")) + .And(x => x.ThenTheErrorMessageAtPositionIs(1, "Upstream Path Template http://asdf.com doesnt start with forward slash")) + .And(x => x.ThenTheErrorMessageAtPositionIs(2, "Downstream Path Template http://www.bbc.co.uk/api/products/{productId} contains scheme")) + .And(x => x.ThenTheErrorMessageAtPositionIs(3, "Upstream Path Template http://asdf.com contains scheme")) .BDDfy(); } @@ -58,7 +62,8 @@ namespace Ocelot.UnitTests.Configuration new FileReRoute { DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/" + UpstreamPathTemplate = "/asdf/", + DownstreamHost = "bbc.co.uk" } } })) @@ -83,6 +88,7 @@ namespace Ocelot.UnitTests.Configuration })) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Downstream Path Template api/products/ doesnt start with forward slash")) .BDDfy(); } @@ -102,6 +108,7 @@ namespace Ocelot.UnitTests.Configuration })) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Upstream Path Template api/prod/ doesnt start with forward slash")) .BDDfy(); } @@ -116,6 +123,7 @@ namespace Ocelot.UnitTests.Configuration { DownstreamPathTemplate = "/api/products/", UpstreamPathTemplate = "/asdf/", + DownstreamHost = "bbc.co.uk", AuthenticationOptions = new FileAuthenticationOptions() { AuthenticationProviderKey = "Test" @@ -148,11 +156,12 @@ namespace Ocelot.UnitTests.Configuration })) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "AuthenticationProviderKey:Test,AllowedScopes:[] is unsupported authentication provider")) .BDDfy(); } [Fact] - public void configuration_is_not_valid_with_duplicate_reroutes() + public void configuration_is_not_valid_with_duplicate_reroutes_all_verbs() { this.Given(x => x.GivenAConfiguration(new FileConfiguration { @@ -161,17 +170,225 @@ namespace Ocelot.UnitTests.Configuration new FileReRoute { DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/" + UpstreamPathTemplate = "/asdf/", + DownstreamHost = "bb.co.uk" }, new FileReRoute { - DownstreamPathTemplate = "http://www.bbc.co.uk", - UpstreamPathTemplate = "/asdf/" + DownstreamPathTemplate = "/www/test/", + UpstreamPathTemplate = "/asdf/", + DownstreamHost = "bb.co.uk" } } })) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "reRoute /asdf/ has duplicate")) + .BDDfy(); + } + + [Fact] + public void configuration_is_not_valid_with_duplicate_reroutes_specific_verbs() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + DownstreamHost = "bbc.co.uk", + UpstreamHttpMethod = new List {"Get"} + }, + new FileReRoute + { + DownstreamPathTemplate = "/www/test/", + UpstreamPathTemplate = "/asdf/", + DownstreamHost = "bbc.co.uk", + UpstreamHttpMethod = new List {"Get"} + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "reRoute /asdf/ has duplicate")) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_with_duplicate_reroutes_different_verbs() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + DownstreamHost = "bbc.co.uk", + }, + new FileReRoute + { + DownstreamPathTemplate = "/www/test/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Post"}, + DownstreamHost = "bbc.co.uk", + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_with_invalid_rate_limit_configuration() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + DownstreamHost = "bbc.co.uk", + RateLimitOptions = new FileRateLimitRule + { + Period = "1x", + EnableRateLimiting = true + } + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "RateLimitOptions.Period does not contains (s,m,h,d)")) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_with_valid_rate_limit_configuration() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + DownstreamHost = "bbc.co.uk", + RateLimitOptions = new FileRateLimitRule + { + Period = "1d", + EnableRateLimiting = true + } + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void configuration_is_invalid_with_using_service_discovery_and_no_service_name(string serviceName) + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + UseServiceDiscovery = true, + ServiceName = serviceName + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "ServiceName cannot be empty or null when using service discovery or Ocelot cannot look up your service!")) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_with_using_service_discovery_and_service_name() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + UseServiceDiscovery = true, + ServiceName = "Test" + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + + [Theory] + [InlineData(null)] + [InlineData("")] + public void configuration_is_invalid_when_not_using_service_discovery_and_host(string downstreamHost) + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + UseServiceDiscovery = false, + DownstreamHost = downstreamHost + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "When not using service discover DownstreamHost must be set or Ocelot cannot find your service!")) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_when_not_using_service_discovery_and_host_is_set() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + UseServiceDiscovery = false, + DownstreamHost = "bbc.co.uk" + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) .BDDfy(); } @@ -201,6 +418,11 @@ namespace Ocelot.UnitTests.Configuration _result.Data.Errors[0].ShouldBeOfType(); } + private void ThenTheErrorMessageAtPositionIs(int index, string expected) + { + _result.Data.Errors[index].Message.ShouldBe(expected); + } + private void GivenTheAuthSchemeExists(string name) { _provider.Setup(x => x.GetAllSchemesAsync()).ReturnsAsync(new List From 79029f50d323e4587bfc171014ae9c0896cf1973 Mon Sep 17 00:00:00 2001 From: Tom Gardham-Pallister Date: Tue, 12 Dec 2017 07:45:03 +0000 Subject: [PATCH 07/13] change to make test pass for issue 171 --- src/Ocelot/DependencyInjection/OcelotBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 3b54303a..b9801a00 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -117,7 +117,7 @@ namespace Ocelot.DependencyInjection // see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc // could maybe use a scoped data repository _services.TryAddSingleton(); - _services.TryAddScoped(); + _services.TryAddSingleton(); _services.AddMemoryCache(); _services.TryAddSingleton(); From 194f76cf7ffc05cc323b5c373e6755bcd24ccd34 Mon Sep 17 00:00:00 2001 From: Philip Wood Date: Mon, 18 Dec 2017 22:11:17 +0000 Subject: [PATCH 08/13] #177 - optimise the build scripts (#178) * Remove explicit restore, and don't rebuild during tests. This currently fails because the release config doesn't contain symbols needed by opencover. * Build unit tests in debug Turns out that for test coverage we need to have debug symbols. --- build.cake | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/build.cake b/build.cake index e988060e..12fcbdd0 100644 --- a/build.cake +++ b/build.cake @@ -102,17 +102,10 @@ Task("Version") } }); -Task("Restore") +Task("Compile") .IsDependentOn("Clean") .IsDependentOn("Version") .Does(() => - { - DotNetCoreRestore(slnFile); - }); - -Task("Compile") - .IsDependentOn("Restore") - .Does(() => { var settings = new DotNetCoreBuildSettings { @@ -199,6 +192,9 @@ Task("RunAcceptanceTests") var settings = new DotNetCoreTestSettings { Configuration = compileConfig, + ArgumentCustomization = args => args + .Append("--no-restore") + .Append("--no-build") }; EnsureDirectoryExists(artifactsForAcceptanceTestsDir); @@ -212,6 +208,9 @@ Task("RunIntegrationTests") var settings = new DotNetCoreTestSettings { Configuration = compileConfig, + ArgumentCustomization = args => args + .Append("--no-restore") + .Append("--no-build") }; EnsureDirectoryExists(artifactsForIntegrationTestsDir); @@ -474,4 +473,4 @@ private bool ShouldPublishToUnstableFeed(string filter, string branchName) Information("Branch " + branchName + " will not be published to the unstable feed"); } return publish; -} \ No newline at end of file +} From f082f7318a75fbf15b08f5c33279d35f2bac0346 Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Mon, 1 Jan 2018 18:40:39 +0000 Subject: [PATCH 09/13] Raft round 2 (#182) * brought in rafty * moved raft classes into Ocelot and deleted from int project * started to set up rafty in Ocelot * RAFTY INSIDE OCELOT...WOOT * more work adding rafty...just need to get auth working now * rudimentary authenticated raft requests working * asyn await stuff * hacked rafty into the fileconfigurationcontroller...everything seems to be working roughly but I have a lot of refactoring to do * updated to latest rafty that doesnt need an id * hacky but all tests passing * changed admin area set up to use builder not configuration.json, changed admin area auth to use client credentials * missing code coverage * ignore raft sectionf for code coverage * ignore raft sectionf for code coverage * back to normal filters * try exclude attr * missed these * moved client secret to builder for authentication and updated docs * lock to try and fix error accessing identity server created temprsa file on build server * updated postman scripts and changed Ocelot to not always use type handling as this looked crap when manually accessing the configuration endpoint * added rafty docs * changes I missed * added serialisation code we need for rafty to process commands when they proxy to leader * moved controllers into their feature slices --- build.cake | 2 +- docs/features/administration.rst | 42 +- docs/features/raft.rst | 45 ++ docs/index.rst | 1 + ocelot.postman_collection.json | 293 ++++++++++-- src/Ocelot/Authentication/BearerToken.cs | 16 + .../OutputCacheController.cs | 2 +- .../OcelotResourceOwnerPasswordValidator.cs | 53 --- .../Creator/FileOcelotConfigurationCreator.cs | 9 +- .../IdentityServerConfigurationCreator.cs | 17 +- .../File/FileGlobalConfiguration.cs | 1 - .../FileConfigurationController.cs | 30 +- .../Provider/IIdentityServerConfiguration.cs | 9 +- .../Provider/IdentityServerConfiguration.cs | 23 +- src/Ocelot/Configuration/Provider/User.cs | 17 - .../DependencyInjection/IOcelotBuilder.cs | 1 + .../DependencyInjection/OcelotBuilder.cs | 98 +++- .../Middleware/OcelotMiddlewareExtensions.cs | 41 +- src/Ocelot/Ocelot.csproj | 51 +-- src/Ocelot/Raft/ExcludeFromCoverage.cs | 7 + src/Ocelot/Raft/FakeCommand.cs | 15 + src/Ocelot/Raft/FileFsm.cs | 33 ++ src/Ocelot/Raft/FilePeer.cs | 8 + src/Ocelot/Raft/FilePeers.cs | 15 + src/Ocelot/Raft/FilePeersProvider.cs | 44 ++ src/Ocelot/Raft/HttpPeer.cs | 128 ++++++ src/Ocelot/Raft/OcelotFiniteStateMachine.cs | 25 + src/Ocelot/Raft/RaftController.cs | 84 ++++ src/Ocelot/Raft/SqlLiteLog.cs | 279 ++++++++++++ src/Ocelot/Raft/UpdateFileConfiguration.cs | 15 + .../{Startup.cs => AcceptanceTestsStartup.cs} | 8 +- test/Ocelot.AcceptanceTests/Steps.cs | 5 +- .../AdministrationTests.cs | 37 +- ...{Startup.cs => IntegrationTestsStartup.cs} | 8 +- .../Ocelot.IntegrationTests.csproj | 51 +-- test/Ocelot.IntegrationTests/RaftStartup.cs | 55 +++ test/Ocelot.IntegrationTests/RaftTests.cs | 431 ++++++++++++++++++ .../ThreadSafeHeadersTests.cs | 2 +- test/Ocelot.IntegrationTests/peers.json | 18 + .../{Startup.cs => ManualTestStartup.cs} | 7 +- test/Ocelot.ManualTest/Program.cs | 2 +- test/Ocelot.ManualTest/configuration.json | 5 +- .../FileConfigurationCreatorTests.cs | 20 +- .../FileConfigurationRepositoryTests.cs | 4 - ...IdentityServerConfigurationCreatorTests.cs | 2 +- ...elotResourceOwnerPasswordValidatorTests.cs | 117 ----- .../FileConfigurationControllerTests.cs | 82 +++- .../Controllers/OutputCacheControllerTests.cs | 3 +- .../DependencyInjection/OcelotBuilderTests.cs | 29 ++ .../Raft/OcelotFiniteStateMachineTests.cs | 45 ++ 50 files changed, 1876 insertions(+), 459 deletions(-) create mode 100644 docs/features/raft.rst create mode 100644 src/Ocelot/Authentication/BearerToken.cs rename src/Ocelot/{Controllers => Cache}/OutputCacheController.cs (95%) delete mode 100644 src/Ocelot/Configuration/Authentication/OcelotResourceOwnerPasswordValidator.cs rename src/Ocelot/{Controllers => Configuration}/FileConfigurationController.cs (55%) delete mode 100644 src/Ocelot/Configuration/Provider/User.cs create mode 100644 src/Ocelot/Raft/ExcludeFromCoverage.cs create mode 100644 src/Ocelot/Raft/FakeCommand.cs create mode 100644 src/Ocelot/Raft/FileFsm.cs create mode 100644 src/Ocelot/Raft/FilePeer.cs create mode 100644 src/Ocelot/Raft/FilePeers.cs create mode 100644 src/Ocelot/Raft/FilePeersProvider.cs create mode 100644 src/Ocelot/Raft/HttpPeer.cs create mode 100644 src/Ocelot/Raft/OcelotFiniteStateMachine.cs create mode 100644 src/Ocelot/Raft/RaftController.cs create mode 100644 src/Ocelot/Raft/SqlLiteLog.cs create mode 100644 src/Ocelot/Raft/UpdateFileConfiguration.cs rename test/Ocelot.AcceptanceTests/{Startup.cs => AcceptanceTestsStartup.cs} (90%) rename test/Ocelot.IntegrationTests/{Startup.cs => IntegrationTestsStartup.cs} (85%) create mode 100644 test/Ocelot.IntegrationTests/RaftStartup.cs create mode 100644 test/Ocelot.IntegrationTests/RaftTests.cs create mode 100644 test/Ocelot.IntegrationTests/peers.json rename test/Ocelot.ManualTest/{Startup.cs => ManualTestStartup.cs} (89%) delete mode 100644 test/Ocelot.UnitTests/Configuration/OcelotResourceOwnerPasswordValidatorTests.cs create mode 100644 test/Ocelot.UnitTests/Raft/OcelotFiniteStateMachineTests.cs diff --git a/build.cake b/build.cake index 12fcbdd0..7281eb3a 100644 --- a/build.cake +++ b/build.cake @@ -133,7 +133,7 @@ Task("RunUnitTests") new OpenCoverSettings() { Register="user", - ArgumentCustomization=args=>args.Append(@"-oldstyle -returntargetcode") + ArgumentCustomization=args=>args.Append(@"-oldstyle -returntargetcode -excludebyattribute:*.ExcludeFromCoverage*") } .WithFilter("+[Ocelot*]*") .WithFilter("-[xunit*]*") diff --git a/docs/features/administration.rst b/docs/features/administration.rst index 34983b63..162920f6 100644 --- a/docs/features/administration.rst +++ b/docs/features/administration.rst @@ -6,33 +6,24 @@ using bearer tokens that you request from Ocelot iteself. This is provided by th `Identity Server `_ project that I have been using for a few years now. Check them out. In order to enable the administration section you need to do a few things. First of all add this to your -initial configuration.json. The value can be anything you want and it is obviously reccomended don't use +initial Startup.cs. + +The path can be anything you want and it is obviously reccomended don't use a url you would like to route through with Ocelot as this will not work. The administration uses the MapWhen functionality of asp.net core and all requests to {root}/administration will be sent there not to the Ocelot middleware. -.. code-block:: json +The secret is the client secret that Ocelot's internal IdentityServer will use to authenticate requests to the administration API. This can be whatever you want it to be! - "GlobalConfiguration": { - "AdministrationPath": "/administration" +.. code-block:: csharp + + public virtual void ConfigureServices(IServiceCollection services) + { + services + .AddOcelot(Configuration) + .AddAdministration("/administration", "secret"); } -This will get the admin area set up but not the authentication. -Please note that this is a very basic approach to -this problem and if needed we can obviously improve on this! - -You need to set 3 environmental variables. - - ``OCELOT_USERNAME`` - - This need to be the admin username you want to use with Ocelot. - ``OCELOT_HASH`` - ``OCELOT_SALT`` - The hash and salt of the password you want to use given hashing algorythm. When requesting bearer tokens for use with the administration api you will need to supply username and password. In order to create a hash and salt of your password please check out HashCreationTests.should_create_hash_and_salt() this technique is based on [this](https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/password-hashing) - using SHA256 rather than SHA1. - - - Now if you went with the configuration options above and want to access the API you can use the postman scripts called ocelot.postman_collection.json in the solution to change the Ocelot configuration. Obviously these will need to be changed if you are running Ocelot on a different url to http://localhost:5000. @@ -40,7 +31,6 @@ will need to be changed if you are running Ocelot on a different url to http://l The scripts show you how to request a bearer token from ocelot and then use it to GET the existing configuration and POST a configuration. - Administration running multiple Ocelot's ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If you are running multiple Ocelot's in a cluster then you need to use a certificate to sign the bearer tokens used to access the administration API. @@ -59,21 +49,17 @@ Administration API **POST {adminPath}/connect/token** -This gets a token for use with the admin area using the username and password we talk about setting above. Under the hood this calls into an IdentityServer hosted within Ocelot. +This gets a token for use with the admin area using the client credentials we talk about setting above. Under the hood this calls into an IdentityServer hosted within Ocelot. The body of the request is form-data as follows ``client_id`` set as admin -``client_secret`` set as secret +``client_secret`` set as whatever you used when setting up the administration services. ``scope`` set as admin -``username`` set as whatever you used - -``password`` set aswhatever you used - -``grant_type`` set as password +``grant_type`` set as client_credentials **GET {adminPath}/configuration** diff --git a/docs/features/raft.rst b/docs/features/raft.rst new file mode 100644 index 00000000..a61e2ed1 --- /dev/null +++ b/docs/features/raft.rst @@ -0,0 +1,45 @@ +Raft (EXPERIMENTAL DO NOT USE IN PRODUCTION) +============================================ + +Ocelot has recenely integrated `Rafty `_ which is an implementation of Raft that I have also been working on over the last year. This project is very experimental so please do not use this feature of Ocelot in production until I think it's OK. + +Raft is a distributed concensus algorythm that allows a cluster of servers (Ocelots) to maintain local state without having a centralised database for storing state (e.g. SQL Server). + +In order to enable Rafty in Ocelot you must make the following changes to your Startup.cs. + +.. code-block:: csharp + + public virtual void ConfigureServices(IServiceCollection services) + { + services + .AddOcelot(Configuration) + .AddAdministration("/administration", "secret") + .AddRafty(); + } + +In addition to this you must add a file called peers.json to your main project and it will look as follows + +.. code-block:: json + + { + "Peers": [{ + "HostAndPort": "http://localhost:5000" + }, + { + "HostAndPort": "http://localhost:5002" + }, + { + "HostAndPort": "http://localhost:5003" + }, + { + "HostAndPort": "http://localhost:5004" + }, + { + "HostAndPort": "http://localhost:5001" + } + ] + } + +Each instance of Ocelot must have it's address in the array so that they can communicate using Rafty. + +Once you have made these configuration changes you must deploy and start each instance of Ocelot using the addresses in the peers.json file. The servers should then start communicating with each other! You can test if everything is working by posting a configuration update and checking it has replicated to all servers by getting there configuration. diff --git a/docs/index.rst b/docs/index.rst index 6e410970..0a292bcf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,6 +24,7 @@ Thanks for taking a look at the Ocelot documentation. Please use the left hand n features/authentication features/authorisation features/administration + features/raft features/caching features/qualityofservice features/claimstransformation diff --git a/ocelot.postman_collection.json b/ocelot.postman_collection.json index 155e11bc..28bbeb0c 100644 --- a/ocelot.postman_collection.json +++ b/ocelot.postman_collection.json @@ -1,65 +1,169 @@ { - "id": "23a49657-e24b-b967-7ec0-943ff1368680", - "name": "Ocelot Admin", + "id": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", + "name": "Ocelot", "description": "", "order": [ - "59162efa-27ce-c230-f523-81d31ead603d", - "e0defe09-c1b2-9e95-8237-67df4bbab284", - "30007c41-565c-5b87-ea34-42170dd386d7" + "a1c95935-ed18-d5dc-bcb8-a3db8ba1934f", + "ea0ed57a-2cb9-8acc-47dd-006b8db2f1b2", + "c4494401-3985-a5bf-71fb-6e4171384ac6", + "09af8dda-a9cb-20d2-5ee3-0a3023773a1a", + "e8825dc3-4137-99a7-0000-ef5786610dc3", + "fddfc4fa-5114-69e3-4744-203ed71a526b", + "c45d30d7-d9c4-fa05-8110-d6e769bb6ff9", + "4684c2fa-f38c-c193-5f55-bf563a1978c6", + "5f308240-79e3-cf74-7a6b-fe462f0d54f1", + "178f16da-c61b-c881-1c33-9d64a56851a4", + "26a08569-85f6-7f9a-726f-61be419c7a34" ], "folders": [], - "timestamp": 1488042899799, + "timestamp": 0, "owner": "212120", "public": false, "requests": [ { - "id": "30007c41-565c-5b87-ea34-42170dd386d7", + "folder": null, + "id": "09af8dda-a9cb-20d2-5ee3-0a3023773a1a", + "name": "GET http://localhost:5000/comments?postId=1", + "dataMode": "params", + "data": null, + "rawModeData": null, + "descriptionFormat": "html", + "description": "", + "headers": "", + "method": "GET", + "pathVariables": {}, + "url": "http://localhost:5000/comments?postId=1", + "preRequestScript": null, + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" + }, + { + "id": "178f16da-c61b-c881-1c33-9d64a56851a4", "headers": "Authorization: Bearer {{AccessToken}}\n", - "url": "http://localhost:5000/admin/configuration", + "url": "http://localhost:5000/administration/configuration", "preRequestScript": null, "pathVariables": {}, "method": "GET", "data": null, "dataMode": "params", - "version": 2, - "tests": null, - "currentHelper": "normal", - "helperAttributes": "{}", - "time": 1487515927978, - "name": "POST http://localhost:5000/admin/configuration", - "description": "", - "collectionId": "23a49657-e24b-b967-7ec0-943ff1368680", - "responses": [], - "isFromCollection": true, - "collectionRequestId": "59162efa-27ce-c230-f523-81d31ead603d" - }, - { - "id": "59162efa-27ce-c230-f523-81d31ead603d", - "headers": "Authorization: Bearer {{AccessToken}}\nContent-Type: application/json\n", - "url": "http://localhost:5000/admin/configuration", - "preRequestScript": null, - "pathVariables": {}, - "method": "POST", - "data": [], - "dataMode": "raw", - "version": 2, "tests": null, "currentHelper": "normal", "helperAttributes": {}, - "time": 1488044268493, + "time": 1508914722969, "name": "GET http://localhost:5000/admin/configuration", "description": "", - "collectionId": "23a49657-e24b-b967-7ec0-943ff1368680", - "responses": [], - "rawModeData": "{\n \"reRoutes\": [\n {\n \"downstreamPathTemplate\": \"/\",\n \"upstreamPathTemplate\": \"/identityserverexample\",\n \"upstreamHttpMethod\": \"Get\",\n \"authenticationOptions\": {\n \"provider\": \"IdentityServer\",\n \"providerRootUrl\": \"http://localhost:52888\",\n \"apiName\": \"api\",\n \"requireHttps\": false,\n \"allowedScopes\": [\n \"openid\",\n \"offline_access\"\n ],\n \"apiSecret\": \"secret\"\n },\n \"addHeadersToRequest\": {\n \"CustomerId\": \"Claims[CustomerId] > value\",\n \"LocationId\": \"Claims[LocationId] > value\",\n \"UserId\": \"Claims[sub] > value[1] > |\",\n \"UserType\": \"Claims[sub] > value[0] > |\"\n },\n \"addClaimsToRequest\": {\n \"CustomerId\": \"Claims[CustomerId] > value\",\n \"LocationId\": \"Claims[LocationId] > value\",\n \"UserId\": \"Claims[sub] > value[1] > |\",\n \"UserType\": \"Claims[sub] > value[0] > |\"\n },\n \"routeClaimsRequirement\": {\n \"UserType\": \"registered\"\n },\n \"addQueriesToRequest\": {\n \"CustomerId\": \"Claims[CustomerId] > value\",\n \"LocationId\": \"Claims[LocationId] > value\",\n \"UserId\": \"Claims[sub] > value[1] > |\",\n \"UserType\": \"Claims[sub] > value[0] > |\"\n },\n \"requestIdKey\": \"OcRequestId\",\n \"fileCacheOptions\": {\n \"ttlSeconds\": 0\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"localhost\",\n \"downstreamPort\": 52876,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/\",\n \"upstreamPathTemplate\": \"/posts\",\n \"upstreamHttpMethod\": \"Get\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"apiName\": null,\n \"requireHttps\": false,\n \"allowedScopes\": [],\n \"apiSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 0\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"www.bbc.co.uk\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/posts/{postId}\",\n \"upstreamPathTemplate\": \"/posts/{postId}\",\n \"upstreamHttpMethod\": \"Get\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"apiName\": null,\n \"requireHttps\": false,\n \"allowedScopes\": [],\n \"apiSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 0\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"jsonplaceholder.typicode.com\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/posts/{postId}/comments\",\n \"upstreamPathTemplate\": \"/posts/{postId}/comments\",\n \"upstreamHttpMethod\": \"Get\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"apiName\": null,\n \"requireHttps\": false,\n \"allowedScopes\": [],\n \"apiSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 0\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"jsonplaceholder.typicode.com\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/comments\",\n \"upstreamPathTemplate\": \"/comments\",\n \"upstreamHttpMethod\": \"Get\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"apiName\": null,\n \"requireHttps\": false,\n \"allowedScopes\": [],\n \"apiSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 0\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"jsonplaceholder.typicode.com\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/posts\",\n \"upstreamPathTemplate\": \"/posts\",\n \"upstreamHttpMethod\": \"Post\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"apiName\": null,\n \"requireHttps\": false,\n \"allowedScopes\": [],\n \"apiSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 0\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"jsonplaceholder.typicode.com\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/posts/{postId}\",\n \"upstreamPathTemplate\": \"/posts/{postId}\",\n \"upstreamHttpMethod\": \"Put\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"apiName\": null,\n \"requireHttps\": false,\n \"allowedScopes\": [],\n \"apiSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 0\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"jsonplaceholder.typicode.com\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/posts/{postId}\",\n \"upstreamPathTemplate\": \"/posts/{postId}\",\n \"upstreamHttpMethod\": \"Patch\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"apiName\": null,\n \"requireHttps\": false,\n \"allowedScopes\": [],\n \"apiSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 0\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"jsonplaceholder.typicode.com\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/posts/{postId}\",\n \"upstreamPathTemplate\": \"/posts/{postId}\",\n \"upstreamHttpMethod\": \"Delete\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"apiName\": null,\n \"requireHttps\": false,\n \"allowedScopes\": [],\n \"apiSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 0\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"jsonplaceholder.typicode.com\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/api/products\",\n \"upstreamPathTemplate\": \"/products\",\n \"upstreamHttpMethod\": \"Get\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"apiName\": null,\n \"requireHttps\": false,\n \"allowedScopes\": [],\n \"apiSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 15\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"jsonplaceholder.typicode.com\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/api/products/{productId}\",\n \"upstreamPathTemplate\": \"/products/{productId}\",\n \"upstreamHttpMethod\": \"Get\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"apiName\": null,\n \"requireHttps\": false,\n \"allowedScopes\": [],\n \"apiSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 15\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"jsonplaceholder.typicode.com\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 0,\n \"durationOfBreak\": 0,\n \"timeoutValue\": 0\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/api/products\",\n \"upstreamPathTemplate\": \"/products\",\n \"upstreamHttpMethod\": \"Post\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"apiName\": null,\n \"requireHttps\": false,\n \"allowedScopes\": [],\n \"apiSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 0\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"products20161126090340.azurewebsites.net\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/api/products/{productId}\",\n \"upstreamPathTemplate\": \"/products/{productId}\",\n \"upstreamHttpMethod\": \"Put\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"apiName\": null,\n \"requireHttps\": false,\n \"allowedScopes\": [],\n \"apiSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 15\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"products20161126090340.azurewebsites.net\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/api/products/{productId}\",\n \"upstreamPathTemplate\": \"/products/{productId}\",\n \"upstreamHttpMethod\": \"Delete\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"apiName\": null,\n \"requireHttps\": false,\n \"allowedScopes\": [],\n \"apiSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 15\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"products20161126090340.azurewebsites.net\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/api/customers\",\n \"upstreamPathTemplate\": \"/customers\",\n \"upstreamHttpMethod\": \"Get\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"apiName\": null,\n \"requireHttps\": false,\n \"allowedScopes\": [],\n \"apiSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 15\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"customers20161126090811.azurewebsites.net\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/api/customers/{customerId}\",\n \"upstreamPathTemplate\": \"/customers/{customerId}\",\n \"upstreamHttpMethod\": \"Get\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"apiName\": null,\n \"requireHttps\": false,\n \"allowedScopes\": [],\n \"apiSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 15\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"customers20161126090811.azurewebsites.net\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/api/customers\",\n \"upstreamPathTemplate\": \"/customers\",\n \"upstreamHttpMethod\": \"Post\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"apiName\": null,\n \"requireHttps\": false,\n \"allowedScopes\": [],\n \"apiSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 15\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"customers20161126090811.azurewebsites.net\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/api/customers/{customerId}\",\n \"upstreamPathTemplate\": \"/customers/{customerId}\",\n \"upstreamHttpMethod\": \"Put\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"apiName\": null,\n \"requireHttps\": false,\n \"allowedScopes\": [],\n \"apiSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 15\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"customers20161126090811.azurewebsites.net\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/api/customers/{customerId}\",\n \"upstreamPathTemplate\": \"/customers/{customerId}\",\n \"upstreamHttpMethod\": \"Delete\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"apiName\": null,\n \"requireHttps\": false,\n \"allowedScopes\": [],\n \"apiSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 15\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"customers20161126090811.azurewebsites.net\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/posts\",\n \"upstreamPathTemplate\": \"/posts/\",\n \"upstreamHttpMethod\": \"Get\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"apiName\": null,\n \"requireHttps\": false,\n \"allowedScopes\": [],\n \"apiSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 15\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"jsonplaceholder.typicode.com\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n }\n ],\n \"globalConfiguration\": {\n \"requestIdKey\": \"OcRequestId\",\n \"serviceDiscoveryProvider\": {\n \"provider\": null,\n \"host\": null,\n \"port\": 0\n },\n \"administrationPath\": \"/admin\"\n }\n}" + "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" }, { - "id": "e0defe09-c1b2-9e95-8237-67df4bbab284", + "id": "26a08569-85f6-7f9a-726f-61be419c7a34", "headers": "", - "url": "http://localhost:5000/admin/connect/token", + "url": "http://localhost:5000/administration/connect/token", "preRequestScript": null, "pathVariables": {}, "method": "POST", + "data": [ + { + "key": "client_id", + "value": "raft", + "type": "text", + "enabled": true + }, + { + "key": "client_secret", + "value": "REALLYHARDPASSWORD", + "type": "text", + "enabled": true + }, + { + "key": "scope", + "value": "admin raft ", + "type": "text", + "enabled": true + }, + { + "key": "username", + "value": "admin", + "type": "text", + "enabled": false + }, + { + "key": "password", + "value": "secret", + "type": "text", + "enabled": false + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text", + "enabled": true + } + ], + "dataMode": "params", + "tests": "var jsonData = JSON.parse(responseBody);\npostman.setGlobalVariable(\"AccessToken\", jsonData.access_token);\npostman.setGlobalVariable(\"RefreshToken\", jsonData.refresh_token);", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1513240031907, + "name": "POST http://localhost:5000/admin/connect/token copy copy", + "description": "", + "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" + }, + { + "folder": null, + "id": "4684c2fa-f38c-c193-5f55-bf563a1978c6", + "name": "DELETE http://localhost:5000/posts/1", + "dataMode": "params", + "data": null, + "rawModeData": null, + "descriptionFormat": "html", + "description": "", + "headers": "", + "method": "DELETE", + "pathVariables": {}, + "url": "http://localhost:5000/posts/1", + "preRequestScript": null, + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" + }, + { + "id": "5f308240-79e3-cf74-7a6b-fe462f0d54f1", + "headers": "Authorization: Bearer {{AccessToken}}\n", + "url": "http://localhost:5000/administration/.well-known/openid-configuration", + "preRequestScript": null, + "pathVariables": {}, + "method": "GET", + "data": null, + "dataMode": "params", + "tests": null, + "currentHelper": "normal", + "helperAttributes": "{}", + "time": 1488038888813, + "name": "GET http://localhost:5000/admin/.well-known/openid-configuration", + "description": "", + "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", + "folder": null, + "rawModeData": null, + "descriptionFormat": null, + "queryParams": [], + "headerData": [ + { + "key": "Authorization", + "value": "Bearer {{AccessToken}}", + "description": "", + "enabled": true + } + ], + "pathVariableData": [] + }, + { + "id": "a1c95935-ed18-d5dc-bcb8-a3db8ba1934f", + "folder": null, + "name": "GET http://localhost:5000/posts", + "dataMode": "params", "data": [ { "key": "client_id", @@ -87,7 +191,7 @@ }, { "key": "password", - "value": "secret", + "value": "admin", "type": "text", "enabled": true }, @@ -98,20 +202,113 @@ "enabled": true } ], - "dataMode": "params", - "version": 2, - "tests": "var jsonData = JSON.parse(responseBody);\npostman.setGlobalVariable(\"AccessToken\", jsonData.access_token);\npostman.setGlobalVariable(\"RefreshToken\", jsonData.refresh_token);", + "rawModeData": null, + "descriptionFormat": "html", + "description": "", + "headers": "", + "method": "POST", + "pathVariables": {}, + "url": "http://localhost:5000/admin/configuration", + "preRequestScript": null, + "tests": null, "currentHelper": "normal", "helperAttributes": "{}", - "time": 1487515922748, - "name": "POST http://localhost:5000/admin/connect/token", - "description": "", - "collectionId": "23a49657-e24b-b967-7ec0-943ff1368680", - "responses": [], + "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" + }, + { + "folder": null, + "id": "c4494401-3985-a5bf-71fb-6e4171384ac6", + "name": "GET http://localhost:5000/posts/1/comments", + "dataMode": "params", + "data": null, "rawModeData": null, - "descriptionFormat": null, - "isFromCollection": true, - "collectionRequestId": "e23e29a1-6abb-abd3-141a-f2202e3f582b" + "descriptionFormat": "html", + "description": "", + "headers": "", + "method": "GET", + "pathVariables": {}, + "url": "http://localhost:5000/posts/1/comments", + "preRequestScript": null, + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" + }, + { + "folder": null, + "id": "c45d30d7-d9c4-fa05-8110-d6e769bb6ff9", + "name": "PATCH http://localhost:5000/posts/1", + "dataMode": "raw", + "data": [], + "descriptionFormat": "html", + "description": "", + "headers": "", + "method": "PATCH", + "pathVariables": {}, + "url": "http://localhost:5000/posts/1", + "preRequestScript": null, + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", + "rawModeData": "{\n \"title\": \"gfdgsgsdgsdfgsdfgdfg\",\n}" + }, + { + "folder": null, + "id": "e8825dc3-4137-99a7-0000-ef5786610dc3", + "name": "POST http://localhost:5000/posts/1", + "dataMode": "raw", + "data": [], + "descriptionFormat": "html", + "description": "", + "headers": "", + "method": "POST", + "pathVariables": {}, + "url": "http://localhost:5000/posts", + "preRequestScript": null, + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", + "rawModeData": "{\n \"userId\": 1,\n \"title\": \"test\",\n \"body\": \"test\"\n}" + }, + { + "folder": null, + "id": "ea0ed57a-2cb9-8acc-47dd-006b8db2f1b2", + "name": "GET http://localhost:5000/posts/1", + "dataMode": "params", + "data": null, + "rawModeData": null, + "descriptionFormat": "html", + "description": "", + "headers": "", + "method": "GET", + "pathVariables": {}, + "url": "http://localhost:5000/posts/1", + "preRequestScript": null, + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" + }, + { + "folder": null, + "id": "fddfc4fa-5114-69e3-4744-203ed71a526b", + "name": "PUT http://localhost:5000/posts/1", + "dataMode": "raw", + "data": [], + "descriptionFormat": "html", + "description": "", + "headers": "", + "method": "PUT", + "pathVariables": {}, + "url": "http://localhost:5000/posts/1", + "preRequestScript": null, + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", + "rawModeData": "{\n \"userId\": 1,\n \"title\": \"test\",\n \"body\": \"test\"\n}" } ] } \ No newline at end of file diff --git a/src/Ocelot/Authentication/BearerToken.cs b/src/Ocelot/Authentication/BearerToken.cs new file mode 100644 index 00000000..8ac4e200 --- /dev/null +++ b/src/Ocelot/Authentication/BearerToken.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Ocelot.Authentication +{ + class BearerToken + { + [JsonProperty("access_token")] + public string AccessToken { get; set; } + + [JsonProperty("expires_in")] + public int ExpiresIn { get; set; } + + [JsonProperty("token_type")] + public string TokenType { get; set; } + } +} \ No newline at end of file diff --git a/src/Ocelot/Controllers/OutputCacheController.cs b/src/Ocelot/Cache/OutputCacheController.cs similarity index 95% rename from src/Ocelot/Controllers/OutputCacheController.cs rename to src/Ocelot/Cache/OutputCacheController.cs index 8d5189c7..2dafcb66 100644 --- a/src/Ocelot/Controllers/OutputCacheController.cs +++ b/src/Ocelot/Cache/OutputCacheController.cs @@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Mvc; using Ocelot.Cache; using Ocelot.Configuration.Provider; -namespace Ocelot.Controllers +namespace Ocelot.Cache { [Authorize] [Route("outputcache")] diff --git a/src/Ocelot/Configuration/Authentication/OcelotResourceOwnerPasswordValidator.cs b/src/Ocelot/Configuration/Authentication/OcelotResourceOwnerPasswordValidator.cs deleted file mode 100644 index 416c8ec2..00000000 --- a/src/Ocelot/Configuration/Authentication/OcelotResourceOwnerPasswordValidator.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using IdentityServer4.Models; -using IdentityServer4.Validation; -using Ocelot.Configuration.Provider; - -namespace Ocelot.Configuration.Authentication -{ - public class OcelotResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator - { - private readonly IHashMatcher _matcher; - private readonly IIdentityServerConfiguration _identityServerConfiguration; - - public OcelotResourceOwnerPasswordValidator(IHashMatcher matcher, IIdentityServerConfiguration identityServerConfiguration) - { - _identityServerConfiguration = identityServerConfiguration; - _matcher = matcher; - } - - public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) - { - try - { - var user = _identityServerConfiguration.Users.FirstOrDefault(u => u.UserName == context.UserName); - - if(user == null) - { - context.Result = new GrantValidationResult( - TokenRequestErrors.InvalidGrant, - "invalid custom credential"); - } - else if(_matcher.Match(context.Password, user.Salt, user.Hash)) - { - context.Result = new GrantValidationResult( - subject: "admin", - authenticationMethod: "custom"); - } - else - { - context.Result = new GrantValidationResult( - TokenRequestErrors.InvalidGrant, - "invalid custom credential"); - } - } - catch(Exception ex) - { - Console.WriteLine(ex); - } - - } - } -} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs index 5e3f4b44..eed04cdb 100644 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs @@ -9,6 +9,7 @@ using Ocelot.Configuration.Builder; using Ocelot.Configuration.File; using Ocelot.Configuration.Parser; using Ocelot.Configuration.Validator; +using Ocelot.DependencyInjection; using Ocelot.LoadBalancer; using Ocelot.LoadBalancer.LoadBalancers; using Ocelot.Logging; @@ -35,6 +36,8 @@ namespace Ocelot.Configuration.Creator private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator; private readonly IRegionCreator _regionCreator; private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; + private readonly IAdministrationPath _adminPath; + public FileOcelotConfigurationCreator( IOptions options, @@ -49,9 +52,11 @@ namespace Ocelot.Configuration.Creator IReRouteOptionsCreator fileReRouteOptionsCreator, IRateLimitOptionsCreator rateLimitOptionsCreator, IRegionCreator regionCreator, - IHttpHandlerOptionsCreator httpHandlerOptionsCreator + IHttpHandlerOptionsCreator httpHandlerOptionsCreator, + IAdministrationPath adminPath ) { + _adminPath = adminPath; _regionCreator = regionCreator; _rateLimitOptionsCreator = rateLimitOptionsCreator; _requestIdKeyCreator = requestIdKeyCreator; @@ -92,7 +97,7 @@ namespace Ocelot.Configuration.Creator var serviceProviderConfiguration = _serviceProviderConfigCreator.Create(fileConfiguration.GlobalConfiguration); - var config = new OcelotConfiguration(reRoutes, fileConfiguration.GlobalConfiguration.AdministrationPath, serviceProviderConfiguration); + var config = new OcelotConfiguration(reRoutes, _adminPath.Path, serviceProviderConfiguration); return new OkResponse(config); } diff --git a/src/Ocelot/Configuration/Creator/IdentityServerConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/IdentityServerConfigurationCreator.cs index 6ed3b6c0..c414c0a5 100644 --- a/src/Ocelot/Configuration/Creator/IdentityServerConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/IdentityServerConfigurationCreator.cs @@ -8,29 +8,16 @@ namespace Ocelot.Configuration.Creator { public static class IdentityServerConfigurationCreator { - public static IdentityServerConfiguration GetIdentityServerConfiguration() + public static IdentityServerConfiguration GetIdentityServerConfiguration(string secret) { - var username = Environment.GetEnvironmentVariable("OCELOT_USERNAME"); - var hash = Environment.GetEnvironmentVariable("OCELOT_HASH"); - var salt = Environment.GetEnvironmentVariable("OCELOT_SALT"); var credentialsSigningCertificateLocation = Environment.GetEnvironmentVariable("OCELOT_CERTIFICATE"); var credentialsSigningCertificatePassword = Environment.GetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD"); return new IdentityServerConfiguration( "admin", false, - SupportedTokens.Both, - "secret", + secret, new List { "admin", "openid", "offline_access" }, - "Ocelot Administration", - true, - GrantTypes.ResourceOwnerPassword, - AccessTokenType.Jwt, - false, - new List - { - new User("admin", username, hash, salt) - }, credentialsSigningCertificateLocation, credentialsSigningCertificatePassword ); diff --git a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs index 4d34f6de..4bb9e191 100644 --- a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs +++ b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs @@ -12,7 +12,6 @@ namespace Ocelot.Configuration.File public string RequestIdKey { get; set; } public FileServiceDiscoveryProvider ServiceDiscoveryProvider {get;set;} - public string AdministrationPath {get;set;} public FileRateLimitOptions RateLimitOptions { get; set; } } diff --git a/src/Ocelot/Controllers/FileConfigurationController.cs b/src/Ocelot/Configuration/FileConfigurationController.cs similarity index 55% rename from src/Ocelot/Controllers/FileConfigurationController.cs rename to src/Ocelot/Configuration/FileConfigurationController.cs index c0ba43ea..e17d9e5c 100644 --- a/src/Ocelot/Controllers/FileConfigurationController.cs +++ b/src/Ocelot/Configuration/FileConfigurationController.cs @@ -1,11 +1,15 @@ +using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration.File; using Ocelot.Configuration.Provider; using Ocelot.Configuration.Setter; +using Ocelot.Raft; +using Rafty.Concensus; -namespace Ocelot.Controllers +namespace Ocelot.Configuration { [Authorize] [Route("configuration")] @@ -13,11 +17,13 @@ namespace Ocelot.Controllers { private readonly IFileConfigurationProvider _configGetter; private readonly IFileConfigurationSetter _configSetter; + private readonly IServiceProvider _serviceProvider; - public FileConfigurationController(IFileConfigurationProvider getFileConfig, IFileConfigurationSetter configSetter) + public FileConfigurationController(IFileConfigurationProvider getFileConfig, IFileConfigurationSetter configSetter, IServiceProvider serviceProvider) { _configGetter = getFileConfig; _configSetter = configSetter; + _serviceProvider = serviceProvider; } [HttpGet] @@ -36,9 +42,23 @@ namespace Ocelot.Controllers [HttpPost] public async Task Post([FromBody]FileConfiguration fileConfiguration) { + //todo - this code is a bit shit sort it out.. + var test = _serviceProvider.GetService(typeof(INode)); + if (test != null) + { + var node = (INode)test; + var result = node.Accept(new UpdateFileConfiguration(fileConfiguration)); + if (result.GetType() == typeof(Rafty.Concensus.ErrorResponse)) + { + return new BadRequestObjectResult("There was a problem. This error message sucks raise an issue in GitHub."); + } + + return new OkObjectResult(result.Command.Configuration); + } + var response = await _configSetter.Set(fileConfiguration); - - if(response.IsError) + + if (response.IsError) { return new BadRequestObjectResult(response.Errors); } @@ -46,4 +66,4 @@ namespace Ocelot.Controllers return new OkObjectResult(fileConfiguration); } } -} \ No newline at end of file +} diff --git a/src/Ocelot/Configuration/Provider/IIdentityServerConfiguration.cs b/src/Ocelot/Configuration/Provider/IIdentityServerConfiguration.cs index 0a388abb..a01ed751 100644 --- a/src/Ocelot/Configuration/Provider/IIdentityServerConfiguration.cs +++ b/src/Ocelot/Configuration/Provider/IIdentityServerConfiguration.cs @@ -7,16 +7,9 @@ namespace Ocelot.Configuration.Provider public interface IIdentityServerConfiguration { string ApiName { get; } + string ApiSecret { get; } bool RequireHttps { get; } List AllowedScopes { get; } - SupportedTokens SupportedTokens { get; } - string ApiSecret { get; } - string Description {get;} - bool Enabled {get;} - IEnumerable AllowedGrantTypes {get;} - AccessTokenType AccessTokenType {get;} - bool RequireClientSecret {get;} - List Users {get;} string CredentialsSigningCertificateLocation { get; } string CredentialsSigningCertificatePassword { get; } } diff --git a/src/Ocelot/Configuration/Provider/IdentityServerConfiguration.cs b/src/Ocelot/Configuration/Provider/IdentityServerConfiguration.cs index 881d6f5a..6f62e53c 100644 --- a/src/Ocelot/Configuration/Provider/IdentityServerConfiguration.cs +++ b/src/Ocelot/Configuration/Provider/IdentityServerConfiguration.cs @@ -9,27 +9,15 @@ namespace Ocelot.Configuration.Provider public IdentityServerConfiguration( string apiName, bool requireHttps, - SupportedTokens supportedTokens, string apiSecret, List allowedScopes, - string description, - bool enabled, - IEnumerable grantType, - AccessTokenType accessTokenType, - bool requireClientSecret, - List users, string credentialsSigningCertificateLocation, string credentialsSigningCertificatePassword) + string credentialsSigningCertificateLocation, + string credentialsSigningCertificatePassword) { ApiName = apiName; RequireHttps = requireHttps; - SupportedTokens = supportedTokens; ApiSecret = apiSecret; AllowedScopes = allowedScopes; - Description = description; - Enabled = enabled; - AllowedGrantTypes = grantType; - AccessTokenType = accessTokenType; - RequireClientSecret = requireClientSecret; - Users = users; CredentialsSigningCertificateLocation = credentialsSigningCertificateLocation; CredentialsSigningCertificatePassword = credentialsSigningCertificatePassword; } @@ -37,14 +25,7 @@ namespace Ocelot.Configuration.Provider public string ApiName { get; private set; } public bool RequireHttps { get; private set; } public List AllowedScopes { get; private set; } - public SupportedTokens SupportedTokens { get; private set; } public string ApiSecret { get; private set; } - public string Description {get;private set;} - public bool Enabled {get;private set;} - public IEnumerable AllowedGrantTypes {get;private set;} - public AccessTokenType AccessTokenType {get;private set;} - public bool RequireClientSecret {get;private set;} - public List Users {get;private set;} public string CredentialsSigningCertificateLocation { get; private set; } public string CredentialsSigningCertificatePassword { get; private set; } } diff --git a/src/Ocelot/Configuration/Provider/User.cs b/src/Ocelot/Configuration/Provider/User.cs deleted file mode 100644 index f61ff4e5..00000000 --- a/src/Ocelot/Configuration/Provider/User.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Ocelot.Configuration.Provider -{ - public class User - { - public User(string subject, string userName, string hash, string salt) - { - Subject = subject; - UserName = userName; - Hash = hash; - Salt = salt; - } - public string Subject { get; private set; } - public string UserName { get; private set; } - public string Hash { get; private set; } - public string Salt { get; private set; } - } -} \ No newline at end of file diff --git a/src/Ocelot/DependencyInjection/IOcelotBuilder.cs b/src/Ocelot/DependencyInjection/IOcelotBuilder.cs index 90877af1..2f4a58fb 100644 --- a/src/Ocelot/DependencyInjection/IOcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/IOcelotBuilder.cs @@ -7,5 +7,6 @@ namespace Ocelot.DependencyInjection { IOcelotBuilder AddStoreOcelotConfigurationInConsul(); IOcelotBuilder AddCacheManager(Action settings); + IOcelotAdministrationBuilder AddAdministration(string path, string secret); } } diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index b9801a00..b8ebc8a2 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -14,7 +14,6 @@ using Ocelot.Configuration.Provider; using Ocelot.Configuration.Repository; using Ocelot.Configuration.Setter; using Ocelot.Configuration.Validator; -using Ocelot.Controllers; using Ocelot.DownstreamRouteFinder.Finder; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.DownstreamUrlCreator; @@ -47,6 +46,12 @@ using Ocelot.Configuration.Builder; using FileConfigurationProvider = Ocelot.Configuration.Provider.FileConfigurationProvider; using Microsoft.Extensions.DependencyInjection.Extensions; using System.Linq; +using Ocelot.Raft; +using Rafty.Concensus; +using Rafty.FiniteStateMachine; +using Rafty.Infrastructure; +using Rafty.Log; +using Newtonsoft.Json; namespace Ocelot.DependencyInjection { @@ -121,14 +126,6 @@ namespace Ocelot.DependencyInjection _services.AddMemoryCache(); _services.TryAddSingleton(); - //add identity server for admin area - var identityServerConfiguration = IdentityServerConfigurationCreator.GetIdentityServerConfiguration(); - - if (identityServerConfiguration != null) - { - AddIdentityServer(identityServerConfiguration); - } - //add asp.net services.. var assembly = typeof(FileConfigurationController).GetTypeInfo().Assembly; @@ -141,6 +138,24 @@ namespace Ocelot.DependencyInjection _services.AddLogging(); _services.AddMiddlewareAnalysis(); _services.AddWebEncoders(); + _services.AddSingleton(new NullAdministrationPath()); + } + + public IOcelotAdministrationBuilder AddAdministration(string path, string secret) + { + var administrationPath = new AdministrationPath(path); + + //add identity server for admin area + var identityServerConfiguration = IdentityServerConfigurationCreator.GetIdentityServerConfiguration(secret); + + if (identityServerConfiguration != null) + { + AddIdentityServer(identityServerConfiguration, administrationPath); + } + + var descriptor = new ServiceDescriptor(typeof(IAdministrationPath), administrationPath); + _services.Replace(descriptor); + return new OcelotAdministrationBuilder(_services, _configurationRoot); } public IOcelotBuilder AddStoreOcelotConfigurationInConsul() @@ -185,7 +200,7 @@ namespace Ocelot.DependencyInjection return this; } - private void AddIdentityServer(IIdentityServerConfiguration identityServerConfiguration) + private void AddIdentityServer(IIdentityServerConfiguration identityServerConfiguration, IAdministrationPath adminPath) { _services.TryAddSingleton(identityServerConfiguration); _services.TryAddSingleton(); @@ -194,8 +209,7 @@ namespace Ocelot.DependencyInjection o.IssuerUri = "Ocelot"; }) .AddInMemoryApiResources(Resources(identityServerConfiguration)) - .AddInMemoryClients(Client(identityServerConfiguration)) - .AddResourceOwnerValidator(); + .AddInMemoryClients(Client(identityServerConfiguration)); //todo - refactor a method so we know why this is happening var whb = _services.First(x => x.ServiceType == typeof(IWebHostBuilder)); @@ -206,8 +220,7 @@ namespace Ocelot.DependencyInjection _services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) .AddIdentityServerAuthentication(o => { - var adminPath = _configurationRoot.GetValue("GlobalConfiguration:AdministrationPath", string.Empty); - o.Authority = baseSchemeUrlAndPort + adminPath; + o.Authority = baseSchemeUrlAndPort + adminPath.Path; o.ApiName = identityServerConfiguration.ApiName; o.RequireHttpsMetadata = identityServerConfiguration.RequireHttps; o.SupportedTokens = SupportedTokens.Both; @@ -240,7 +253,7 @@ namespace Ocelot.DependencyInjection Value = identityServerConfiguration.ApiSecret.Sha256() } } - } + }, }; } @@ -251,12 +264,65 @@ namespace Ocelot.DependencyInjection new Client { ClientId = identityServerConfiguration.ApiName, - AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + AllowedGrantTypes = GrantTypes.ClientCredentials, ClientSecrets = new List {new Secret(identityServerConfiguration.ApiSecret.Sha256())}, AllowedScopes = { identityServerConfiguration.ApiName } } }; } + } + public interface IOcelotAdministrationBuilder + { + IOcelotAdministrationBuilder AddRafty(); + } + + public class OcelotAdministrationBuilder : IOcelotAdministrationBuilder + { + private IServiceCollection _services; + private IConfigurationRoot _configurationRoot; + + public OcelotAdministrationBuilder(IServiceCollection services, IConfigurationRoot configurationRoot) + { + _configurationRoot = configurationRoot; + _services = services; + } + + public IOcelotAdministrationBuilder AddRafty() + { + var settings = new InMemorySettings(4000, 5000, 100, 5000); + _services.AddSingleton(); + _services.AddSingleton(); + _services.AddSingleton(settings); + _services.AddSingleton(); + _services.AddSingleton(); + _services.Configure(_configurationRoot); + return this; + } + } + + public interface IAdministrationPath + { + string Path {get;} + } + + public class NullAdministrationPath : IAdministrationPath + { + public NullAdministrationPath() + { + Path = null; + } + + public string Path {get;private set;} + } + + public class AdministrationPath : IAdministrationPath + { + public AdministrationPath(string path) + { + Path = path; + } + + public string Path {get;private set;} } } diff --git a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs index 4929f9b2..ab029950 100644 --- a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs +++ b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs @@ -7,7 +7,6 @@ using Microsoft.Extensions.DependencyInjection; using Ocelot.Authentication.Middleware; using Ocelot.Cache.Middleware; using Ocelot.Claims.Middleware; -using Ocelot.Controllers; using Ocelot.DownstreamRouteFinder.Middleware; using Ocelot.DownstreamUrlCreator.Middleware; using Ocelot.Errors.Middleware; @@ -23,12 +22,15 @@ using Ocelot.RateLimit.Middleware; namespace Ocelot.Middleware { using System; + using System.IO; using System.Linq; using System.Threading.Tasks; using Authorisation.Middleware; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; + using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; + using Newtonsoft.Json; using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; @@ -36,7 +38,10 @@ namespace Ocelot.Middleware using Ocelot.Configuration.Repository; using Ocelot.Configuration.Setter; using Ocelot.LoadBalancer.Middleware; + using Ocelot.Raft; using Ocelot.Responses; + using Rafty.Concensus; + using Rafty.Infrastructure; public static class OcelotMiddlewareExtensions { @@ -64,6 +69,11 @@ namespace Ocelot.Middleware await CreateAdministrationArea(builder, configuration); + if(UsingRafty(builder)) + { + SetUpRafty(builder); + } + ConfigureDiagnosticListener(builder); // This is registered to catch any global exceptions that are not handled @@ -149,6 +159,26 @@ namespace Ocelot.Middleware return builder; } + private static bool UsingRafty(IApplicationBuilder builder) + { + var possible = builder.ApplicationServices.GetService(typeof(INode)) as INode; + if(possible != null) + { + return true; + } + + return false; + } + + private static void SetUpRafty(IApplicationBuilder builder) + { + var applicationLifetime = (IApplicationLifetime)builder.ApplicationServices.GetService(typeof(IApplicationLifetime)); + applicationLifetime.ApplicationStopping.Register(() => OnShutdown(builder)); + var node = (INode)builder.ApplicationServices.GetService(typeof(INode)); + var nodeId = (NodeId)builder.ApplicationServices.GetService(typeof(NodeId)); + node.Start(nodeId.Id); + } + private static async Task CreateConfiguration(IApplicationBuilder builder) { var deps = GetDependencies(builder); @@ -183,7 +213,7 @@ namespace Ocelot.Middleware return response == null || response.IsError; } - private static bool ConfigurationNotSetUp(Response ocelotConfiguration) + private static bool ConfigurationNotSetUp(Ocelot.Responses.Response ocelotConfiguration) { return ocelotConfiguration == null || ocelotConfiguration.Data == null || ocelotConfiguration.IsError; } @@ -247,6 +277,7 @@ namespace Ocelot.Middleware return new ErrorResponse(ocelotConfig.Errors); } config = await ocelotConfigurationRepository.AddOrReplace(ocelotConfig.Data); + //todo - this starts the poller if it has been registered...please this is so bad. var hack = builder.ApplicationServices.GetService(typeof(ConsulFileConfigurationPoller)); } @@ -292,5 +323,11 @@ namespace Ocelot.Middleware diagnosticListener.SubscribeWithAdapter(listener); } } + + private static void OnShutdown(IApplicationBuilder app) + { + var node = (INode)app.ApplicationServices.GetService(typeof(INode)); + node.Stop(); + } } } diff --git a/src/Ocelot/Ocelot.csproj b/src/Ocelot/Ocelot.csproj index ca664327..adabe885 100644 --- a/src/Ocelot/Ocelot.csproj +++ b/src/Ocelot/Ocelot.csproj @@ -1,5 +1,4 @@ - - + netcoreapp2.0 2.0.0 @@ -11,39 +10,37 @@ Ocelot API Gateway;.NET core https://github.com/TomPallister/Ocelot - https://github.com/TomPallister/Ocelot + https://github.com/TomPallister/Ocelot win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 false false - True + True false - Tom Pallister + Tom Pallister - full True - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - + \ No newline at end of file diff --git a/src/Ocelot/Raft/ExcludeFromCoverage.cs b/src/Ocelot/Raft/ExcludeFromCoverage.cs new file mode 100644 index 00000000..9ea5544a --- /dev/null +++ b/src/Ocelot/Raft/ExcludeFromCoverage.cs @@ -0,0 +1,7 @@ +using System; + +namespace Ocelot.Raft +{ + [AttributeUsage(AttributeTargets.Class|AttributeTargets.Method|AttributeTargets.Property)] + public class ExcludeFromCoverageAttribute : Attribute{} +} \ No newline at end of file diff --git a/src/Ocelot/Raft/FakeCommand.cs b/src/Ocelot/Raft/FakeCommand.cs new file mode 100644 index 00000000..b8699c5e --- /dev/null +++ b/src/Ocelot/Raft/FakeCommand.cs @@ -0,0 +1,15 @@ +using Rafty.FiniteStateMachine; + +namespace Ocelot.Raft +{ + [ExcludeFromCoverage] + public class FakeCommand : ICommand + { + public FakeCommand(string value) + { + this.Value = value; + + } + public string Value { get; private set; } + } +} diff --git a/src/Ocelot/Raft/FileFsm.cs b/src/Ocelot/Raft/FileFsm.cs new file mode 100644 index 00000000..dbae10da --- /dev/null +++ b/src/Ocelot/Raft/FileFsm.cs @@ -0,0 +1,33 @@ +using System; +using System.IO; +using Newtonsoft.Json; +using Rafty.FiniteStateMachine; +using Rafty.Infrastructure; +using Rafty.Log; + +namespace Ocelot.Raft +{ + [ExcludeFromCoverage] + public class FileFsm : IFiniteStateMachine + { + private string _id; + + public FileFsm(NodeId nodeId) + { + _id = nodeId.Id.Replace("/","").Replace(":",""); + } + + public void Handle(LogEntry log) + { + try + { + var json = JsonConvert.SerializeObject(log.CommandData); + File.AppendAllText(_id, json); + } + catch(Exception exception) + { + Console.WriteLine(exception); + } + } + } +} diff --git a/src/Ocelot/Raft/FilePeer.cs b/src/Ocelot/Raft/FilePeer.cs new file mode 100644 index 00000000..f983d3cc --- /dev/null +++ b/src/Ocelot/Raft/FilePeer.cs @@ -0,0 +1,8 @@ +namespace Ocelot.Raft +{ + [ExcludeFromCoverage] + public class FilePeer + { + public string HostAndPort { get; set; } + } +} diff --git a/src/Ocelot/Raft/FilePeers.cs b/src/Ocelot/Raft/FilePeers.cs new file mode 100644 index 00000000..0aab1df4 --- /dev/null +++ b/src/Ocelot/Raft/FilePeers.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace Ocelot.Raft +{ + [ExcludeFromCoverage] + public class FilePeers + { + public FilePeers() + { + Peers = new List(); + } + + public List Peers {get; set;} + } +} diff --git a/src/Ocelot/Raft/FilePeersProvider.cs b/src/Ocelot/Raft/FilePeersProvider.cs new file mode 100644 index 00000000..413fdb42 --- /dev/null +++ b/src/Ocelot/Raft/FilePeersProvider.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Options; +using Ocelot.Configuration; +using Ocelot.Configuration.Provider; +using Rafty.Concensus; +using Rafty.Infrastructure; + +namespace Ocelot.Raft +{ + [ExcludeFromCoverage] + public class FilePeersProvider : IPeersProvider + { + private readonly IOptions _options; + private List _peers; + private IWebHostBuilder _builder; + private IOcelotConfigurationProvider _provider; + private IIdentityServerConfiguration _identityServerConfig; + + public FilePeersProvider(IOptions options, IWebHostBuilder builder, IOcelotConfigurationProvider provider, IIdentityServerConfiguration identityServerConfig) + { + _identityServerConfig = identityServerConfig; + _provider = provider; + _builder = builder; + _options = options; + _peers = new List(); + //todo - sort out async nonsense.. + var config = _provider.Get().GetAwaiter().GetResult(); + foreach (var item in _options.Value.Peers) + { + var httpClient = new HttpClient(); + //todo what if this errors? + var httpPeer = new HttpPeer(item.HostAndPort, httpClient, _builder, config.Data, _identityServerConfig); + _peers.Add(httpPeer); + } + } + public List Get() + { + return _peers; + } + } +} diff --git a/src/Ocelot/Raft/HttpPeer.cs b/src/Ocelot/Raft/HttpPeer.cs new file mode 100644 index 00000000..8ba8fe70 --- /dev/null +++ b/src/Ocelot/Raft/HttpPeer.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Newtonsoft.Json; +using Ocelot.Authentication; +using Ocelot.Configuration; +using Ocelot.Configuration.Provider; +using Rafty.Concensus; +using Rafty.FiniteStateMachine; + +namespace Ocelot.Raft +{ + [ExcludeFromCoverage] + public class HttpPeer : IPeer + { + private string _hostAndPort; + private HttpClient _httpClient; + private JsonSerializerSettings _jsonSerializerSettings; + private string _baseSchemeUrlAndPort; + private BearerToken _token; + private IOcelotConfiguration _config; + private IIdentityServerConfiguration _identityServerConfiguration; + + public HttpPeer(string hostAndPort, HttpClient httpClient, IWebHostBuilder builder, IOcelotConfiguration config, IIdentityServerConfiguration identityServerConfiguration) + { + _identityServerConfiguration = identityServerConfiguration; + _config = config; + Id = hostAndPort; + _hostAndPort = hostAndPort; + _httpClient = httpClient; + _jsonSerializerSettings = new JsonSerializerSettings() { + TypeNameHandling = TypeNameHandling.All + }; + _baseSchemeUrlAndPort = builder.GetSetting(WebHostDefaults.ServerUrlsKey); + } + + public string Id {get; private set;} + + public RequestVoteResponse Request(RequestVote requestVote) + { + if(_token == null) + { + SetToken(); + } + + var json = JsonConvert.SerializeObject(requestVote, _jsonSerializerSettings); + var content = new StringContent(json); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + var response = _httpClient.PostAsync($"{_hostAndPort}/administration/raft/requestvote", content).GetAwaiter().GetResult(); + if(response.IsSuccessStatusCode) + { + return JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult(), _jsonSerializerSettings); + } + else + { + return new RequestVoteResponse(false, requestVote.Term); + } + } + + public AppendEntriesResponse Request(AppendEntries appendEntries) + { + try + { + if(_token == null) + { + SetToken(); + } + var json = JsonConvert.SerializeObject(appendEntries, _jsonSerializerSettings); + var content = new StringContent(json); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + var response = _httpClient.PostAsync($"{_hostAndPort}/administration/raft/appendEntries", content).GetAwaiter().GetResult(); + if(response.IsSuccessStatusCode) + { + return JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult(),_jsonSerializerSettings); + } + else + { + return new AppendEntriesResponse(appendEntries.Term, false); + } + } + catch(Exception ex) + { + Console.WriteLine(ex); + return new AppendEntriesResponse(appendEntries.Term, false); + } + } + + public Response Request(T command) where T : ICommand + { + if(_token == null) + { + SetToken(); + } + var json = JsonConvert.SerializeObject(command, _jsonSerializerSettings); + var content = new StringContent(json); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + var response = _httpClient.PostAsync($"{_hostAndPort}/administration/raft/command", content).GetAwaiter().GetResult(); + if(response.IsSuccessStatusCode) + { + return JsonConvert.DeserializeObject>(response.Content.ReadAsStringAsync().GetAwaiter().GetResult(), _jsonSerializerSettings); + } + else + { + return new ErrorResponse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult(), command); + } + } + + private void SetToken() + { + var tokenUrl = $"{_baseSchemeUrlAndPort}{_config.AdministrationPath}/connect/token"; + var formData = new List> + { + new KeyValuePair("client_id", _identityServerConfiguration.ApiName), + new KeyValuePair("client_secret", _identityServerConfiguration.ApiSecret), + new KeyValuePair("scope", _identityServerConfiguration.ApiName), + new KeyValuePair("grant_type", "client_credentials") + }; + var content = new FormUrlEncodedContent(formData); + var response = _httpClient.PostAsync(tokenUrl, content).GetAwaiter().GetResult(); + var responseContent = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(_token.TokenType, _token.AccessToken); + } + } +} diff --git a/src/Ocelot/Raft/OcelotFiniteStateMachine.cs b/src/Ocelot/Raft/OcelotFiniteStateMachine.cs new file mode 100644 index 00000000..96a9ceb1 --- /dev/null +++ b/src/Ocelot/Raft/OcelotFiniteStateMachine.cs @@ -0,0 +1,25 @@ +using Ocelot.Configuration.Setter; +using Rafty.FiniteStateMachine; +using Rafty.Log; + +namespace Ocelot.Raft +{ + [ExcludeFromCoverage] + public class OcelotFiniteStateMachine : IFiniteStateMachine + { + private IFileConfigurationSetter _setter; + + public OcelotFiniteStateMachine(IFileConfigurationSetter setter) + { + _setter = setter; + } + + public void Handle(LogEntry log) + { + //todo - handle an error + //hack it to just cast as at the moment we know this is the only command :P + var hack = (UpdateFileConfiguration)log.CommandData; + _setter.Set(hack.Configuration).GetAwaiter().GetResult();; + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Raft/RaftController.cs b/src/Ocelot/Raft/RaftController.cs new file mode 100644 index 00000000..08ee0c34 --- /dev/null +++ b/src/Ocelot/Raft/RaftController.cs @@ -0,0 +1,84 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Ocelot.Logging; +using Ocelot.Raft; +using Rafty.Concensus; +using Rafty.FiniteStateMachine; + +namespace Ocelot.Raft +{ + [ExcludeFromCoverage] + [Authorize] + [Route("raft")] + public class RaftController : Controller + { + private readonly INode _node; + private IOcelotLogger _logger; + private string _baseSchemeUrlAndPort; + private JsonSerializerSettings _jsonSerialiserSettings; + + public RaftController(INode node, IOcelotLoggerFactory loggerFactory, IWebHostBuilder builder) + { + _jsonSerialiserSettings = new JsonSerializerSettings { + TypeNameHandling = TypeNameHandling.All + }; + _baseSchemeUrlAndPort = builder.GetSetting(WebHostDefaults.ServerUrlsKey); + _logger = loggerFactory.CreateLogger(); + _node = node; + } + + [Route("appendentries")] + public async Task AppendEntries() + { + using(var reader = new StreamReader(HttpContext.Request.Body)) + { + var json = await reader.ReadToEndAsync(); + var appendEntries = JsonConvert.DeserializeObject(json, _jsonSerialiserSettings); + _logger.LogDebug($"{_baseSchemeUrlAndPort}/appendentries called, my state is {_node.State.GetType().FullName}"); + var appendEntriesResponse = _node.Handle(appendEntries); + return new OkObjectResult(appendEntriesResponse); + } + } + + [Route("requestvote")] + public async Task RequestVote() + { + using(var reader = new StreamReader(HttpContext.Request.Body)) + { + var json = await reader.ReadToEndAsync(); + var requestVote = JsonConvert.DeserializeObject(json, _jsonSerialiserSettings); + _logger.LogDebug($"{_baseSchemeUrlAndPort}/requestvote called, my state is {_node.State.GetType().FullName}"); + var requestVoteResponse = _node.Handle(requestVote); + return new OkObjectResult(requestVoteResponse); + } + } + + [Route("command")] + public async Task Command() + { + try + { + using(var reader = new StreamReader(HttpContext.Request.Body)) + { + var json = await reader.ReadToEndAsync(); + var command = JsonConvert.DeserializeObject(json, _jsonSerialiserSettings); + _logger.LogDebug($"{_baseSchemeUrlAndPort}/command called, my state is {_node.State.GetType().FullName}"); + var commandResponse = _node.Accept(command); + json = JsonConvert.SerializeObject(commandResponse, _jsonSerialiserSettings); + return StatusCode(200, json); + } + } + catch(Exception e) + { + _logger.LogError($"THERE WAS A PROBLEM ON NODE {_node.State.CurrentState.Id}", e); + throw e; + } + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Raft/SqlLiteLog.cs b/src/Ocelot/Raft/SqlLiteLog.cs new file mode 100644 index 00000000..aaa1e726 --- /dev/null +++ b/src/Ocelot/Raft/SqlLiteLog.cs @@ -0,0 +1,279 @@ +using System.IO; +using Rafty.Log; +using Microsoft.Data.Sqlite; +using Newtonsoft.Json; +using System; +using Rafty.Infrastructure; +using System.Collections.Generic; + +namespace Ocelot.Raft +{ + [ExcludeFromCoverage] + public class SqlLiteLog : ILog + { + private string _path; + private readonly object _lock = new object(); + + public SqlLiteLog(NodeId nodeId) + { + _path = $"{nodeId.Id.Replace("/","").Replace(":","")}.db"; + if(!File.Exists(_path)) + { + lock(_lock) + { + FileStream fs = File.Create(_path); + fs.Dispose(); + } + using(var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + var sql = @"create table logs ( + id integer primary key, + data text not null + )"; + using(var command = new SqliteCommand(sql, connection)) + { + var result = command.ExecuteNonQuery(); + } + } + } + } + + public int LastLogIndex + { + get + { + lock(_lock) + { + var result = 1; + using(var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + var sql = @"select id from logs order by id desc limit 1"; + using(var command = new SqliteCommand(sql, connection)) + { + var index = Convert.ToInt32(command.ExecuteScalar()); + if(index > result) + { + result = index; + } + } + } + return result; + } + } + } + + public long LastLogTerm + { + get + { + lock(_lock) + { + long result = 0; + using(var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + var sql = @"select data from logs order by id desc limit 1"; + using(var command = new SqliteCommand(sql, connection)) + { + var data = Convert.ToString(command.ExecuteScalar()); + var jsonSerializerSettings = new JsonSerializerSettings() { + TypeNameHandling = TypeNameHandling.All + }; + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + if(log != null && log.Term > result) + { + result = log.Term; + } + } + } + return result; + } + } + } + + public int Count + { + get + { + lock(_lock) + { + var result = 0; + using(var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + var sql = @"select count(id) from logs"; + using(var command = new SqliteCommand(sql, connection)) + { + var index = Convert.ToInt32(command.ExecuteScalar()); + if(index > result) + { + result = index; + } + } + } + return result; + } + } + } + + public int Apply(LogEntry log) + { + lock(_lock) + { + using(var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + var jsonSerializerSettings = new JsonSerializerSettings() { + TypeNameHandling = TypeNameHandling.All + }; + var data = JsonConvert.SerializeObject(log, jsonSerializerSettings); + //todo - sql injection dont copy this.. + var sql = $"insert into logs (data) values ('{data}')"; + using(var command = new SqliteCommand(sql, connection)) + { + var result = command.ExecuteNonQuery(); + } + + sql = "select last_insert_rowid()"; + using(var command = new SqliteCommand(sql, connection)) + { + var result = command.ExecuteScalar(); + return Convert.ToInt32(result); + } + } + } + } + + public void DeleteConflictsFromThisLog(int index, LogEntry logEntry) + { + lock(_lock) + { + using(var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + //todo - sql injection dont copy this.. + var sql = $"select data from logs where id = {index};"; + using(var command = new SqliteCommand(sql, connection)) + { + var data = Convert.ToString(command.ExecuteScalar()); + var jsonSerializerSettings = new JsonSerializerSettings() { + TypeNameHandling = TypeNameHandling.All + }; + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + if(logEntry != null && log != null && logEntry.Term != log.Term) + { + //todo - sql injection dont copy this.. + var deleteSql = $"delete from logs where id >= {index};"; + using(var deleteCommand = new SqliteCommand(deleteSql, connection)) + { + var result = deleteCommand.ExecuteNonQuery(); + } + } + } + } + } + } + + public LogEntry Get(int index) + { + lock(_lock) + { + using(var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + //todo - sql injection dont copy this.. + var sql = $"select data from logs where id = {index}"; + using(var command = new SqliteCommand(sql, connection)) + { + var data = Convert.ToString(command.ExecuteScalar()); + var jsonSerializerSettings = new JsonSerializerSettings() { + TypeNameHandling = TypeNameHandling.All + }; + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + return log; + } + } + } + } + + public System.Collections.Generic.List<(int index, LogEntry logEntry)> GetFrom(int index) + { + lock(_lock) + { + var logsToReturn = new List<(int, LogEntry)>(); + + using(var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + //todo - sql injection dont copy this.. + var sql = $"select id, data from logs where id >= {index}"; + using(var command = new SqliteCommand(sql, connection)) + { + using(var reader = command.ExecuteReader()) + { + while(reader.Read()) + { + var id = Convert.ToInt32(reader[0]); + var data = (string)reader[1]; + var jsonSerializerSettings = new JsonSerializerSettings() { + TypeNameHandling = TypeNameHandling.All + }; + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + logsToReturn.Add((id, log)); + + } + } + } + } + + return logsToReturn; + } + + } + + public long GetTermAtIndex(int index) + { + lock(_lock) + { + long result = 0; + using(var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + //todo - sql injection dont copy this.. + var sql = $"select data from logs where id = {index}"; + using(var command = new SqliteCommand(sql, connection)) + { + var data = Convert.ToString(command.ExecuteScalar()); + var jsonSerializerSettings = new JsonSerializerSettings() { + TypeNameHandling = TypeNameHandling.All + }; + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + if(log != null && log.Term > result) + { + result = log.Term; + } + } + } + return result; + } + } + public void Remove(int indexOfCommand) + { + lock(_lock) + { + using(var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + //todo - sql injection dont copy this.. + var deleteSql = $"delete from logs where id >= {indexOfCommand};"; + using(var deleteCommand = new SqliteCommand(deleteSql, connection)) + { + var result = deleteCommand.ExecuteNonQuery(); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Raft/UpdateFileConfiguration.cs b/src/Ocelot/Raft/UpdateFileConfiguration.cs new file mode 100644 index 00000000..39ed73f9 --- /dev/null +++ b/src/Ocelot/Raft/UpdateFileConfiguration.cs @@ -0,0 +1,15 @@ +using Ocelot.Configuration.File; +using Rafty.FiniteStateMachine; + +namespace Ocelot.Raft +{ + public class UpdateFileConfiguration : ICommand + { + public UpdateFileConfiguration(FileConfiguration configuration) + { + Configuration = configuration; + } + + public FileConfiguration Configuration {get;private set;} + } +} \ No newline at end of file diff --git a/test/Ocelot.AcceptanceTests/Startup.cs b/test/Ocelot.AcceptanceTests/AcceptanceTestsStartup.cs similarity index 90% rename from test/Ocelot.AcceptanceTests/Startup.cs rename to test/Ocelot.AcceptanceTests/AcceptanceTestsStartup.cs index 9c8a6e77..bae6fb34 100644 --- a/test/Ocelot.AcceptanceTests/Startup.cs +++ b/test/Ocelot.AcceptanceTests/AcceptanceTestsStartup.cs @@ -12,9 +12,9 @@ using Ocelot.AcceptanceTests.Caching; namespace Ocelot.AcceptanceTests { - public class Startup + public class AcceptanceTestsStartup { - public Startup(IHostingEnvironment env) + public AcceptanceTestsStartup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) @@ -41,7 +41,7 @@ namespace Ocelot.AcceptanceTests } } - public class Startup_WithCustomCacheHandle : Startup + public class Startup_WithCustomCacheHandle : AcceptanceTestsStartup { public Startup_WithCustomCacheHandle(IHostingEnvironment env) : base(env) { } @@ -60,7 +60,7 @@ namespace Ocelot.AcceptanceTests } } - public class Startup_WithConsul_And_CustomCacheHandle : Startup + public class Startup_WithConsul_And_CustomCacheHandle : AcceptanceTestsStartup { public Startup_WithConsul_And_CustomCacheHandle(IHostingEnvironment env) : base(env) { } diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 83fe8456..71d3fb16 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -83,7 +83,7 @@ namespace Ocelot.AcceptanceTests }); _ocelotServer = new TestServer(_webHostBuilder - .UseStartup()); + .UseStartup()); _ocelotClient = _ocelotServer.CreateClient(); } @@ -103,7 +103,7 @@ namespace Ocelot.AcceptanceTests }); _ocelotServer = new TestServer(_webHostBuilder - .UseStartup()); + .UseStartup()); _ocelotClient = _ocelotServer.CreateClient(); } @@ -157,7 +157,6 @@ namespace Ocelot.AcceptanceTests { var response = JsonConvert.DeserializeObject(_response.Content.ReadAsStringAsync().Result); - response.GlobalConfiguration.AdministrationPath.ShouldBe(expected.GlobalConfiguration.AdministrationPath); response.GlobalConfiguration.RequestIdKey.ShouldBe(expected.GlobalConfiguration.RequestIdKey); response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Host); response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Port); diff --git a/test/Ocelot.IntegrationTests/AdministrationTests.cs b/test/Ocelot.IntegrationTests/AdministrationTests.cs index 240b7c25..b7c2813e 100644 --- a/test/Ocelot.IntegrationTests/AdministrationTests.cs +++ b/test/Ocelot.IntegrationTests/AdministrationTests.cs @@ -39,13 +39,7 @@ namespace Ocelot.IntegrationTests [Fact] public void should_return_response_401_with_call_re_routes_controller() { - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - AdministrationPath = "/administration" - } - }; + var configuration = new FileConfiguration(); this.Given(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) @@ -57,13 +51,7 @@ namespace Ocelot.IntegrationTests [Fact] public void should_return_response_200_with_call_re_routes_controller() { - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - AdministrationPath = "/administration" - } - }; + var configuration = new FileConfiguration(); this.Given(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) @@ -77,13 +65,7 @@ namespace Ocelot.IntegrationTests [Fact] public void should_be_able_to_use_token_from_ocelot_a_on_ocelot_b() { - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - AdministrationPath = "/administration" - } - }; + var configuration = new FileConfiguration(); this.Given(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenIdentityServerSigningEnvironmentalVariablesAreSet()) @@ -102,7 +84,6 @@ namespace Ocelot.IntegrationTests { GlobalConfiguration = new FileGlobalConfiguration { - AdministrationPath = "/administration", RequestIdKey = "RequestId", ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { @@ -160,7 +141,6 @@ namespace Ocelot.IntegrationTests { GlobalConfiguration = new FileGlobalConfiguration { - AdministrationPath = "/administration" }, ReRoutes = new List() { @@ -189,7 +169,6 @@ namespace Ocelot.IntegrationTests { GlobalConfiguration = new FileGlobalConfiguration { - AdministrationPath = "/administration" }, ReRoutes = new List() { @@ -234,7 +213,6 @@ namespace Ocelot.IntegrationTests { GlobalConfiguration = new FileGlobalConfiguration { - AdministrationPath = "/administration" }, ReRoutes = new List() { @@ -289,7 +267,7 @@ namespace Ocelot.IntegrationTests .ConfigureServices(x => { x.AddSingleton(_webHostBuilderTwo); }) - .UseStartup(); + .UseStartup(); _builderTwo = _webHostBuilderTwo.Build(); @@ -327,7 +305,6 @@ namespace Ocelot.IntegrationTests { var response = JsonConvert.DeserializeObject(_response.Content.ReadAsStringAsync().Result); - response.GlobalConfiguration.AdministrationPath.ShouldBe(expected.GlobalConfiguration.AdministrationPath); response.GlobalConfiguration.RequestIdKey.ShouldBe(expected.GlobalConfiguration.RequestIdKey); response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Host); response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Port); @@ -356,9 +333,7 @@ namespace Ocelot.IntegrationTests new KeyValuePair("client_id", "admin"), new KeyValuePair("client_secret", "secret"), new KeyValuePair("scope", "admin"), - new KeyValuePair("username", "admin"), - new KeyValuePair("password", "secret"), - new KeyValuePair("grant_type", "password") + new KeyValuePair("grant_type", "client_credentials") }; var content = new FormUrlEncodedContent(formData); @@ -380,7 +355,7 @@ namespace Ocelot.IntegrationTests .ConfigureServices(x => { x.AddSingleton(_webHostBuilder); }) - .UseStartup(); + .UseStartup(); _builder = _webHostBuilder.Build(); diff --git a/test/Ocelot.IntegrationTests/Startup.cs b/test/Ocelot.IntegrationTests/IntegrationTestsStartup.cs similarity index 85% rename from test/Ocelot.IntegrationTests/Startup.cs rename to test/Ocelot.IntegrationTests/IntegrationTestsStartup.cs index 60b99f02..097c1b5c 100644 --- a/test/Ocelot.IntegrationTests/Startup.cs +++ b/test/Ocelot.IntegrationTests/IntegrationTestsStartup.cs @@ -11,9 +11,9 @@ using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBui namespace Ocelot.IntegrationTests { - public class Startup + public class IntegrationTestsStartup { - public Startup(IHostingEnvironment env) + public IntegrationTestsStartup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) @@ -38,7 +38,9 @@ namespace Ocelot.IntegrationTests .WithDictionaryHandle(); }; - services.AddOcelot(Configuration); + services.AddOcelot(Configuration) + .AddCacheManager(settings) + .AddAdministration("/administration", "secret"); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) diff --git a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj index 5d1d4841..bf5b2fe4 100644 --- a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj +++ b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj @@ -1,5 +1,4 @@ - - + 0.0.0-dev netcoreapp2.0 @@ -13,39 +12,35 @@ false false - - + PreserveNewest - - + - - - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - + \ No newline at end of file diff --git a/test/Ocelot.IntegrationTests/RaftStartup.cs b/test/Ocelot.IntegrationTests/RaftStartup.cs new file mode 100644 index 00000000..25015358 --- /dev/null +++ b/test/Ocelot.IntegrationTests/RaftStartup.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using System.Linq; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; +using Ocelot.Raft; +using Rafty.Concensus; +using Rafty.FiniteStateMachine; +using Rafty.Infrastructure; +using Rafty.Log; +using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; + +namespace Ocelot.IntegrationTests +{ + public class RaftStartup + { + public RaftStartup(IHostingEnvironment env) + { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddJsonFile("peers.json", optional: true, reloadOnChange: true) + .AddJsonFile("configuration.json") + .AddEnvironmentVariables(); + + Configuration = builder.Build(); + } + + public IConfigurationRoot Configuration { get; } + + public virtual void ConfigureServices(IServiceCollection services) + { + services + .AddOcelot(Configuration) + .AddAdministration("/administration", "secret") + .AddRafty(); + } + + public virtual void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + + //this is from Ocelot...so we need to move stuff below into it... + loggerFactory.AddConsole(Configuration.GetSection("Logging")); + app.UseOcelot().Wait(); + } + } +} diff --git a/test/Ocelot.IntegrationTests/RaftTests.cs b/test/Ocelot.IntegrationTests/RaftTests.cs new file mode 100644 index 00000000..528b6d20 --- /dev/null +++ b/test/Ocelot.IntegrationTests/RaftTests.cs @@ -0,0 +1,431 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using Ocelot.Raft; +using Rafty.Concensus; +using Rafty.FiniteStateMachine; +using Rafty.Infrastructure; +using Shouldly; +using Xunit; +using static Rafty.Infrastructure.Wait; +using Microsoft.Data.Sqlite; + +namespace Ocelot.IntegrationTests +{ + public class RaftTests : IDisposable + { + private List _builders; + private List _webHostBuilders; + private List _threads; + private FilePeers _peers; + private HttpClient _httpClient; + private HttpClient _httpClientForAssertions; + private string _ocelotBaseUrl; + private BearerToken _token; + private HttpResponseMessage _response; + private static object _lock = new object(); + + public RaftTests() + { + _httpClientForAssertions = new HttpClient(); + _httpClient = new HttpClient(); + _ocelotBaseUrl = "http://localhost:5000"; + _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); + _webHostBuilders = new List(); + _builders = new List(); + _threads = new List(); + } + public void Dispose() + { + foreach (var builder in _builders) + { + builder?.Dispose(); + } + + foreach (var peer in _peers.Peers) + { + File.Delete(peer.HostAndPort.Replace("/","").Replace(":","")); + File.Delete($"{peer.HostAndPort.Replace("/","").Replace(":","")}.db"); + } + } + + [Fact] + public void should_persist_command_to_five_servers() + { + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + } + }; + + var updatedConfiguration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + }, + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHost = "127.0.0.1", + DownstreamPort = 80, + DownstreamScheme = "http", + DownstreamPathTemplate = "/geoffrey", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/" + }, + new FileReRoute() + { + DownstreamHost = "123.123.123", + DownstreamPort = 443, + DownstreamScheme = "https", + DownstreamPathTemplate = "/blooper/{productId}", + UpstreamHttpMethod = new List { "post" }, + UpstreamPathTemplate = "/test" + } + } + }; + + var command = new UpdateFileConfiguration(updatedConfiguration); + GivenThereIsAConfiguration(configuration); + GivenFiveServersAreRunning(); + GivenALeaderIsElected(); + GivenIHaveAnOcelotToken("/administration"); + WhenISendACommandIntoTheCluster(command); + ThenTheCommandIsReplicatedToAllStateMachines(command); + } + + [Fact] + public void should_persist_command_to_five_servers_when_using_administration_api() + { + var configuration = new FileConfiguration + { + }; + + var updatedConfiguration = new FileConfiguration + { + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHost = "127.0.0.1", + DownstreamPort = 80, + DownstreamScheme = "http", + DownstreamPathTemplate = "/geoffrey", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/" + }, + new FileReRoute() + { + DownstreamHost = "123.123.123", + DownstreamPort = 443, + DownstreamScheme = "https", + DownstreamPathTemplate = "/blooper/{productId}", + UpstreamHttpMethod = new List { "post" }, + UpstreamPathTemplate = "/test" + } + } + }; + + var command = new UpdateFileConfiguration(updatedConfiguration); + GivenThereIsAConfiguration(configuration); + GivenFiveServersAreRunning(); + GivenALeaderIsElected(); + GivenIHaveAnOcelotToken("/administration"); + GivenIHaveAddedATokenToMyRequest(); + WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration); + ThenTheCommandIsReplicatedToAllStateMachines(command); + } + + private void WhenISendACommandIntoTheCluster(UpdateFileConfiguration command) + { + var p = _peers.Peers.First(); + var json = JsonConvert.SerializeObject(command,new JsonSerializerSettings() { + TypeNameHandling = TypeNameHandling.All + }); + var httpContent = new StringContent(json); + httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + using(var httpClient = new HttpClient()) + { + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + var response = httpClient.PostAsync($"{p.HostAndPort}/administration/raft/command", httpContent).GetAwaiter().GetResult(); + response.EnsureSuccessStatusCode(); + var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + var result = JsonConvert.DeserializeObject>(content); + result.Command.Configuration.ReRoutes.Count.ShouldBe(2); + } + + //dirty sleep to make sure command replicated... + var stopwatch = Stopwatch.StartNew(); + while(stopwatch.ElapsedMilliseconds < 10000) + { + + } + } + + private void ThenTheCommandIsReplicatedToAllStateMachines(UpdateFileConfiguration expected) + { + //dirty sleep to give a chance to replicate... + var stopwatch = Stopwatch.StartNew(); + while(stopwatch.ElapsedMilliseconds < 2000) + { + + } + + bool CommandCalledOnAllStateMachines() + { + try + { + var passed = 0; + foreach (var peer in _peers.Peers) + { + var path = $"{peer.HostAndPort.Replace("/","").Replace(":","")}.db"; + using(var connection = new SqliteConnection($"Data Source={path};")) + { + connection.Open(); + var sql = @"select count(id) from logs"; + using(var command = new SqliteCommand(sql, connection)) + { + var index = Convert.ToInt32(command.ExecuteScalar()); + index.ShouldBe(1); + } + } + _httpClientForAssertions.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + var result = _httpClientForAssertions.GetAsync($"{peer.HostAndPort}/administration/configuration").Result; + var json = result.Content.ReadAsStringAsync().Result; + var response = JsonConvert.DeserializeObject(json, new JsonSerializerSettings{TypeNameHandling = TypeNameHandling.All}); + response.GlobalConfiguration.RequestIdKey.ShouldBe(expected.Configuration.GlobalConfiguration.RequestIdKey); + response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expected.Configuration.GlobalConfiguration.ServiceDiscoveryProvider.Host); + response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expected.Configuration.GlobalConfiguration.ServiceDiscoveryProvider.Port); + + for (var i = 0; i < response.ReRoutes.Count; i++) + { + response.ReRoutes[i].DownstreamHost.ShouldBe(expected.Configuration.ReRoutes[i].DownstreamHost); + response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expected.Configuration.ReRoutes[i].DownstreamPathTemplate); + response.ReRoutes[i].DownstreamPort.ShouldBe(expected.Configuration.ReRoutes[i].DownstreamPort); + response.ReRoutes[i].DownstreamScheme.ShouldBe(expected.Configuration.ReRoutes[i].DownstreamScheme); + response.ReRoutes[i].UpstreamPathTemplate.ShouldBe(expected.Configuration.ReRoutes[i].UpstreamPathTemplate); + response.ReRoutes[i].UpstreamHttpMethod.ShouldBe(expected.Configuration.ReRoutes[i].UpstreamHttpMethod); + } + passed++; + } + + return passed == 5; + } + catch(Exception e) + { + Console.WriteLine(e); + return false; + } + } + + var commandOnAllStateMachines = WaitFor(20000).Until(() => CommandCalledOnAllStateMachines()); + commandOnAllStateMachines.ShouldBeTrue(); + } + + private void ThenTheResponseShouldBe(FileConfiguration expected) + { + var response = JsonConvert.DeserializeObject(_response.Content.ReadAsStringAsync().Result); + + response.GlobalConfiguration.RequestIdKey.ShouldBe(expected.GlobalConfiguration.RequestIdKey); + response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Host); + response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Port); + + for (var i = 0; i < response.ReRoutes.Count; i++) + { + response.ReRoutes[i].DownstreamHost.ShouldBe(expected.ReRoutes[i].DownstreamHost); + response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expected.ReRoutes[i].DownstreamPathTemplate); + response.ReRoutes[i].DownstreamPort.ShouldBe(expected.ReRoutes[i].DownstreamPort); + response.ReRoutes[i].DownstreamScheme.ShouldBe(expected.ReRoutes[i].DownstreamScheme); + response.ReRoutes[i].UpstreamPathTemplate.ShouldBe(expected.ReRoutes[i].UpstreamPathTemplate); + response.ReRoutes[i].UpstreamHttpMethod.ShouldBe(expected.ReRoutes[i].UpstreamHttpMethod); + } + } + + private void WhenIGetUrlOnTheApiGateway(string url) + { + _response = _httpClient.GetAsync(url).Result; + } + + private void WhenIPostOnTheApiGateway(string url, FileConfiguration updatedConfiguration) + { + var json = JsonConvert.SerializeObject(updatedConfiguration); + var content = new StringContent(json); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + _response = _httpClient.PostAsync(url, content).Result; + } + + private void GivenIHaveAddedATokenToMyRequest() + { + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + } + + private void GivenIHaveAnOcelotToken(string adminPath) + { + var tokenUrl = $"{adminPath}/connect/token"; + var formData = new List> + { + new KeyValuePair("client_id", "admin"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "admin"), + new KeyValuePair("grant_type", "client_credentials") + }; + var content = new FormUrlEncodedContent(formData); + + var response = _httpClient.PostAsync(tokenUrl, content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + var configPath = $"{adminPath}/.well-known/openid-configuration"; + response = _httpClient.GetAsync(configPath).Result; + response.EnsureSuccessStatusCode(); + } + + private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) + { + var configurationPath = $"{Directory.GetCurrentDirectory()}/configuration.json"; + + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + + var text = File.ReadAllText(configurationPath); + + configurationPath = $"{AppContext.BaseDirectory}/configuration.json"; + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + + text = File.ReadAllText(configurationPath); + } + + private void GivenAServerIsRunning(string url) + { + lock(_lock) + { + IWebHostBuilder webHostBuilder = new WebHostBuilder(); + webHostBuilder.UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureServices(x => + { + x.AddSingleton(webHostBuilder); + x.AddSingleton(new NodeId(url)); + }) + .UseStartup(); + + var builder = webHostBuilder.Build(); + builder.Start(); + + _webHostBuilders.Add(webHostBuilder); + _builders.Add(builder); + } + } + + private void GivenFiveServersAreRunning() + { + var bytes = File.ReadAllText("peers.json"); + _peers = JsonConvert.DeserializeObject(bytes); + + foreach (var peer in _peers.Peers) + { + var thread = new Thread(() => GivenAServerIsRunning(peer.HostAndPort)); + thread.Start(); + _threads.Add(thread); + } + } + + private void GivenALeaderIsElected() + { + //dirty sleep to make sure we have a leader + var stopwatch = Stopwatch.StartNew(); + while(stopwatch.ElapsedMilliseconds < 20000) + { + + } + } + + private void WhenISendACommandIntoTheCluster(FakeCommand command) + { + var p = _peers.Peers.First(); + var json = JsonConvert.SerializeObject(command,new JsonSerializerSettings() { + TypeNameHandling = TypeNameHandling.All + }); + var httpContent = new StringContent(json); + httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + using(var httpClient = new HttpClient()) + { + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + var response = httpClient.PostAsync($"{p.HostAndPort}/administration/raft/command", httpContent).GetAwaiter().GetResult(); + response.EnsureSuccessStatusCode(); + var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + var result = JsonConvert.DeserializeObject>(content); + result.Command.Value.ShouldBe(command.Value); + } + + //dirty sleep to make sure command replicated... + var stopwatch = Stopwatch.StartNew(); + while(stopwatch.ElapsedMilliseconds < 10000) + { + + } + } + + private void ThenTheCommandIsReplicatedToAllStateMachines(FakeCommand command) + { + //dirty sleep to give a chance to replicate... + var stopwatch = Stopwatch.StartNew(); + while(stopwatch.ElapsedMilliseconds < 2000) + { + + } + + bool CommandCalledOnAllStateMachines() + { + try + { + var passed = 0; + foreach (var peer in _peers.Peers) + { + string fsmData; + fsmData = File.ReadAllText(peer.HostAndPort.Replace("/","").Replace(":","")); + fsmData.ShouldNotBeNullOrEmpty(); + var fakeCommand = JsonConvert.DeserializeObject(fsmData); + fakeCommand.Value.ShouldBe(command.Value); + passed++; + } + + return passed == 5; + } + catch(Exception e) + { + return false; + } + } + + var commandOnAllStateMachines = WaitFor(20000).Until(() => CommandCalledOnAllStateMachines()); + commandOnAllStateMachines.ShouldBeTrue(); + } + } +} diff --git a/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs b/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs index 301d21d6..51901026 100644 --- a/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs +++ b/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs @@ -95,7 +95,7 @@ namespace Ocelot.IntegrationTests { x.AddSingleton(_webHostBuilder); }) - .UseStartup(); + .UseStartup(); _builder = _webHostBuilder.Build(); diff --git a/test/Ocelot.IntegrationTests/peers.json b/test/Ocelot.IntegrationTests/peers.json new file mode 100644 index 00000000..d81d183f --- /dev/null +++ b/test/Ocelot.IntegrationTests/peers.json @@ -0,0 +1,18 @@ +{ + "Peers": [{ + "HostAndPort": "http://localhost:5000" + }, + { + "HostAndPort": "http://localhost:5002" + }, + { + "HostAndPort": "http://localhost:5003" + }, + { + "HostAndPort": "http://localhost:5004" + }, + { + "HostAndPort": "http://localhost:5001" + } + ] +} \ No newline at end of file diff --git a/test/Ocelot.ManualTest/Startup.cs b/test/Ocelot.ManualTest/ManualTestStartup.cs similarity index 89% rename from test/Ocelot.ManualTest/Startup.cs rename to test/Ocelot.ManualTest/ManualTestStartup.cs index d18b5baf..ac48f67e 100644 --- a/test/Ocelot.ManualTest/Startup.cs +++ b/test/Ocelot.ManualTest/ManualTestStartup.cs @@ -11,9 +11,9 @@ using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBui namespace Ocelot.ManualTest { - public class Startup + public class ManualTestStartup { - public Startup(IHostingEnvironment env) + public ManualTestStartup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) @@ -45,7 +45,8 @@ namespace Ocelot.ManualTest x.Audience = "test"; }); - services.AddOcelot(Configuration); + services.AddOcelot(Configuration) + .AddAdministration("/administration", "secret"); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) diff --git a/test/Ocelot.ManualTest/Program.cs b/test/Ocelot.ManualTest/Program.cs index 98b1f927..9fe2b4f0 100644 --- a/test/Ocelot.ManualTest/Program.cs +++ b/test/Ocelot.ManualTest/Program.cs @@ -15,7 +15,7 @@ namespace Ocelot.ManualTest builder.UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() - .UseStartup(); + .UseStartup(); var host = builder.Build(); host.Run(); } diff --git a/test/Ocelot.ManualTest/configuration.json b/test/Ocelot.ManualTest/configuration.json index a063fe76..0adac11d 100644 --- a/test/Ocelot.ManualTest/configuration.json +++ b/test/Ocelot.ManualTest/configuration.json @@ -300,12 +300,11 @@ "DownstreamHost": "www.bbc.co.uk", "DownstreamPort": 80, "UpstreamPathTemplate": "/bbc/", - "UpstreamHttpMethod": [ "Get" ], + "UpstreamHttpMethod": [ "Get" ] } ], "GlobalConfiguration": { - "RequestIdKey": "OcRequestId", - "AdministrationPath": "/administration" + "RequestIdKey": "OcRequestId" } } \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs index aef99941..1182a56f 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs @@ -15,6 +15,7 @@ using Xunit; namespace Ocelot.UnitTests.Configuration { + using Ocelot.DependencyInjection; using Ocelot.Errors; using Ocelot.UnitTests.TestData; @@ -36,6 +37,7 @@ namespace Ocelot.UnitTests.Configuration private Mock _rateLimitOptions; private Mock _regionCreator; private Mock _httpHandlerOptionsCreator; + private Mock _adminPath; public FileConfigurationCreatorTests() { @@ -52,13 +54,23 @@ namespace Ocelot.UnitTests.Configuration _rateLimitOptions = new Mock(); _regionCreator = new Mock(); _httpHandlerOptionsCreator = new Mock(); + _adminPath = new Mock(); _ocelotConfigurationCreator = new FileOcelotConfigurationCreator( - _fileConfig.Object, _validator.Object, _logger.Object, + _fileConfig.Object, + _validator.Object, + _logger.Object, _claimsToThingCreator.Object, - _authOptionsCreator.Object, _upstreamTemplatePatternCreator.Object, _requestIdKeyCreator.Object, - _serviceProviderConfigCreator.Object, _qosOptionsCreator.Object, _fileReRouteOptionsCreator.Object, - _rateLimitOptions.Object, _regionCreator.Object, _httpHandlerOptionsCreator.Object); + _authOptionsCreator.Object, + _upstreamTemplatePatternCreator.Object, + _requestIdKeyCreator.Object, + _serviceProviderConfigCreator.Object, + _qosOptionsCreator.Object, + _fileReRouteOptionsCreator.Object, + _rateLimitOptions.Object, + _regionCreator.Object, + _httpHandlerOptionsCreator.Object, + _adminPath.Object); } [Fact] diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationRepositoryTests.cs index 63841953..bd955879 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationRepositoryTests.cs @@ -91,7 +91,6 @@ namespace Ocelot.UnitTests.Configuration private void ThenTheConfigurationIsStoredAs(FileConfiguration expected) { - _result.GlobalConfiguration.AdministrationPath.ShouldBe(expected.GlobalConfiguration.AdministrationPath); _result.GlobalConfiguration.RequestIdKey.ShouldBe(expected.GlobalConfiguration.RequestIdKey); _result.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Host); _result.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Port); @@ -126,7 +125,6 @@ namespace Ocelot.UnitTests.Configuration private void ThenTheFollowingIsReturned(FileConfiguration expected) { - _result.GlobalConfiguration.AdministrationPath.ShouldBe(expected.GlobalConfiguration.AdministrationPath); _result.GlobalConfiguration.RequestIdKey.ShouldBe(expected.GlobalConfiguration.RequestIdKey); _result.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Host); _result.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Port); @@ -155,7 +153,6 @@ namespace Ocelot.UnitTests.Configuration var globalConfiguration = new FileGlobalConfiguration { - AdministrationPath = "asdas", ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { Port = 198, @@ -185,7 +182,6 @@ namespace Ocelot.UnitTests.Configuration var globalConfiguration = new FileGlobalConfiguration { - AdministrationPath = "testy", ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { Port = 198, diff --git a/test/Ocelot.UnitTests/Configuration/IdentityServerConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/IdentityServerConfigurationCreatorTests.cs index 8d100e10..93a86743 100644 --- a/test/Ocelot.UnitTests/Configuration/IdentityServerConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/IdentityServerConfigurationCreatorTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Configuration [Fact] public void happy_path_only_exists_for_test_coverage_even_uncle_bob_probably_wouldnt_test_this() { - var result = IdentityServerConfigurationCreator.GetIdentityServerConfiguration(); + var result = IdentityServerConfigurationCreator.GetIdentityServerConfiguration("secret"); result.ApiName.ShouldBe("admin"); } } diff --git a/test/Ocelot.UnitTests/Configuration/OcelotResourceOwnerPasswordValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/OcelotResourceOwnerPasswordValidatorTests.cs deleted file mode 100644 index a8d11713..00000000 --- a/test/Ocelot.UnitTests/Configuration/OcelotResourceOwnerPasswordValidatorTests.cs +++ /dev/null @@ -1,117 +0,0 @@ -using Ocelot.Configuration.Authentication; -using Xunit; -using Shouldly; -using TestStack.BDDfy; -using Moq; -using IdentityServer4.Validation; -using Ocelot.Configuration.Provider; -using System.Collections.Generic; - -namespace Ocelot.UnitTests.Configuration -{ - public class OcelotResourceOwnerPasswordValidatorTests - { - private OcelotResourceOwnerPasswordValidator _validator; - private Mock _matcher; - private string _userName; - private string _password; - private ResourceOwnerPasswordValidationContext _context; - private Mock _config; - private User _user; - - public OcelotResourceOwnerPasswordValidatorTests() - { - _matcher = new Mock(); - _config = new Mock(); - _validator = new OcelotResourceOwnerPasswordValidator(_matcher.Object, _config.Object); - } - - [Fact] - public void should_return_success() - { - this.Given(x => GivenTheUserName("tom")) - .And(x => GivenThePassword("password")) - .And(x => GivenTheUserIs(new User("sub", "tom", "xxx", "xxx"))) - .And(x => GivenTheMatcherReturns(true)) - .When(x => WhenIValidate()) - .Then(x => ThenTheUserIsValidated()) - .And(x => ThenTheMatcherIsCalledCorrectly()) - .BDDfy(); - } - - [Fact] - public void should_return_fail_when_no_user() - { - this.Given(x => GivenTheUserName("bob")) - .And(x => GivenTheUserIs(new User("sub", "tom", "xxx", "xxx"))) - .And(x => GivenTheMatcherReturns(true)) - .When(x => WhenIValidate()) - .Then(x => ThenTheUserIsNotValidated()) - .BDDfy(); - } - - [Fact] - public void should_return_fail_when_password_doesnt_match() - { - this.Given(x => GivenTheUserName("tom")) - .And(x => GivenThePassword("password")) - .And(x => GivenTheUserIs(new User("sub", "tom", "xxx", "xxx"))) - .And(x => GivenTheMatcherReturns(false)) - .When(x => WhenIValidate()) - .Then(x => ThenTheUserIsNotValidated()) - .And(x => ThenTheMatcherIsCalledCorrectly()) - .BDDfy(); - } - - private void ThenTheMatcherIsCalledCorrectly() - { - _matcher - .Verify(x => x.Match(_password, _user.Salt, _user.Hash), Times.Once); - } - - private void GivenThePassword(string password) - { - _password = password; - } - - private void GivenTheUserIs(User user) - { - _user = user; - _config - .Setup(x => x.Users) - .Returns(new List{_user}); - } - - private void GivenTheMatcherReturns(bool expected) - { - _matcher - .Setup(x => x.Match(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(expected); - } - - private void GivenTheUserName(string userName) - { - _userName = userName; - } - - private void WhenIValidate() - { - _context = new ResourceOwnerPasswordValidationContext - { - UserName = _userName, - Password = _password - }; - _validator.ValidateAsync(_context).Wait(); - } - - private void ThenTheUserIsValidated() - { - _context.Result.IsError.ShouldBe(false); - } - - private void ThenTheUserIsNotValidated() - { - _context.Result.IsError.ShouldBe(true); - } - } -} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs b/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs index c54c5ba2..f23a8b15 100644 --- a/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs +++ b/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs @@ -1,14 +1,20 @@ +using System; using Microsoft.AspNetCore.Mvc; using Moq; using Ocelot.Configuration.File; using Ocelot.Configuration.Setter; -using Ocelot.Controllers; using Ocelot.Errors; using Ocelot.Responses; using TestStack.BDDfy; using Xunit; using Shouldly; using Ocelot.Configuration.Provider; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Raft; +using Rafty.Concensus; +using Newtonsoft.Json; +using Rafty.FiniteStateMachine; +using Ocelot.Configuration; namespace Ocelot.UnitTests.Controllers { @@ -19,18 +25,21 @@ namespace Ocelot.UnitTests.Controllers private Mock _configSetter; private IActionResult _result; private FileConfiguration _fileConfiguration; + private Mock _provider; + private Mock _node; public FileConfigurationControllerTests() { + _provider = new Mock(); _configGetter = new Mock(); _configSetter = new Mock(); - _controller = new FileConfigurationController(_configGetter.Object, _configSetter.Object); + _controller = new FileConfigurationController(_configGetter.Object, _configSetter.Object, _provider.Object); } [Fact] public void should_get_file_configuration() { - var expected = new OkResponse(new FileConfiguration()); + var expected = new Responses.OkResponse(new FileConfiguration()); this.Given(x => x.GivenTheGetConfigurationReturns(expected)) .When(x => x.WhenIGetTheFileConfiguration()) @@ -41,7 +50,7 @@ namespace Ocelot.UnitTests.Controllers [Fact] public void should_return_error_when_cannot_get_config() { - var expected = new ErrorResponse(It.IsAny()); + var expected = new Responses.ErrorResponse(It.IsAny()); this.Given(x => x.GivenTheGetConfigurationReturns(expected)) .When(x => x.WhenIGetTheFileConfiguration()) @@ -56,26 +65,81 @@ namespace Ocelot.UnitTests.Controllers var expected = new FileConfiguration(); this.Given(x => GivenTheFileConfiguration(expected)) - .And(x => GivenTheConfigSetterReturnsAnError(new OkResponse())) + .And(x => GivenTheConfigSetterReturns(new OkResponse())) .When(x => WhenIPostTheFileConfiguration()) .Then(x => x.ThenTheConfigrationSetterIsCalledCorrectly()) .BDDfy(); } + [Fact] + public void should_post_file_configuration_using_raft_node() + { + var expected = new FileConfiguration(); + + this.Given(x => GivenTheFileConfiguration(expected)) + .And(x => GivenARaftNodeIsRegistered()) + .And(x => GivenTheNodeReturnsOK()) + .And(x => GivenTheConfigSetterReturns(new OkResponse())) + .When(x => WhenIPostTheFileConfiguration()) + .Then(x => x.ThenTheNodeIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_return_error_when_cannot_set_config_using_raft_node() + { + var expected = new FileConfiguration(); + + this.Given(x => GivenTheFileConfiguration(expected)) + .And(x => GivenARaftNodeIsRegistered()) + .And(x => GivenTheNodeReturnsError()) + .When(x => WhenIPostTheFileConfiguration()) + .Then(x => ThenTheResponseIs()) + .BDDfy(); + } + [Fact] public void should_return_error_when_cannot_set_config() { var expected = new FileConfiguration(); this.Given(x => GivenTheFileConfiguration(expected)) - .And(x => GivenTheConfigSetterReturnsAnError(new ErrorResponse(new FakeError()))) + .And(x => GivenTheConfigSetterReturns(new ErrorResponse(new FakeError()))) .When(x => WhenIPostTheFileConfiguration()) .Then(x => x.ThenTheConfigrationSetterIsCalledCorrectly()) .And(x => ThenTheResponseIs()) .BDDfy(); } - private void GivenTheConfigSetterReturnsAnError(Response response) + + private void ThenTheNodeIsCalledCorrectly() + { + _node.Verify(x => x.Accept(It.IsAny()), Times.Once); + } + + private void GivenARaftNodeIsRegistered() + { + _node = new Mock(); + _provider + .Setup(x => x.GetService(typeof(INode))) + .Returns(_node.Object); + } + + private void GivenTheNodeReturnsOK() + { + _node + .Setup(x => x.Accept(It.IsAny())) + .Returns(new Rafty.Concensus.OkResponse(new UpdateFileConfiguration(new FileConfiguration()))); + } + + private void GivenTheNodeReturnsError() + { + _node + .Setup(x => x.Accept(It.IsAny())) + .Returns(new Rafty.Concensus.ErrorResponse("error", new UpdateFileConfiguration(new FileConfiguration()))); + } + + private void GivenTheConfigSetterReturns(Response response) { _configSetter .Setup(x => x.Set(It.IsAny())) @@ -103,7 +167,7 @@ namespace Ocelot.UnitTests.Controllers _result.ShouldBeOfType(); } - private void GivenTheGetConfigurationReturns(Response fileConfiguration) + private void GivenTheGetConfigurationReturns(Ocelot.Responses.Response fileConfiguration) { _configGetter .Setup(x => x.Get()) @@ -128,4 +192,4 @@ namespace Ocelot.UnitTests.Controllers } } } -} \ No newline at end of file +} diff --git a/test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs b/test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs index d449d280..28818e71 100644 --- a/test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs +++ b/test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs @@ -1,10 +1,9 @@ using Xunit; using Shouldly; using TestStack.BDDfy; -using Ocelot.Controllers; +using Ocelot.Cache; using System; using Moq; -using Ocelot.Cache; using System.Net.Http; using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; diff --git a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs index 834879c5..818a9e1f 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs @@ -75,6 +75,16 @@ namespace Ocelot.UnitTests.DependencyInjection .BDDfy(); } + [Fact] + public void should_set_up_rafty() + { + this.Given(x => WhenISetUpOcelotServices()) + .When(x => WhenISetUpRafty()) + .Then(x => ThenAnExceptionIsntThrown()) + .Then(x => ThenTheCorrectAdminPathIsRegitered()) + .BDDfy(); + } + [Fact] public void should_use_logger_factory() { @@ -85,6 +95,13 @@ namespace Ocelot.UnitTests.DependencyInjection .BDDfy(); } + private void ThenTheCorrectAdminPathIsRegitered() + { + _serviceProvider = _services.BuildServiceProvider(); + var path = _serviceProvider.GetService(); + path.Path.ShouldBe("/administration"); + } + private void OnlyOneVersionOfEachCacheIsRegistered() { var outputCache = _services.Single(x => x.ServiceType == typeof(IOcelotCache)); @@ -111,6 +128,18 @@ namespace Ocelot.UnitTests.DependencyInjection } } + private void WhenISetUpRafty() + { + try + { + _ocelotBuilder.AddAdministration("/administration", "secret").AddRafty(); + } + catch (Exception e) + { + _ex = e; + } + } + private void ThenAnOcelotBuilderIsReturned() { _ocelotBuilder.ShouldBeOfType(); diff --git a/test/Ocelot.UnitTests/Raft/OcelotFiniteStateMachineTests.cs b/test/Ocelot.UnitTests/Raft/OcelotFiniteStateMachineTests.cs new file mode 100644 index 00000000..1451e839 --- /dev/null +++ b/test/Ocelot.UnitTests/Raft/OcelotFiniteStateMachineTests.cs @@ -0,0 +1,45 @@ +using Moq; +using Ocelot.Configuration.Setter; +using Ocelot.Raft; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Raft +{ + public class OcelotFiniteStateMachineTests + { + private UpdateFileConfiguration _command; + private OcelotFiniteStateMachine _fsm; + private Mock _setter; + + public OcelotFiniteStateMachineTests() + { + _setter = new Mock(); + _fsm = new OcelotFiniteStateMachine(_setter.Object); + } + + [Fact] + public void should_handle_update_file_configuration_command() + { + this.Given(x => GivenACommand(new UpdateFileConfiguration(new Ocelot.Configuration.File.FileConfiguration()))) + .When(x => WhenTheCommandIsHandled()) + .Then(x => ThenTheStateIsUpdated()) + .BDDfy(); + } + + private void GivenACommand(UpdateFileConfiguration command) + { + _command = command; + } + + private void WhenTheCommandIsHandled() + { + _fsm.Handle(new Rafty.Log.LogEntry(_command, _command.GetType(), 0)); + } + + private void ThenTheStateIsUpdated() + { + _setter.Verify(x => x.Set(_command.Configuration), Times.Once); + } + } +} \ No newline at end of file From 931a115ffae7141b4ccc183d4d253b6ef79ddd2e Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Tue, 2 Jan 2018 18:49:22 +0000 Subject: [PATCH 10/13] changes to add new feature to url routing (#186) --- .../Creator/UpstreamTemplatePatternCreator.cs | 20 ++++++- .../DownstreamRouteFinderMiddleware.cs | 1 + test/Ocelot.AcceptanceTests/RoutingTests.cs | 59 ++++++++++++++++++- .../UpstreamTemplatePatternCreatorTests.cs | 29 +++++++++ 4 files changed, 106 insertions(+), 3 deletions(-) diff --git a/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs b/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs index 92559bfb..a45d2864 100644 --- a/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs +++ b/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs @@ -9,6 +9,7 @@ namespace Ocelot.Configuration.Creator private const string RegExMatchEndString = "$"; private const string RegExIgnoreCase = "(?i)"; private const string RegExForwardSlashOnly = "^/$"; + private const string RegExForwardSlashAndOnePlaceHolder = "^/.*"; public string Create(FileReRoute reRoute) { @@ -22,8 +23,13 @@ namespace Ocelot.Configuration.Creator { var postitionOfPlaceHolderClosingBracket = upstreamTemplate.IndexOf('}', i); var difference = postitionOfPlaceHolderClosingBracket - i + 1; - var variableName = upstreamTemplate.Substring(i, difference); - placeholders.Add(variableName); + var placeHolderName = upstreamTemplate.Substring(i, difference); + placeholders.Add(placeHolderName); + + if(ForwardSlashAndOnePlaceHolder(upstreamTemplate, placeholders, postitionOfPlaceHolderClosingBracket)) + { + return RegExForwardSlashAndOnePlaceHolder; + } } } @@ -49,6 +55,16 @@ namespace Ocelot.Configuration.Creator return route; } + private bool ForwardSlashAndOnePlaceHolder(string upstreamTemplate, List placeholders, int postitionOfPlaceHolderClosingBracket) + { + if(upstreamTemplate.Substring(0, 2) == "/{" && placeholders.Count == 1 && upstreamTemplate.Length == postitionOfPlaceHolderClosingBracket + 1) + { + return true; + } + + return false; + } + private bool IsPlaceHolder(string upstreamTemplate, int i) { diff --git a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs index 36f01569..15aa79cf 100644 --- a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs +++ b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs @@ -38,6 +38,7 @@ namespace Ocelot.DownstreamRouteFinder.Middleware //todo make this getting config its own middleware one day? var configuration = await _configProvider.Get(); + if(configuration.IsError) { _logger.LogError($"{MiddlewareName} setting pipeline errors. IOcelotConfigurationProvider returned {configuration.Errors.ToErrorString()}"); diff --git a/test/Ocelot.AcceptanceTests/RoutingTests.cs b/test/Ocelot.AcceptanceTests/RoutingTests.cs index 926cb849..587327d1 100644 --- a/test/Ocelot.AcceptanceTests/RoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/RoutingTests.cs @@ -32,7 +32,35 @@ namespace Ocelot.AcceptanceTests .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) .BDDfy(); } - + + [Fact] + public void should_return_response_200_with_forward_slash_and_placeholder_only() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + [Fact] public void should_return_response_200_with_simple_url() { @@ -275,6 +303,35 @@ namespace Ocelot.AcceptanceTests } + [Fact] + public void should_return_response_200_with_complex_url_that_starts_with_placeholder() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/{variantId}/products/{productId}", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamPathTemplate = "/{variantId}/products/{productId}", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/api/23/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("23/products/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Some Product")) + .BDDfy(); + } + + [Fact] public void should_not_add_trailing_slash_to_downstream_url() { diff --git a/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs index fba285e2..d5c9774b 100644 --- a/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs @@ -121,6 +121,35 @@ namespace Ocelot.UnitTests.Configuration .BDDfy(); } + [Fact] + public void should_create_template_pattern_that_matches_to_end_of_string_when_slash_and_placeholder() + { + var fileReRoute = new FileReRoute + { + UpstreamPathTemplate = "/{url}" + }; + + this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^/.*")) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_starts_with_placeholder_then_has_another_later() + { + var fileReRoute = new FileReRoute + { + UpstreamPathTemplate = "/{productId}/products/variants/{variantId}/", + ReRouteIsCaseSensitive = true + }; + + this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^/[0-9a-zA-Z].*/products/variants/[0-9a-zA-Z].*(/|)$")) + .BDDfy(); + } + private void GivenTheFollowingFileReRoute(FileReRoute fileReRoute) { _fileReRoute = fileReRoute; From fef19ddf9875af756ad151d1f2ab8411fa095602 Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Fri, 5 Jan 2018 21:26:15 +0000 Subject: [PATCH 11/13] Changed routing to support a catch all style (#187) * Changed routing to support a catch all style * refactoring placeholder tuff * implemented simple priority in the routing --- docs/features/routing.rst | 31 +++- .../Configuration/Builder/ReRouteBuilder.cs | 5 +- .../IUpstreamTemplatePatternCreator.cs | 3 +- .../Creator/UpstreamTemplatePatternCreator.cs | 10 +- src/Ocelot/Configuration/ReRoute.cs | 5 +- .../DependencyInjection/OcelotBuilder.cs | 2 +- .../DownstreamRouteFinder/DownstreamRoute.cs | 4 +- .../Finder/DownstreamRouteFinder.cs | 29 +-- .../IUrlPathPlaceholderNameAndValueFinder.cs | 4 +- .../UrlPathPlaceholderNameAndValue.cs | 12 +- .../UrlPathPlaceholderNameAndValueFinder.cs | 66 +++++-- .../DownstreamUrlTemplateVariableReplacer.cs | 4 +- ...wnstreamUrlPathTemplateVariableReplacer.cs | 2 +- ...nstreamPathTemplate.cs => PathTemplate.cs} | 0 src/Ocelot/Values/UpstreamPathTemplate.cs | 14 ++ test/Ocelot.AcceptanceTests/RoutingTests.cs | 166 +++++++++++++++++- .../AuthenticationMiddlewareTests.cs | 2 +- .../AuthorisationMiddlewareTests.cs | 2 +- .../Cache/OutputCacheMiddlewareTests.cs | 2 +- .../Claims/ClaimsBuilderMiddlewareTests.cs | 2 +- .../FileConfigurationCreatorTests.cs | 7 +- .../UpstreamTemplatePatternCreatorTests.cs | 20 ++- .../DownstreamRouteFinderMiddlewareTests.cs | 2 +- .../DownstreamRouteFinderTests.cs | 144 +++++++++++---- .../UrlMatcher/RegExUrlMatcherTests.cs | 12 ++ ...PathPlaceholderNameAndValueFinderTests.cs} | 134 ++++++++++---- .../DownstreamUrlCreatorMiddlewareTests.cs | 4 +- ...eamUrlPathTemplateVariableReplacerTests.cs | 32 ++-- ...ttpRequestHeadersBuilderMiddlewareTests.cs | 2 +- .../LoadBalancerMiddlewareTests.cs | 6 +- .../QueryStringBuilderMiddlewareTests.cs | 2 +- .../ClientRateLimitMiddlewareTests.cs | 4 +- .../HttpRequestBuilderMiddlewareTests.cs | 2 +- .../RequestId/RequestIdMiddlewareTests.cs | 4 +- 34 files changed, 575 insertions(+), 165 deletions(-) rename src/Ocelot/Values/{DownstreamPathTemplate.cs => PathTemplate.cs} (100%) create mode 100644 src/Ocelot/Values/UpstreamPathTemplate.cs rename test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/{TemplateVariableNameAndValueFinderTests.cs => UrlPathPlaceholderNameAndValueFinderTests.cs} (57%) diff --git a/docs/features/routing.rst b/docs/features/routing.rst index ed0d967e..0359d917 100644 --- a/docs/features/routing.rst +++ b/docs/features/routing.rst @@ -62,4 +62,33 @@ In order to change this you can specify on a per ReRoute basis the following set This means that when Ocelot tries to match the incoming upstream url with an upstream template the evaluation will be case sensitive. This setting defaults to false so only set it if you want -the ReRoute to be case sensitive is my advice! \ No newline at end of file +the ReRoute to be case sensitive is my advice! + +Catch All +^^^^^^^^^ + +Ocelot's routing also supports a catch all style routing where the user can specify that they want to match all traffic if you set up your config like below the request will be proxied straight through (it doesnt have to be url any placeholder name will work). + +.. code-block:: json + + { + "DownstreamPathTemplate": "/{url}", + "DownstreamScheme": "https", + "DownstreamPort": 80, + "DownstreamHost" "localhost", + "UpstreamPathTemplate": "/{url}", + "UpstreamHttpMethod": [ "Get" ] + } + +The catch all has a lower priority than any other ReRoute. If you also have the ReRoute below in your config then Ocelot would match it before the catch all. + +.. code-block:: json + + { + "DownstreamPathTemplate": "/", + "DownstreamScheme": "https", + "DownstreamPort": 80, + "DownstreamHost" "10.0.10.1", + "UpstreamPathTemplate": "/", + "UpstreamHttpMethod": [ "Get" ] + } \ No newline at end of file diff --git a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs index 1a8877e7..cc5a61aa 100644 --- a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs @@ -2,6 +2,7 @@ using System.Net.Http; using Ocelot.Values; using System.Linq; +using Ocelot.Configuration.Creator; namespace Ocelot.Configuration.Builder { @@ -11,7 +12,7 @@ namespace Ocelot.Configuration.Builder private string _loadBalancerKey; private string _downstreamPathTemplate; private string _upstreamTemplate; - private string _upstreamTemplatePattern; + private UpstreamPathTemplate _upstreamTemplatePattern; private List _upstreamHttpMethod; private bool _isAuthenticated; private List _configHeaderExtractorProperties; @@ -65,7 +66,7 @@ namespace Ocelot.Configuration.Builder return this; } - public ReRouteBuilder WithUpstreamTemplatePattern(string input) + public ReRouteBuilder WithUpstreamTemplatePattern(UpstreamPathTemplate input) { _upstreamTemplatePattern = input; return this; diff --git a/src/Ocelot/Configuration/Creator/IUpstreamTemplatePatternCreator.cs b/src/Ocelot/Configuration/Creator/IUpstreamTemplatePatternCreator.cs index ae62c47a..14de619d 100644 --- a/src/Ocelot/Configuration/Creator/IUpstreamTemplatePatternCreator.cs +++ b/src/Ocelot/Configuration/Creator/IUpstreamTemplatePatternCreator.cs @@ -1,9 +1,10 @@ using Ocelot.Configuration.File; +using Ocelot.Values; namespace Ocelot.Configuration.Creator { public interface IUpstreamTemplatePatternCreator { - string Create(FileReRoute reRoute); + UpstreamPathTemplate Create(FileReRoute reRoute); } } \ No newline at end of file diff --git a/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs b/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs index a45d2864..7816d118 100644 --- a/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs +++ b/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Ocelot.Configuration.File; +using Ocelot.Values; namespace Ocelot.Configuration.Creator { @@ -11,7 +12,7 @@ namespace Ocelot.Configuration.Creator private const string RegExForwardSlashOnly = "^/$"; private const string RegExForwardSlashAndOnePlaceHolder = "^/.*"; - public string Create(FileReRoute reRoute) + public UpstreamPathTemplate Create(FileReRoute reRoute) { var upstreamTemplate = reRoute.UpstreamPathTemplate; @@ -26,9 +27,10 @@ namespace Ocelot.Configuration.Creator var placeHolderName = upstreamTemplate.Substring(i, difference); placeholders.Add(placeHolderName); + //hack to handle /{url} case if(ForwardSlashAndOnePlaceHolder(upstreamTemplate, placeholders, postitionOfPlaceHolderClosingBracket)) { - return RegExForwardSlashAndOnePlaceHolder; + return new UpstreamPathTemplate(RegExForwardSlashAndOnePlaceHolder, 0); } } } @@ -40,7 +42,7 @@ namespace Ocelot.Configuration.Creator if (upstreamTemplate == "/") { - return RegExForwardSlashOnly; + return new UpstreamPathTemplate(RegExForwardSlashOnly, 1); } if(upstreamTemplate.EndsWith("/")) @@ -52,7 +54,7 @@ namespace Ocelot.Configuration.Creator ? $"^{upstreamTemplate}{RegExMatchEndString}" : $"^{RegExIgnoreCase}{upstreamTemplate}{RegExMatchEndString}"; - return route; + return new UpstreamPathTemplate(route, 1); } private bool ForwardSlashAndOnePlaceHolder(string upstreamTemplate, List placeholders, int postitionOfPlaceHolderClosingBracket) diff --git a/src/Ocelot/Configuration/ReRoute.cs b/src/Ocelot/Configuration/ReRoute.cs index 0d373425..18e068aa 100644 --- a/src/Ocelot/Configuration/ReRoute.cs +++ b/src/Ocelot/Configuration/ReRoute.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Net.Http; +using Ocelot.Configuration.Creator; using Ocelot.Values; namespace Ocelot.Configuration @@ -9,7 +10,7 @@ namespace Ocelot.Configuration public ReRoute(PathTemplate downstreamPathTemplate, PathTemplate upstreamPathTemplate, List upstreamHttpMethod, - string upstreamTemplatePattern, + UpstreamPathTemplate upstreamTemplatePattern, bool isAuthenticated, AuthenticationOptions authenticationOptions, List claimsToHeaders, @@ -67,7 +68,7 @@ namespace Ocelot.Configuration public string ReRouteKey {get;private set;} public PathTemplate DownstreamPathTemplate { get; private set; } public PathTemplate UpstreamPathTemplate { get; private set; } - public string UpstreamTemplatePattern { get; private set; } + public UpstreamPathTemplate UpstreamTemplatePattern { get; private set; } public List UpstreamHttpMethod { get; private set; } public bool IsAuthenticated { get; private set; } public bool IsAuthorised { get; private set; } diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index b8ebc8a2..c5317a51 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -108,7 +108,7 @@ namespace Ocelot.DependencyInjection _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); - _services.TryAddSingleton(); + _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); diff --git a/src/Ocelot/DownstreamRouteFinder/DownstreamRoute.cs b/src/Ocelot/DownstreamRouteFinder/DownstreamRoute.cs index d4a117c0..7a4a66ea 100644 --- a/src/Ocelot/DownstreamRouteFinder/DownstreamRoute.cs +++ b/src/Ocelot/DownstreamRouteFinder/DownstreamRoute.cs @@ -6,12 +6,12 @@ namespace Ocelot.DownstreamRouteFinder { public class DownstreamRoute { - public DownstreamRoute(List templatePlaceholderNameAndValues, ReRoute reRoute) + public DownstreamRoute(List templatePlaceholderNameAndValues, ReRoute reRoute) { TemplatePlaceholderNameAndValues = templatePlaceholderNameAndValues; ReRoute = reRoute; } - public List TemplatePlaceholderNameAndValues { get; private set; } + public List TemplatePlaceholderNameAndValues { get; private set; } public ReRoute ReRoute { get; private set; } } } \ No newline at end of file diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs index 2c3077cc..643a4464 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs @@ -13,34 +13,30 @@ namespace Ocelot.DownstreamRouteFinder.Finder public class DownstreamRouteFinder : IDownstreamRouteFinder { private readonly IUrlPathToUrlTemplateMatcher _urlMatcher; - private readonly IUrlPathPlaceholderNameAndValueFinder _urlPathPlaceholderNameAndValueFinder; + private readonly IPlaceholderNameAndValueFinder __placeholderNameAndValueFinder; - public DownstreamRouteFinder(IUrlPathToUrlTemplateMatcher urlMatcher, IUrlPathPlaceholderNameAndValueFinder urlPathPlaceholderNameAndValueFinder) + public DownstreamRouteFinder(IUrlPathToUrlTemplateMatcher urlMatcher, IPlaceholderNameAndValueFinder urlPathPlaceholderNameAndValueFinder) { _urlMatcher = urlMatcher; - _urlPathPlaceholderNameAndValueFinder = urlPathPlaceholderNameAndValueFinder; + __placeholderNameAndValueFinder = urlPathPlaceholderNameAndValueFinder; } - public Response FindDownstreamRoute(string upstreamUrlPath, string upstreamHttpMethod, IOcelotConfiguration configuration) + public Response FindDownstreamRoute(string path, string httpMethod, IOcelotConfiguration configuration) { - var applicableReRoutes = configuration.ReRoutes.Where(r => r.UpstreamHttpMethod.Count == 0 || r.UpstreamHttpMethod.Select(x => x.Method.ToLower()).Contains(upstreamHttpMethod.ToLower())); + var applicableReRoutes = configuration.ReRoutes.Where(r => r.UpstreamHttpMethod.Count == 0 || r.UpstreamHttpMethod.Select(x => x.Method.ToLower()).Contains(httpMethod.ToLower())).OrderByDescending(x => x.UpstreamTemplatePattern.Priority); foreach (var reRoute in applicableReRoutes) { - if (upstreamUrlPath == reRoute.UpstreamTemplatePattern) + if (path == reRoute.UpstreamTemplatePattern.Template) { - var templateVariableNameAndValues = _urlPathPlaceholderNameAndValueFinder.Find(upstreamUrlPath, reRoute.UpstreamPathTemplate.Value); - - return new OkResponse(new DownstreamRoute(templateVariableNameAndValues.Data, reRoute)); + return GetPlaceholderNamesAndValues(path, reRoute); } - var urlMatch = _urlMatcher.Match(upstreamUrlPath, reRoute.UpstreamTemplatePattern); + var urlMatch = _urlMatcher.Match(path, reRoute.UpstreamTemplatePattern.Template); if (urlMatch.Data.Match) { - var templateVariableNameAndValues = _urlPathPlaceholderNameAndValueFinder.Find(upstreamUrlPath, reRoute.UpstreamPathTemplate.Value); - - return new OkResponse(new DownstreamRoute(templateVariableNameAndValues.Data, reRoute)); + return GetPlaceholderNamesAndValues(path, reRoute); } } @@ -49,5 +45,12 @@ namespace Ocelot.DownstreamRouteFinder.Finder new UnableToFindDownstreamRouteError() }); } + + private OkResponse GetPlaceholderNamesAndValues(string path, ReRoute reRoute) + { + var templatePlaceholderNameAndValues = __placeholderNameAndValueFinder.Find(path, reRoute.UpstreamPathTemplate.Value); + + return new OkResponse(new DownstreamRoute(templatePlaceholderNameAndValues.Data, reRoute)); + } } } \ No newline at end of file diff --git a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/IUrlPathPlaceholderNameAndValueFinder.cs b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/IUrlPathPlaceholderNameAndValueFinder.cs index 788299cb..678b1081 100644 --- a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/IUrlPathPlaceholderNameAndValueFinder.cs +++ b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/IUrlPathPlaceholderNameAndValueFinder.cs @@ -3,8 +3,8 @@ using Ocelot.Responses; namespace Ocelot.DownstreamRouteFinder.UrlMatcher { - public interface IUrlPathPlaceholderNameAndValueFinder + public interface IPlaceholderNameAndValueFinder { - Response> Find(string upstreamUrlPath, string upstreamUrlPathTemplate); + Response> Find(string path, string pathTemplate); } } diff --git a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValue.cs b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValue.cs index cb690666..825f1bab 100644 --- a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValue.cs +++ b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValue.cs @@ -1,13 +1,13 @@ namespace Ocelot.DownstreamRouteFinder.UrlMatcher { - public class UrlPathPlaceholderNameAndValue + public class PlaceholderNameAndValue { - public UrlPathPlaceholderNameAndValue(string templateVariableName, string templateVariableValue) + public PlaceholderNameAndValue(string name, string value) { - TemplateVariableName = templateVariableName; - TemplateVariableValue = templateVariableValue; + Name = name; + Value = value; } - public string TemplateVariableName {get;private set;} - public string TemplateVariableValue {get;private set;} + public string Name {get;private set;} + public string Value {get;private set;} } } \ No newline at end of file diff --git a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinder.cs b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinder.cs index 946a365c..8b1c6acf 100644 --- a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinder.cs +++ b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinder.cs @@ -3,44 +3,72 @@ using Ocelot.Responses; namespace Ocelot.DownstreamRouteFinder.UrlMatcher { - public class UrlPathPlaceholderNameAndValueFinder : IUrlPathPlaceholderNameAndValueFinder + public class UrlPathPlaceholderNameAndValueFinder : IPlaceholderNameAndValueFinder { - public Response> Find(string upstreamUrlPath, string upstreamUrlPathTemplate) + public Response> Find(string path, string pathTemplate) { - var templateKeysAndValues = new List(); + var placeHolderNameAndValues = new List(); - int counterForUrl = 0; + int counterForPath = 0; - for (int counterForTemplate = 0; counterForTemplate < upstreamUrlPathTemplate.Length; counterForTemplate++) + for (int counterForTemplate = 0; counterForTemplate < pathTemplate.Length; counterForTemplate++) { - if ((upstreamUrlPath.Length > counterForUrl) && CharactersDontMatch(upstreamUrlPathTemplate[counterForTemplate], upstreamUrlPath[counterForUrl]) && ContinueScanningUrl(counterForUrl,upstreamUrlPath.Length)) + if ((path.Length > counterForPath) && CharactersDontMatch(pathTemplate[counterForTemplate], path[counterForPath]) && ContinueScanningUrl(counterForPath,path.Length)) { - if (IsPlaceholder(upstreamUrlPathTemplate[counterForTemplate])) + if (IsPlaceholder(pathTemplate[counterForTemplate])) { - var variableName = GetPlaceholderVariableName(upstreamUrlPathTemplate, counterForTemplate); + var placeholderName = GetPlaceholderName(pathTemplate, counterForTemplate); - var variableValue = GetPlaceholderVariableValue(upstreamUrlPathTemplate, variableName, upstreamUrlPath, counterForUrl); + var placeholderValue = GetPlaceholderValue(pathTemplate, placeholderName, path, counterForPath); - var templateVariableNameAndValue = new UrlPathPlaceholderNameAndValue(variableName, variableValue); + placeHolderNameAndValues.Add(new PlaceholderNameAndValue(placeholderName, placeholderValue)); - templateKeysAndValues.Add(templateVariableNameAndValue); + counterForTemplate = GetNextCounterPosition(pathTemplate, counterForTemplate, '}'); - counterForTemplate = GetNextCounterPosition(upstreamUrlPathTemplate, counterForTemplate, '}'); - - counterForUrl = GetNextCounterPosition(upstreamUrlPath, counterForUrl, '/'); + counterForPath = GetNextCounterPosition(path, counterForPath, '/'); continue; } - return new OkResponse>(templateKeysAndValues); + return new OkResponse>(placeHolderNameAndValues); } - counterForUrl++; + else if(IsCatchAll(path, counterForPath, pathTemplate)) + { + var endOfPlaceholder = GetNextCounterPosition(pathTemplate, counterForTemplate, '}'); + + var placeholderName = GetPlaceholderName(pathTemplate, 1); + + if(NothingAfterFirstForwardSlash(path)) + { + placeHolderNameAndValues.Add(new PlaceholderNameAndValue(placeholderName, "")); + } + else + { + var placeholderValue = GetPlaceholderValue(pathTemplate, placeholderName, path, counterForPath + 1); + placeHolderNameAndValues.Add(new PlaceholderNameAndValue(placeholderName, placeholderValue)); + } + + counterForTemplate = endOfPlaceholder; + } + counterForPath++; } - return new OkResponse>(templateKeysAndValues); + return new OkResponse>(placeHolderNameAndValues); } - private string GetPlaceholderVariableValue(string urlPathTemplate, string variableName, string urlPath, int counterForUrl) + private bool IsCatchAll(string path, int counterForPath, string pathTemplate) + { + return string.IsNullOrEmpty(path) || (path.Length > counterForPath && path[counterForPath] == '/') && pathTemplate.Length > 1 + && pathTemplate.Substring(0, 2) == "/{" + && pathTemplate.IndexOf('}') == pathTemplate.Length - 1; + } + + private bool NothingAfterFirstForwardSlash(string path) + { + return path.Length == 1 || path.Length == 0; + } + + private string GetPlaceholderValue(string urlPathTemplate, string variableName, string urlPath, int counterForUrl) { var positionOfNextSlash = urlPath.IndexOf('/', counterForUrl); @@ -54,7 +82,7 @@ namespace Ocelot.DownstreamRouteFinder.UrlMatcher return variableValue; } - private string GetPlaceholderVariableName(string urlPathTemplate, int counterForTemplate) + private string GetPlaceholderName(string urlPathTemplate, int counterForTemplate) { var postitionOfPlaceHolderClosingBracket = urlPathTemplate.IndexOf('}', counterForTemplate) + 1; diff --git a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamUrlTemplateVariableReplacer.cs b/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamUrlTemplateVariableReplacer.cs index 3c42b4f4..1b744819 100644 --- a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamUrlTemplateVariableReplacer.cs +++ b/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamUrlTemplateVariableReplacer.cs @@ -8,7 +8,7 @@ namespace Ocelot.DownstreamUrlCreator.UrlTemplateReplacer { public class DownstreamTemplatePathPlaceholderReplacer : IDownstreamPathPlaceholderReplacer { - public Response Replace(PathTemplate downstreamPathTemplate, List urlPathPlaceholderNameAndValues) + public Response Replace(PathTemplate downstreamPathTemplate, List urlPathPlaceholderNameAndValues) { var downstreamPath = new StringBuilder(); @@ -16,7 +16,7 @@ namespace Ocelot.DownstreamUrlCreator.UrlTemplateReplacer foreach (var placeholderVariableAndValue in urlPathPlaceholderNameAndValues) { - downstreamPath.Replace(placeholderVariableAndValue.TemplateVariableName, placeholderVariableAndValue.TemplateVariableValue); + downstreamPath.Replace(placeholderVariableAndValue.Name, placeholderVariableAndValue.Value); } return new OkResponse(new DownstreamPath(downstreamPath.ToString())); diff --git a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/IDownstreamUrlPathTemplateVariableReplacer.cs b/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/IDownstreamUrlPathTemplateVariableReplacer.cs index 647af63a..46e998d4 100644 --- a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/IDownstreamUrlPathTemplateVariableReplacer.cs +++ b/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/IDownstreamUrlPathTemplateVariableReplacer.cs @@ -7,6 +7,6 @@ namespace Ocelot.DownstreamUrlCreator.UrlTemplateReplacer { public interface IDownstreamPathPlaceholderReplacer { - Response Replace(PathTemplate downstreamPathTemplate, List urlPathPlaceholderNameAndValues); + Response Replace(PathTemplate downstreamPathTemplate, List urlPathPlaceholderNameAndValues); } } \ No newline at end of file diff --git a/src/Ocelot/Values/DownstreamPathTemplate.cs b/src/Ocelot/Values/PathTemplate.cs similarity index 100% rename from src/Ocelot/Values/DownstreamPathTemplate.cs rename to src/Ocelot/Values/PathTemplate.cs diff --git a/src/Ocelot/Values/UpstreamPathTemplate.cs b/src/Ocelot/Values/UpstreamPathTemplate.cs new file mode 100644 index 00000000..00b70b2f --- /dev/null +++ b/src/Ocelot/Values/UpstreamPathTemplate.cs @@ -0,0 +1,14 @@ +namespace Ocelot.Values +{ + public class UpstreamPathTemplate + { + public UpstreamPathTemplate(string template, int priority) + { + Template = template; + Priority = priority; + } + + public string Template {get;} + public int Priority {get;} + } +} \ No newline at end of file diff --git a/test/Ocelot.AcceptanceTests/RoutingTests.cs b/test/Ocelot.AcceptanceTests/RoutingTests.cs index 587327d1..21c21c06 100644 --- a/test/Ocelot.AcceptanceTests/RoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/RoutingTests.cs @@ -52,7 +52,7 @@ namespace Ocelot.AcceptanceTests } }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "", 200, "Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/", "/", 200, "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) @@ -61,6 +61,145 @@ namespace Ocelot.AcceptanceTests .BDDfy(); } + [Fact] + public void should_return_response_200_favouring_forward_slash_with_path_route() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51880, + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + }, + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51880/", "/test", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/test")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_favouring_forward_slash() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51880, + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + }, + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_favouring_forward_slash_route_because_it_is_first() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51880, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + }, + new FileReRoute + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51880/", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_nothing_and_placeholder_only() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + [Fact] public void should_return_response_200_with_simple_url() { @@ -80,7 +219,7 @@ namespace Ocelot.AcceptanceTests } }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "", 200, "Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 200, "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) @@ -378,7 +517,7 @@ namespace Ocelot.AcceptanceTests } }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "", 201, string.Empty)) + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 201, string.Empty)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .And(x => _steps.GivenThePostHasContent("postContent")) @@ -406,7 +545,7 @@ namespace Ocelot.AcceptanceTests } }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "", 200, "Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/newThing", 200, "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/newThing?DeviceType=IphoneApp&Browser=moonpigIphone&BrowserString=-&CountryCode=123&DeviceName=iPhone 5 (GSM+CDMA)&OperatingSystem=iPhone OS 7.1.2&BrowserVersion=3708AdHoc&ipAddress=-")) @@ -434,7 +573,7 @@ namespace Ocelot.AcceptanceTests } }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/myApp1Name/api/products/1", 200, "Some Product")) + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/api/products/1", 200, "Some Product")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/myApp1Name/api/products/1")) @@ -490,7 +629,7 @@ namespace Ocelot.AcceptanceTests } }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "", 200, "Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 200, "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) @@ -564,7 +703,7 @@ namespace Ocelot.AcceptanceTests } }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", "", 200, "Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", "/api/swagger/lib/backbone-min.js", 200, "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/platform/swagger/lib/backbone-min.js")) @@ -587,8 +726,17 @@ namespace Ocelot.AcceptanceTests app.Run(async context => { _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); + + if(_downstreamPath != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } }); }) .Build(); diff --git a/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs b/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs index e6bcc500..dcb54388 100644 --- a/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs @@ -29,7 +29,7 @@ { this.Given(x => x.GivenTheDownStreamRouteIs( new DownstreamRoute( - new List(), + new List(), new ReRouteBuilder().WithUpstreamHttpMethod(new List { "Get" }).Build()))) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenTheUserIsAuthenticated()) diff --git a/test/Ocelot.UnitTests/Authorization/AuthorisationMiddlewareTests.cs b/test/Ocelot.UnitTests/Authorization/AuthorisationMiddlewareTests.cs index ca346974..9ab81f94 100644 --- a/test/Ocelot.UnitTests/Authorization/AuthorisationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Authorization/AuthorisationMiddlewareTests.cs @@ -32,7 +32,7 @@ [Fact] public void should_call_authorisation_service() { - this.Given(x => x.GivenTheDownStreamRouteIs(new DownstreamRoute(new List(), + this.Given(x => x.GivenTheDownStreamRouteIs(new DownstreamRoute(new List(), new ReRouteBuilder() .WithIsAuthorised(true) .WithUpstreamHttpMethod(new List { "Get" }) diff --git a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs index 9b78b3cc..1499e19d 100644 --- a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs @@ -94,7 +94,7 @@ .WithUpstreamHttpMethod(new List { "Get" }) .Build(); - var downstreamRoute = new DownstreamRoute(new List(), reRoute); + var downstreamRoute = new DownstreamRoute(new List(), reRoute); ScopedRepository .Setup(x => x.Get(It.IsAny())) diff --git a/test/Ocelot.UnitTests/Claims/ClaimsBuilderMiddlewareTests.cs b/test/Ocelot.UnitTests/Claims/ClaimsBuilderMiddlewareTests.cs index 3eb86d1e..2470a6fb 100644 --- a/test/Ocelot.UnitTests/Claims/ClaimsBuilderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Claims/ClaimsBuilderMiddlewareTests.cs @@ -31,7 +31,7 @@ [Fact] public void should_call_claims_to_request_correctly() { - var downstreamRoute = new DownstreamRoute(new List(), + var downstreamRoute = new DownstreamRoute(new List(), new ReRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithClaimsToClaims(new List diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs index 1182a56f..d39fa394 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs @@ -18,6 +18,7 @@ namespace Ocelot.UnitTests.Configuration using Ocelot.DependencyInjection; using Ocelot.Errors; using Ocelot.UnitTests.TestData; + using Ocelot.Values; public class FileConfigurationCreatorTests { @@ -367,7 +368,7 @@ namespace Ocelot.UnitTests.Configuration .WithDownstreamPathTemplate("/products/{productId}") .WithUpstreamPathTemplate("/api/products/{productId}") .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamTemplatePattern("(?i)/api/products/.*/$") + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("(?i)/api/products/.*/$", 1)) .Build() })) .BDDfy(); @@ -580,7 +581,7 @@ namespace Ocelot.UnitTests.Configuration result.DownstreamPathTemplate.Value.ShouldBe(expected.DownstreamPathTemplate.Value); result.UpstreamHttpMethod.ShouldBe(expected.UpstreamHttpMethod); result.UpstreamPathTemplate.Value.ShouldBe(expected.UpstreamPathTemplate.Value); - result.UpstreamTemplatePattern.ShouldBe(expected.UpstreamTemplatePattern); + result.UpstreamTemplatePattern?.Template.ShouldBe(expected.UpstreamTemplatePattern?.Template); result.ClaimsToClaims.Count.ShouldBe(expected.ClaimsToClaims.Count); result.ClaimsToHeaders.Count.ShouldBe(expected.ClaimsToHeaders.Count); result.ClaimsToQueries.Count.ShouldBe(expected.ClaimsToQueries.Count); @@ -623,7 +624,7 @@ namespace Ocelot.UnitTests.Configuration { _upstreamTemplatePatternCreator .Setup(x => x.Create(It.IsAny())) - .Returns(pattern); + .Returns(new UpstreamPathTemplate(pattern, 1)); } private void ThenTheRequestIdKeyCreatorIsCalledCorrectly() diff --git a/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs index d5c9774b..87e2f2d6 100644 --- a/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs @@ -1,5 +1,7 @@ +using System; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; +using Ocelot.Values; using Shouldly; using TestStack.BDDfy; using Xunit; @@ -10,7 +12,7 @@ namespace Ocelot.UnitTests.Configuration { private FileReRoute _fileReRoute; private UpstreamTemplatePatternCreator _creator; - private string _result; + private UpstreamPathTemplate _result; public UpstreamTemplatePatternCreatorTests() { @@ -29,6 +31,7 @@ namespace Ocelot.UnitTests.Configuration this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) .When(x => x.WhenICreateTheTemplatePattern()) .Then(x => x.ThenTheFollowingIsReturned("^(?i)/PRODUCTS/[0-9a-zA-Z].*$")) + .And(x => ThenThePriorityIs(1)) .BDDfy(); } @@ -45,6 +48,7 @@ namespace Ocelot.UnitTests.Configuration this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) .When(x => x.WhenICreateTheTemplatePattern()) .Then(x => x.ThenTheFollowingIsReturned("^(?i)/PRODUCTS(/|)$")) + .And(x => ThenThePriorityIs(1)) .BDDfy(); } @@ -59,6 +63,7 @@ namespace Ocelot.UnitTests.Configuration this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) .When(x => x.WhenICreateTheTemplatePattern()) .Then(x => x.ThenTheFollowingIsReturned("^/PRODUCTS/[0-9a-zA-Z].*$")) + .And(x => ThenThePriorityIs(1)) .BDDfy(); } @@ -74,6 +79,7 @@ namespace Ocelot.UnitTests.Configuration this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) .When(x => x.WhenICreateTheTemplatePattern()) .Then(x => x.ThenTheFollowingIsReturned("^/api/products/[0-9a-zA-Z].*$")) + .And(x => ThenThePriorityIs(1)) .BDDfy(); } @@ -89,6 +95,7 @@ namespace Ocelot.UnitTests.Configuration this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) .When(x => x.WhenICreateTheTemplatePattern()) .Then(x => x.ThenTheFollowingIsReturned("^/api/products/[0-9a-zA-Z].*/variants/[0-9a-zA-Z].*$")) + .And(x => ThenThePriorityIs(1)) .BDDfy(); } @@ -104,6 +111,7 @@ namespace Ocelot.UnitTests.Configuration this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) .When(x => x.WhenICreateTheTemplatePattern()) .Then(x => x.ThenTheFollowingIsReturned("^/api/products/[0-9a-zA-Z].*/variants/[0-9a-zA-Z].*(/|)$")) + .And(x => ThenThePriorityIs(1)) .BDDfy(); } @@ -118,6 +126,7 @@ namespace Ocelot.UnitTests.Configuration this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) .When(x => x.WhenICreateTheTemplatePattern()) .Then(x => x.ThenTheFollowingIsReturned("^/$")) + .And(x => ThenThePriorityIs(1)) .BDDfy(); } @@ -132,6 +141,7 @@ namespace Ocelot.UnitTests.Configuration this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) .When(x => x.WhenICreateTheTemplatePattern()) .Then(x => x.ThenTheFollowingIsReturned("^/.*")) + .And(x => ThenThePriorityIs(0)) .BDDfy(); } @@ -147,6 +157,7 @@ namespace Ocelot.UnitTests.Configuration this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) .When(x => x.WhenICreateTheTemplatePattern()) .Then(x => x.ThenTheFollowingIsReturned("^/[0-9a-zA-Z].*/products/variants/[0-9a-zA-Z].*(/|)$")) + .And(x => ThenThePriorityIs(1)) .BDDfy(); } @@ -162,7 +173,12 @@ namespace Ocelot.UnitTests.Configuration private void ThenTheFollowingIsReturned(string expected) { - _result.ShouldBe(expected); + _result.Template.ShouldBe(expected); + } + + private void ThenThePriorityIs(int v) + { + _result.Priority.ShouldBe(v); } } } \ No newline at end of file diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs index 9b241fbe..5781996b 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs @@ -38,7 +38,7 @@ this.Given(x => x.GivenTheDownStreamRouteFinderReturns( new DownstreamRoute( - new List(), + new List(), new ReRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithUpstreamHttpMethod(new List { "Get" }) diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs index a1839bea..1593b3f3 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs @@ -2,11 +2,13 @@ using Moq; using Ocelot.Configuration; using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Creator; using Ocelot.Configuration.Provider; using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder.Finder; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Responses; +using Ocelot.Values; using Shouldly; using TestStack.BDDfy; using Xunit; @@ -17,7 +19,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder { private readonly IDownstreamRouteFinder _downstreamRouteFinder; private readonly Mock _mockMatcher; - private readonly Mock _finder; + private readonly Mock _finder; private string _upstreamUrlPath; private Response _result; private List _reRoutesConfig; @@ -28,10 +30,84 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder public DownstreamRouteFinderTests() { _mockMatcher = new Mock(); - _finder = new Mock(); + _finder = new Mock(); _downstreamRouteFinder = new Ocelot.DownstreamRouteFinder.Finder.DownstreamRouteFinder(_mockMatcher.Object, _finder.Object); } + + [Fact] + public void should_return_highest_priority_when_first() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) + .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( + new OkResponse>(new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new ReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamPathTemplate("someUpstreamPath") + .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("test", 1)) + .Build(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamPathTemplate("someUpstreamPath") + .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("test", 0)) + .Build() + }, string.Empty, serviceProviderConfig)) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) + .When(x => x.WhenICallTheFinder()) + .Then(x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("test", 1)) + .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("test", 1)) + .Build() + ))) + .BDDfy(); + } + + [Fact] + public void should_return_highest_priority_when_lowest() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) + .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( + new OkResponse>(new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new ReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamPathTemplate("someUpstreamPath") + .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("test", 0)) + .Build(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamPathTemplate("someUpstreamPath") + .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("test", 1)) + .Build() + }, string.Empty, serviceProviderConfig)) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) + .When(x => x.WhenICallTheFinder()) + .Then(x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("test", 1)) + .WithUpstreamHttpMethod(new List { "Post" }) + .Build() + ))) + .BDDfy(); + } + [Fact] public void should_return_route() { @@ -39,15 +115,15 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) .And(x =>x.GivenTheTemplateVariableAndNameFinderReturns( - new OkResponse>( - new List()))) + new OkResponse>( + new List()))) .And(x => x.GivenTheConfigurationIs(new List { new ReRouteBuilder() .WithDownstreamPathTemplate("someDownstreamPath") .WithUpstreamPathTemplate("someUpstreamPath") .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamTemplatePattern("someUpstreamPath") + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("someUpstreamPath", 1)) .Build() }, string.Empty, serviceProviderConfig )) @@ -56,10 +132,11 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .When(x => x.WhenICallTheFinder()) .Then( x => x.ThenTheFollowingIsReturned(new DownstreamRoute( - new List(), + new List(), new ReRouteBuilder() .WithDownstreamPathTemplate("someDownstreamPath") .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("someUpstreamPath", 1)) .Build() ))) .And(x => x.ThenTheUrlMatcherIsCalledCorrectly()) @@ -74,15 +151,15 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher")) .And(x =>x.GivenTheTemplateVariableAndNameFinderReturns( - new OkResponse>( - new List()))) + new OkResponse>( + new List()))) .And(x => x.GivenTheConfigurationIs(new List { new ReRouteBuilder() .WithDownstreamPathTemplate("someDownstreamPath") .WithUpstreamPathTemplate("someUpstreamPath") .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamTemplatePattern("someUpstreamPath") + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("someUpstreamPath", 1)) .Build() }, string.Empty, serviceProviderConfig )) @@ -91,10 +168,11 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .When(x => x.WhenICallTheFinder()) .Then( x => x.ThenTheFollowingIsReturned(new DownstreamRoute( - new List(), + new List(), new ReRouteBuilder() .WithDownstreamPathTemplate("someDownstreamPath") .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("someUpstreamPath", 1)) .Build() ))) .And(x => x.ThenTheUrlMatcherIsCalledCorrectly("matchInUrlMatcher")) @@ -110,14 +188,14 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .And( x => x.GivenTheTemplateVariableAndNameFinderReturns( - new OkResponse>(new List()))) + new OkResponse>(new List()))) .And(x => x.GivenTheConfigurationIs(new List { new ReRouteBuilder() .WithDownstreamPathTemplate("someDownstreamPath") .WithUpstreamPathTemplate("someUpstreamPath") .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamTemplatePattern("someUpstreamPath") + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("someUpstreamPath", 1)) .Build() }, string.Empty, serviceProviderConfig )) @@ -125,10 +203,11 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) .When(x => x.WhenICallTheFinder()) .Then( - x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), + x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), new ReRouteBuilder() .WithDownstreamPathTemplate("someDownstreamPath") .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("someUpstreamPath", 1)) .Build() ))) .And(x => x.ThenTheUrlMatcherIsNotCalled()) @@ -144,20 +223,20 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .And( x => x.GivenTheTemplateVariableAndNameFinderReturns( - new OkResponse>(new List()))) + new OkResponse>(new List()))) .And(x => x.GivenTheConfigurationIs(new List { new ReRouteBuilder() .WithDownstreamPathTemplate("someDownstreamPath") .WithUpstreamPathTemplate("someUpstreamPath") .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamTemplatePattern("") + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("", 1)) .Build(), new ReRouteBuilder() .WithDownstreamPathTemplate("someDownstreamPathForAPost") .WithUpstreamPathTemplate("someUpstreamPath") .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamTemplatePattern("") + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("", 1)) .Build() }, string.Empty, serviceProviderConfig )) @@ -165,10 +244,11 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) .When(x => x.WhenICallTheFinder()) .Then( - x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), + x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), new ReRouteBuilder() .WithDownstreamPathTemplate("someDownstreamPathForAPost") .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("", 1)) .Build() ))) .BDDfy(); @@ -186,7 +266,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .WithDownstreamPathTemplate("somPath") .WithUpstreamPathTemplate("somePath") .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamTemplatePattern("somePath") + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("somePath", 1)) .Build(), }, string.Empty, serviceProviderConfig )) @@ -208,14 +288,14 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .And( x => x.GivenTheTemplateVariableAndNameFinderReturns( - new OkResponse>(new List()))) + new OkResponse>(new List()))) .And(x => x.GivenTheConfigurationIs(new List { new ReRouteBuilder() .WithDownstreamPathTemplate("someDownstreamPath") .WithUpstreamPathTemplate("someUpstreamPath") .WithUpstreamHttpMethod(new List { "Get", "Post" }) - .WithUpstreamTemplatePattern("") + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("", 1)) .Build() }, string.Empty, serviceProviderConfig )) @@ -223,10 +303,11 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) .When(x => x.WhenICallTheFinder()) .Then( - x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), + x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), new ReRouteBuilder() .WithDownstreamPathTemplate("someDownstreamPath") .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("", 1)) .Build() ))) .BDDfy(); @@ -241,14 +322,14 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .And( x => x.GivenTheTemplateVariableAndNameFinderReturns( - new OkResponse>(new List()))) + new OkResponse>(new List()))) .And(x => x.GivenTheConfigurationIs(new List { new ReRouteBuilder() .WithDownstreamPathTemplate("someDownstreamPath") .WithUpstreamPathTemplate("someUpstreamPath") .WithUpstreamHttpMethod(new List()) - .WithUpstreamTemplatePattern("") + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("", 1)) .Build() }, string.Empty, serviceProviderConfig )) @@ -256,10 +337,11 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) .When(x => x.WhenICallTheFinder()) .Then( - x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), + x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), new ReRouteBuilder() .WithDownstreamPathTemplate("someDownstreamPath") .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("", 1)) .Build() ))) .BDDfy(); @@ -274,14 +356,14 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .And( x => x.GivenTheTemplateVariableAndNameFinderReturns( - new OkResponse>(new List()))) + new OkResponse>(new List()))) .And(x => x.GivenTheConfigurationIs(new List { new ReRouteBuilder() .WithDownstreamPathTemplate("someDownstreamPath") .WithUpstreamPathTemplate("someUpstreamPath") .WithUpstreamHttpMethod(new List { "Get", "Patch", "Delete" }) - .WithUpstreamTemplatePattern("") + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("", 1)) .Build() }, string.Empty, serviceProviderConfig )) @@ -294,7 +376,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .BDDfy(); } - private void GivenTheTemplateVariableAndNameFinderReturns(Response> response) + private void GivenTheTemplateVariableAndNameFinderReturns(Response> response) { _finder .Setup(x => x.Find(It.IsAny(), It.IsAny())) @@ -356,14 +438,12 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder private void ThenTheFollowingIsReturned(DownstreamRoute expected) { _result.Data.ReRoute.DownstreamPathTemplate.Value.ShouldBe(expected.ReRoute.DownstreamPathTemplate.Value); + _result.Data.ReRoute.UpstreamTemplatePattern.Priority.ShouldBe(expected.ReRoute.UpstreamTemplatePattern.Priority); for (int i = 0; i < _result.Data.TemplatePlaceholderNameAndValues.Count; i++) { - _result.Data.TemplatePlaceholderNameAndValues[i].TemplateVariableName.ShouldBe( - expected.TemplatePlaceholderNameAndValues[i].TemplateVariableName); - - _result.Data.TemplatePlaceholderNameAndValues[i].TemplateVariableValue.ShouldBe( - expected.TemplatePlaceholderNameAndValues[i].TemplateVariableValue); + _result.Data.TemplatePlaceholderNameAndValues[i].Name.ShouldBe(expected.TemplatePlaceholderNameAndValues[i].Name); + _result.Data.TemplatePlaceholderNameAndValues[i].Value.ShouldBe(expected.TemplatePlaceholderNameAndValues[i].Value); } _result.IsError.ShouldBeFalse(); diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs index 9f76228b..f57ed842 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs @@ -18,6 +18,18 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher _urlMatcher = new RegExUrlMatcher(); } + [Fact] + public void should_not_match_slash_becaue_we_need_to_match_something_after_it() + { + const string RegExForwardSlashAndOnePlaceHolder = "^/[0-9a-zA-Z].*"; + + this.Given(x => x.GivenIHaveAUpstreamPath("/")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern(RegExForwardSlashAndOnePlaceHolder)) + .When(x => x.WhenIMatchThePaths()) + .And(x => x.ThenTheResultIsFalse()) + .BDDfy(); + } + [Fact] public void should_not_match_forward_slash_only_regex() { diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/TemplateVariableNameAndValueFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinderTests.cs similarity index 57% rename from test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/TemplateVariableNameAndValueFinderTests.cs rename to test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinderTests.cs index fded9ecf..9fa9366b 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/TemplateVariableNameAndValueFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinderTests.cs @@ -8,14 +8,14 @@ using Xunit; namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher { - public class UrlPathToUrlTemplateMatcherTests + public class UrlPathPlaceholderNameAndValueFinderTests { - private readonly IUrlPathPlaceholderNameAndValueFinder _finder; + private readonly IPlaceholderNameAndValueFinder _finder; private string _downstreamUrlPath; private string _downstreamPathTemplate; - private Response> _result; + private Response> _result; - public UrlPathToUrlTemplateMatcherTests() + public UrlPathPlaceholderNameAndValueFinderTests() { _finder = new UrlPathPlaceholderNameAndValueFinder(); } @@ -26,7 +26,82 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher this.Given(x => x.GivenIHaveAUpstreamPath("")) .And(x => x.GivenIHaveAnUpstreamUrlTemplate("")) .When(x => x.WhenIFindTheUrlVariableNamesAndValues()) - .And(x => x.ThenTheTemplatesVariablesAre(new List())) + .And(x => x.ThenTheTemplatesVariablesAre(new List())) + .BDDfy(); + } + + + [Fact] + public void can_match_down_stream_url_with_nothing_then_placeholder_no_value_is_blank() + { + var expectedTemplates = new List + { + new PlaceholderNameAndValue("{url}", "") + }; + + this.Given(x => x.GivenIHaveAUpstreamPath("")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplate("/{url}")) + .When(x => x.WhenIFindTheUrlVariableNamesAndValues()) + .And(x => x.ThenTheTemplatesVariablesAre(expectedTemplates)) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_nothing_then_placeholder_value_is_test() + { + var expectedTemplates = new List + { + new PlaceholderNameAndValue("{url}", "test") + }; + + this.Given(x => x.GivenIHaveAUpstreamPath("/test")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplate("/{url}")) + .When(x => x.WhenIFindTheUrlVariableNamesAndValues()) + .And(x => x.ThenTheTemplatesVariablesAre(expectedTemplates)) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_forward_slash_then_placeholder_no_value_is_blank() + { + var expectedTemplates = new List + { + new PlaceholderNameAndValue("{url}", "") + }; + + this.Given(x => x.GivenIHaveAUpstreamPath("/")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplate("/{url}")) + .When(x => x.WhenIFindTheUrlVariableNamesAndValues()) + .And(x => x.ThenTheTemplatesVariablesAre(expectedTemplates)) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_forward_slash() + { + var expectedTemplates = new List + { + }; + + this.Given(x => x.GivenIHaveAUpstreamPath("/")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplate("/")) + .When(x => x.WhenIFindTheUrlVariableNamesAndValues()) + .And(x => x.ThenTheTemplatesVariablesAre(expectedTemplates)) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_forward_slash_then_placeholder_then_another_value() + { + var expectedTemplates = new List + { + new PlaceholderNameAndValue("{url}", "1") + }; + + this.Given(x => x.GivenIHaveAUpstreamPath("/1/products")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplate("/{url}/products")) + .When(x => x.WhenIFindTheUrlVariableNamesAndValues()) + .And(x => x.ThenTheTemplatesVariablesAre(expectedTemplates)) .BDDfy(); } @@ -36,7 +111,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher this.Given(x => x.GivenIHaveAUpstreamPath("/products")) .And(x => x.GivenIHaveAnUpstreamUrlTemplate("/products/")) .When(x => x.WhenIFindTheUrlVariableNamesAndValues()) - .And(x => x.ThenTheTemplatesVariablesAre(new List())) + .And(x => x.ThenTheTemplatesVariablesAre(new List())) .BDDfy(); } @@ -46,7 +121,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher this.Given(x => x.GivenIHaveAUpstreamPath("api")) .Given(x => x.GivenIHaveAnUpstreamUrlTemplate("api")) .When(x => x.WhenIFindTheUrlVariableNamesAndValues()) - .And(x => x.ThenTheTemplatesVariablesAre(new List())) + .And(x => x.ThenTheTemplatesVariablesAre(new List())) .BDDfy(); } @@ -56,7 +131,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher this.Given(x => x.GivenIHaveAUpstreamPath("api/")) .Given(x => x.GivenIHaveAnUpstreamUrlTemplate("api/")) .When(x => x.WhenIFindTheUrlVariableNamesAndValues()) - .And(x => x.ThenTheTemplatesVariablesAre(new List())) + .And(x => x.ThenTheTemplatesVariablesAre(new List())) .BDDfy(); } @@ -66,16 +141,16 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/")) .Given(x => x.GivenIHaveAnUpstreamUrlTemplate("api/product/products/")) .When(x => x.WhenIFindTheUrlVariableNamesAndValues()) - .And(x => x.ThenTheTemplatesVariablesAre(new List())) + .And(x => x.ThenTheTemplatesVariablesAre(new List())) .BDDfy(); } [Fact] public void can_match_down_stream_url_with_downstream_template_with_one_place_holder() { - var expectedTemplates = new List + var expectedTemplates = new List { - new UrlPathPlaceholderNameAndValue("{productId}", "1") + new PlaceholderNameAndValue("{productId}", "1") }; this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1")) @@ -88,10 +163,10 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher [Fact] public void can_match_down_stream_url_with_downstream_template_with_two_place_holders() { - var expectedTemplates = new List + var expectedTemplates = new List { - new UrlPathPlaceholderNameAndValue("{productId}", "1"), - new UrlPathPlaceholderNameAndValue("{categoryId}", "2") + new PlaceholderNameAndValue("{productId}", "1"), + new PlaceholderNameAndValue("{categoryId}", "2") }; this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/2")) @@ -104,10 +179,10 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher [Fact] public void can_match_down_stream_url_with_downstream_template_with_two_place_holders_seperated_by_something() { - var expectedTemplates = new List + var expectedTemplates = new List { - new UrlPathPlaceholderNameAndValue("{productId}", "1"), - new UrlPathPlaceholderNameAndValue("{categoryId}", "2") + new PlaceholderNameAndValue("{productId}", "1"), + new PlaceholderNameAndValue("{categoryId}", "2") }; this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/categories/2")) @@ -120,11 +195,11 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher [Fact] public void can_match_down_stream_url_with_downstream_template_with_three_place_holders_seperated_by_something() { - var expectedTemplates = new List + var expectedTemplates = new List { - new UrlPathPlaceholderNameAndValue("{productId}", "1"), - new UrlPathPlaceholderNameAndValue("{categoryId}", "2"), - new UrlPathPlaceholderNameAndValue("{variantId}", "123") + new PlaceholderNameAndValue("{productId}", "1"), + new PlaceholderNameAndValue("{categoryId}", "2"), + new PlaceholderNameAndValue("{variantId}", "123") }; this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/categories/2/variant/123")) @@ -137,10 +212,10 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher [Fact] public void can_match_down_stream_url_with_downstream_template_with_three_place_holders() { - var expectedTemplates = new List + var expectedTemplates = new List { - new UrlPathPlaceholderNameAndValue("{productId}", "1"), - new UrlPathPlaceholderNameAndValue("{categoryId}", "2") + new PlaceholderNameAndValue("{productId}", "1"), + new PlaceholderNameAndValue("{categoryId}", "2") }; this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/categories/2/variant/")) @@ -153,9 +228,9 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher [Fact] public void can_match_down_stream_url_with_downstream_template_with_place_holder_to_final_url_path() { - var expectedTemplates = new List + var expectedTemplates = new List { - new UrlPathPlaceholderNameAndValue("{finalUrlPath}", "product/products/categories/"), + new PlaceholderNameAndValue("{finalUrlPath}", "product/products/categories/"), }; this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/categories/")) @@ -165,13 +240,12 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher .BDDfy(); } - private void ThenTheTemplatesVariablesAre(List expectedResults) + private void ThenTheTemplatesVariablesAre(List expectedResults) { foreach (var expectedResult in expectedResults) { - var result = _result.Data - .First(t => t.TemplateVariableName == expectedResult.TemplateVariableName); - result.TemplateVariableValue.ShouldBe(expectedResult.TemplateVariableValue); + var result = _result.Data.First(t => t.Name == expectedResult.Name); + result.Value.ShouldBe(expectedResult.Value); } } diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs index 1af70f71..04afdf3a 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs @@ -47,7 +47,7 @@ { this.Given(x => x.GivenTheDownStreamRouteIs( new DownstreamRoute( - new List(), + new List(), new ReRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithUpstreamHttpMethod(new List { "Get" }) @@ -91,7 +91,7 @@ { _downstreamPath = new OkResponse(new DownstreamPath(path)); _downstreamUrlTemplateVariableReplacer - .Setup(x => x.Replace(It.IsAny(), It.IsAny>())) + .Setup(x => x.Replace(It.IsAny(), It.IsAny>())) .Returns(_downstreamPath); } diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs index 4939c901..7aa8fc16 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs @@ -27,7 +27,7 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer { this.Given(x => x.GivenThereIsAUrlMatch( new DownstreamRoute( - new List(), + new List(), new ReRouteBuilder() .WithUpstreamHttpMethod(new List { "Get" }) .Build()))) @@ -41,7 +41,7 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer { this.Given(x => x.GivenThereIsAUrlMatch( new DownstreamRoute( - new List(), + new List(), new ReRouteBuilder() .WithDownstreamPathTemplate("/") .WithUpstreamHttpMethod(new List { "Get" }) @@ -54,7 +54,7 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer [Fact] public void can_replace_url_no_slash() { - this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), new ReRouteBuilder() .WithDownstreamPathTemplate("api") .WithUpstreamHttpMethod(new List { "Get" }) @@ -67,7 +67,7 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer [Fact] public void can_replace_url_one_slash() { - this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), new ReRouteBuilder() .WithDownstreamPathTemplate("api/") .WithUpstreamHttpMethod(new List { "Get" }) @@ -80,7 +80,7 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer [Fact] public void can_replace_url_multiple_slash() { - this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), new ReRouteBuilder() .WithDownstreamPathTemplate("api/product/products/") .WithUpstreamHttpMethod(new List { "Get" }) @@ -93,9 +93,9 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer [Fact] public void can_replace_url_one_template_variable() { - var templateVariables = new List() + var templateVariables = new List() { - new UrlPathPlaceholderNameAndValue("{productId}", "1") + new PlaceholderNameAndValue("{productId}", "1") }; this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, @@ -111,9 +111,9 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer [Fact] public void can_replace_url_one_template_variable_with_path_after() { - var templateVariables = new List() + var templateVariables = new List() { - new UrlPathPlaceholderNameAndValue("{productId}", "1") + new PlaceholderNameAndValue("{productId}", "1") }; this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, @@ -129,10 +129,10 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer [Fact] public void can_replace_url_two_template_variable() { - var templateVariables = new List() + var templateVariables = new List() { - new UrlPathPlaceholderNameAndValue("{productId}", "1"), - new UrlPathPlaceholderNameAndValue("{variantId}", "12") + new PlaceholderNameAndValue("{productId}", "1"), + new PlaceholderNameAndValue("{variantId}", "12") }; this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, @@ -148,11 +148,11 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer [Fact] public void can_replace_url_three_template_variable() { - var templateVariables = new List() + var templateVariables = new List() { - new UrlPathPlaceholderNameAndValue("{productId}", "1"), - new UrlPathPlaceholderNameAndValue("{variantId}", "12"), - new UrlPathPlaceholderNameAndValue("{categoryId}", "34") + new PlaceholderNameAndValue("{productId}", "1"), + new PlaceholderNameAndValue("{variantId}", "12"), + new PlaceholderNameAndValue("{categoryId}", "34") }; this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, diff --git a/test/Ocelot.UnitTests/Headers/HttpRequestHeadersBuilderMiddlewareTests.cs b/test/Ocelot.UnitTests/Headers/HttpRequestHeadersBuilderMiddlewareTests.cs index 76a75d0a..20dbe84d 100644 --- a/test/Ocelot.UnitTests/Headers/HttpRequestHeadersBuilderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpRequestHeadersBuilderMiddlewareTests.cs @@ -37,7 +37,7 @@ [Fact] public void should_call_add_headers_to_request_correctly() { - var downstreamRoute = new DownstreamRoute(new List(), + var downstreamRoute = new DownstreamRoute(new List(), new ReRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithClaimsToHeaders(new List diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs index 002e64a0..936e9249 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs @@ -47,7 +47,7 @@ namespace Ocelot.UnitTests.LoadBalancer [Fact] public void should_call_scoped_data_repository_correctly() { - var downstreamRoute = new DownstreamRoute(new List(), + var downstreamRoute = new DownstreamRoute(new List(), new ReRouteBuilder() .WithUpstreamHttpMethod(new List { "Get" }) .Build()); @@ -68,7 +68,7 @@ namespace Ocelot.UnitTests.LoadBalancer [Fact] public void should_set_pipeline_error_if_cannot_get_load_balancer() { - var downstreamRoute = new DownstreamRoute(new List(), + var downstreamRoute = new DownstreamRoute(new List(), new ReRouteBuilder() .WithUpstreamHttpMethod(new List { "Get" }) .Build()); @@ -88,7 +88,7 @@ namespace Ocelot.UnitTests.LoadBalancer [Fact] public void should_set_pipeline_error_if_cannot_get_least() { - var downstreamRoute = new DownstreamRoute(new List(), + var downstreamRoute = new DownstreamRoute(new List(), new ReRouteBuilder() .WithUpstreamHttpMethod(new List { "Get" }) .Build()); diff --git a/test/Ocelot.UnitTests/QueryStrings/QueryStringBuilderMiddlewareTests.cs b/test/Ocelot.UnitTests/QueryStrings/QueryStringBuilderMiddlewareTests.cs index 4bbcfd2c..a4a3b197 100644 --- a/test/Ocelot.UnitTests/QueryStrings/QueryStringBuilderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/QueryStrings/QueryStringBuilderMiddlewareTests.cs @@ -37,7 +37,7 @@ [Fact] public void should_call_add_queries_correctly() { - var downstreamRoute = new DownstreamRoute(new List(), + var downstreamRoute = new DownstreamRoute(new List(), new ReRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithClaimsToQueries(new List diff --git a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs index 1b73e233..e832760b 100644 --- a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs @@ -31,7 +31,7 @@ [Fact] public void should_call_middleware_and_ratelimiting() { - var downstreamRoute = new DownstreamRoute(new List(), + var downstreamRoute = new DownstreamRoute(new List(), new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions( new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List(), false, "", "", new Ocelot.Configuration.RateLimitRule("1s", 100, 3), 429)) .WithUpstreamHttpMethod(new List { "Get" }) @@ -48,7 +48,7 @@ [Fact] public void should_call_middleware_withWhitelistClient() { - var downstreamRoute = new DownstreamRoute(new List(), + var downstreamRoute = new DownstreamRoute(new List(), new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions( new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List() { "ocelotclient2" }, false, "", "", new RateLimitRule( "1s", 100,3),429)) .WithUpstreamHttpMethod(new List { "Get" }) diff --git a/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs b/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs index 4c72d137..10cbc120 100644 --- a/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs @@ -47,7 +47,7 @@ public void should_call_scoped_data_repository_correctly() { - var downstreamRoute = new DownstreamRoute(new List(), + var downstreamRoute = new DownstreamRoute(new List(), new ReRouteBuilder() .WithRequestIdKey("LSRequestId") .WithUpstreamHttpMethod(new List { "Get" }) diff --git a/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs b/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs index a34290f1..4d684084 100644 --- a/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs @@ -40,7 +40,7 @@ [Fact] public void should_pass_down_request_id_from_upstream_request() { - var downstreamRoute = new DownstreamRoute(new List(), + var downstreamRoute = new DownstreamRoute(new List(), new ReRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithRequestIdKey("LSRequestId") @@ -59,7 +59,7 @@ [Fact] public void should_add_request_id_when_not_on_upstream_request() { - var downstreamRoute = new DownstreamRoute(new List(), + var downstreamRoute = new DownstreamRoute(new List(), new ReRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithRequestIdKey("LSRequestId") From 464f2661483ff2e5f91ca8e2ad78da94505d2ba5 Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Fri, 5 Jan 2018 21:49:03 +0000 Subject: [PATCH 12/13] Feature/fix #185 round 2 (#188) * Changed routing to support a catch all style * refactoring placeholder tuff * implemented simple priority in the routing From 6a20baeb971ed7cdfefad55da669dffd359571b5 Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Sat, 6 Jan 2018 16:39:05 +0000 Subject: [PATCH 13/13] Improving logging and request id (#189) * hacking around to work out why logging and request id isnt working * pass request id into logger so it can be structured, removed a bunch of debug logging we dont need because diagnostic trace gets it * changed config dependency * always have tracing available * made it so we dont need to pass config into services.AddOcelot anymore with .net core 2.0 * add test * lots of changes relating to logging and request ids, also updated documentation * fixed failing test i missed --- docs/features/logging.rst | 8 +- docs/features/requestid.rst | 56 +++++++--- docs/introduction/gettingstarted.rst | 102 +++++++++++++++--- .../Creator/FileOcelotConfigurationCreator.cs | 2 +- .../Creator/RequestIdKeyCreator.cs | 8 +- .../Configuration/IOcelotConfiguration.cs | 1 + .../Configuration/OcelotConfiguration.cs | 4 +- .../DependencyInjection/OcelotBuilder.cs | 8 +- .../ServiceCollectionExtensions.cs | 12 ++- .../DownstreamRouteFinderMiddleware.cs | 1 - .../Middleware/ExceptionHandlerMiddleware.cs | 45 +++++++- .../RequestData/HttpDataRepository.cs | 16 +++ .../IRequestScopedDataRepository.cs | 1 + src/Ocelot/Logging/AspDotNetLogger.cs | 53 +++++++-- src/Ocelot/Logging/IOcelotLoggerFactory.cs | 10 +- .../Middleware/OcelotMiddlewareExtensions.cs | 18 ++-- .../DownstreamRequestInitialiserMiddleware.cs | 6 -- .../HttpRequestBuilderMiddleware.cs | 6 -- ...eware.cs => ReRouteRequestIdMiddleware.cs} | 21 +++- .../RequestIdMiddlewareExtensions.cs | 2 +- .../Middleware/HttpRequesterMiddleware.cs | 4 - .../AcceptanceTestsStartup.cs | 45 +------- test/Ocelot.AcceptanceTests/ConsulStartup.cs | 15 +-- .../StartupWithConsulAndCustomCacheHandle.cs | 29 +++++ .../StartupWithCustomCacheHandle.cs | 28 +++++ test/Ocelot.AcceptanceTests/Steps.cs | 4 +- .../IntegrationTestsStartup.cs | 4 +- test/Ocelot.IntegrationTests/RaftStartup.cs | 5 +- test/Ocelot.ManualTest/ManualTestStartup.cs | 27 +---- test/Ocelot.ManualTest/Program.cs | 16 +++ test/Ocelot.ManualTest/appsettings.json | 8 +- test/Ocelot.ManualTest/configuration.json | 1 + .../FileConfigurationSetterTests.cs | 2 +- .../InMemoryConfigurationRepositoryTests.cs | 2 + .../OcelotConfigurationProviderTests.cs | 4 +- .../Configuration/RequestIdKeyCreatorTests.cs | 7 +- .../DependencyInjection/OcelotBuilderTests.cs | 24 ++++- .../DownstreamRouteFinderMiddlewareTests.cs | 2 +- .../DownstreamRouteFinderTests.cs | 2 +- .../Errors/ExceptionHandlerMiddlewareTests.cs | 57 ++++++++++ .../Infrastructure/HttpDataRepositoryTests.cs | 19 +++- ....cs => ReRouteRequestIdMiddlewareTests.cs} | 68 +++++++++++- .../ServerHostedMiddlewareTest.cs | 6 ++ 43 files changed, 562 insertions(+), 197 deletions(-) rename src/Ocelot/RequestId/Middleware/{RequestIdMiddleware.cs => ReRouteRequestIdMiddleware.cs} (71%) create mode 100644 test/Ocelot.AcceptanceTests/StartupWithConsulAndCustomCacheHandle.cs create mode 100644 test/Ocelot.AcceptanceTests/StartupWithCustomCacheHandle.cs rename test/Ocelot.UnitTests/RequestId/{RequestIdMiddlewareTests.cs => ReRouteRequestIdMiddlewareTests.cs} (59%) diff --git a/docs/features/logging.rst b/docs/features/logging.rst index 888640fc..fba8e5dc 100644 --- a/docs/features/logging.rst +++ b/docs/features/logging.rst @@ -3,11 +3,11 @@ Logging Ocelot uses the standard logging interfaces ILoggerFactory / ILogger at the moment. This is encapsulated in IOcelotLogger / IOcelotLoggerFactory with an implementation -for the standard asp.net core logging stuff at the moment. +for the standard asp.net core logging stuff at the moment. This is because Ocelot add's some extra info to the logs such as request id if it is configured. -There are a bunch of debugging logs in the ocelot middlewares however I think the -system probably needs more logging in the code it calls into. Other than the debugging -there is a global error handler that should catch any errors thrown and log them as errors. +There is a global error handler that should catch any exceptions thrown and log them as errors. + +Finally if logging is set to trace level Ocelot will log starting, finishing and any middlewares that throw an exception which can be quite useful. The reason for not just using bog standard framework logging is that I could not work out how to override the request id that get's logged when setting IncludeScopes diff --git a/docs/features/requestid.rst b/docs/features/requestid.rst index cf5e443b..a9fe4c41 100644 --- a/docs/features/requestid.rst +++ b/docs/features/requestid.rst @@ -4,25 +4,57 @@ Request Id / Correlation Id Ocelot supports a client sending a request id in the form of a header. If set Ocelot will use the requestid for logging as soon as it becomes available in the middleware pipeline. Ocelot will also forward the request id with the specified header to the downstream service. -I'm not sure if have this spot on yet in terms of the pipeline order becasue there are a few logs -that don't get the users request id at the moment and ocelot just logs not set for request id -which sucks. You can still get the framework request id in the logs if you set -IncludeScopes true in your logging config. This can then be used to match up later logs that do -have an OcelotRequestId. -In order to use the requestid feature in your ReRoute configuration add this setting +You can still get the asp.net core request id in the logs if you set +IncludeScopes true in your logging config. + +In order to use the reques tid feature you have two options. + +*Global* + +In your configuration.json set the following in the GlobalConfiguration section. This will be used for all requests into Ocelot. + +.. code-block:: json + + "GlobalConfiguration": { + "RequestIdKey": "OcRequestId" + } + +I reccomend using the GlobalConfiguration unless you really need it to be ReRoute specific. + +*ReRoute* + +If you want to override this for a specific ReRoute add the following to configuration.json for the specific ReRoute. .. code-block:: json "RequestIdKey": "OcRequestId" -In this example OcRequestId is the request header that contains the clients request id. +Once Ocelot has identified the incoming requests matching ReRoute object it will set the request id based on the ReRoute configuration. -There is also a setting in the GlobalConfiguration section which will override whatever has been -set at ReRoute level for the request id. The setting is as fllows. +This can lead to a small gotcha. If you set a GlobalConfiguration it is possible to get one request id until the ReRoute is identified and then another after that because the request id key can change. This is by design and is the best solution I can think of at the moment. In this case the OcelotLogger will show the request id and previous request id in the logs. -.. code-block:: json +Below is an example of the logging when set at Debug level for a normal request.. - "RequestIdKey": "OcRequestId" +.. code-block:: bash -It behaves in exactly the same way as the ReRoute level RequestIdKey settings. \ No newline at end of file + dbug: Ocelot.Errors.Middleware.ExceptionHandlerMiddleware[0] + requestId: asdf, previousRequestId: no previous request id, message: ocelot pipeline started, + dbug: Ocelot.DownstreamRouteFinder.Middleware.DownstreamRouteFinderMiddleware[0] + requestId: asdf, previousRequestId: no previous request id, message: upstream url path is {upstreamUrlPath}, + dbug: Ocelot.DownstreamRouteFinder.Middleware.DownstreamRouteFinderMiddleware[0] + requestId: asdf, previousRequestId: no previous request id, message: downstream template is {downstreamRoute.Data.ReRoute.DownstreamPath}, + dbug: Ocelot.RateLimit.Middleware.ClientRateLimitMiddleware[0] + requestId: asdf, previousRequestId: no previous request id, message: EndpointRateLimiting is not enabled for Ocelot.Values.PathTemplate, + dbug: Ocelot.Authorisation.Middleware.AuthorisationMiddleware[0] + requestId: 1234, previousRequestId: asdf, message: /posts/{postId} route does not require user to be authorised, + dbug: Ocelot.DownstreamUrlCreator.Middleware.DownstreamUrlCreatorMiddleware[0] + requestId: 1234, previousRequestId: asdf, message: downstream url is {downstreamUrl.Data.Value}, + dbug: Ocelot.Request.Middleware.HttpRequestBuilderMiddleware[0] + requestId: 1234, previousRequestId: asdf, message: setting upstream request, + dbug: Ocelot.Requester.Middleware.HttpRequesterMiddleware[0] + requestId: 1234, previousRequestId: asdf, message: setting http response message, + dbug: Ocelot.Responder.Middleware.ResponderMiddleware[0] + requestId: 1234, previousRequestId: asdf, message: no pipeline errors, setting and returning completed response, + dbug: Ocelot.Errors.Middleware.ExceptionHandlerMiddleware[0] + requestId: 1234, previousRequestId: asdf, message: ocelot pipeline finished, diff --git a/docs/introduction/gettingstarted.rst b/docs/introduction/gettingstarted.rst index da3a590f..1ac8a298 100644 --- a/docs/introduction/gettingstarted.rst +++ b/docs/introduction/gettingstarted.rst @@ -1,9 +1,11 @@ Getting Started =============== -Ocelot is designed to work with ASP.NET core only and is currently +Ocelot is designed to work with .NET Core only and is currently built to netcoreapp2.0 `this `_ documentation may prove helpful when working out if Ocelot would be suitable for you. +.NET Core 2.0 +^^^^^^^^^^^^^ **Install NuGet package** @@ -30,6 +32,86 @@ The following is a very basic configuration.json. It won't do anything but shoul Then in your Program.cs you will want to have the following. This can be changed if you don't wan't to use the default url e.g. UseUrls(someUrls) and should work as long as you keep the WebHostBuilder registration. +.. code-block:: csharp + + public class Program + { + public static void Main(string[] args) + { + IWebHostBuilder builder = new WebHostBuilder(); + builder.ConfigureServices(s => { + s.AddSingleton(builder); + }); + builder.UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + config.AddJsonFile("configuration.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureLogging((hostingContext, logging) => + { + logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + logging.AddConsole(); + }) + .UseIISIntegration() + .UseStartup(); + var host = builder.Build(); + host.Run(); + } + } + +Sadly we need to inject the IWebHostBuilder interface to get the applications scheme, url and port later. I cannot find a better way of doing this at the moment without setting this in a static or some kind of config. + +**Startup** + +An example startup using a json file for configuration can be seen below. This is the most basic startup and Ocelot has quite a few more options. Detailed in the rest of these docs! If you get a stuck a good place to look is at the ManualTests project in the source code. + +.. code-block:: csharp + + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddOcelot(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseOcelot().Wait(); + } + } + +.NET Core 1.0 +^^^^^^^^^^^^^ + +**Install NuGet package** + +Install Ocelot and it's dependecies using nuget. You will need to create a netcoreapp1.0+ projct and bring the package into it. Then follow the Startup below and :doc:`../features/configuration` sections +to get up and running. Please note you will need to choose one of the Ocelot packages from the NuGet feed. + +All versions can be found `here `_. + +**Configuration** + +The following is a very basic configuration.json. It won't do anything but should get Ocelot starting. + +.. code-block:: json + + { + "ReRoutes": [], + "GlobalConfiguration": {} + } + +**Program** + +Then in your Program.cs you will want to have the following. This can be changed if you +don't wan't to use the default url e.g. UseUrls(someUrls) and should work as long as you keep the WebHostBuilder registration. + .. code-block:: csharp public class Program @@ -57,7 +139,6 @@ Sadly we need to inject the IWebHostBuilder interface to get the applications sc **Startup** An example startup using a json file for configuration can be seen below. -Currently this is the only way to get configuration into Ocelot. .. code-block:: csharp @@ -79,22 +160,13 @@ Currently this is the only way to get configuration into Ocelot. public void ConfigureServices(IServiceCollection services) { - services.AddOcelot(Configuration) - .AddCacheManager(x => { - x.WithMicrosoftLogging(log => - { - log.AddConsole(LogLevel.Debug); - }) - .WithDictionaryHandle(); - });; - } + services.AddOcelot(Configuration); + } - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + public void Configure(IApplicationBuilder app) { - loggerFactory.AddConsole(Configuration.GetSection("Logging")); - app.UseOcelot().Wait(); } } -This is pretty much all you need to get going.......more to come! \ No newline at end of file +This is pretty much all you need to get going. \ No newline at end of file diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs index eed04cdb..a701d9a0 100644 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs @@ -97,7 +97,7 @@ namespace Ocelot.Configuration.Creator var serviceProviderConfiguration = _serviceProviderConfigCreator.Create(fileConfiguration.GlobalConfiguration); - var config = new OcelotConfiguration(reRoutes, _adminPath.Path, serviceProviderConfiguration); + var config = new OcelotConfiguration(reRoutes, _adminPath.Path, serviceProviderConfiguration, fileConfiguration.GlobalConfiguration.RequestIdKey); return new OkResponse(config); } diff --git a/src/Ocelot/Configuration/Creator/RequestIdKeyCreator.cs b/src/Ocelot/Configuration/Creator/RequestIdKeyCreator.cs index caf00402..dde171a8 100644 --- a/src/Ocelot/Configuration/Creator/RequestIdKeyCreator.cs +++ b/src/Ocelot/Configuration/Creator/RequestIdKeyCreator.cs @@ -6,11 +6,11 @@ namespace Ocelot.Configuration.Creator { public string Create(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration) { - var globalRequestIdConfiguration = !string.IsNullOrEmpty(globalConfiguration?.RequestIdKey); + var reRouteId = !string.IsNullOrEmpty(fileReRoute.RequestIdKey); - var requestIdKey = globalRequestIdConfiguration - ? globalConfiguration.RequestIdKey - : fileReRoute.RequestIdKey; + var requestIdKey = reRouteId + ? fileReRoute.RequestIdKey + : globalConfiguration.RequestIdKey; return requestIdKey; } diff --git a/src/Ocelot/Configuration/IOcelotConfiguration.cs b/src/Ocelot/Configuration/IOcelotConfiguration.cs index cb1ff606..5384630e 100644 --- a/src/Ocelot/Configuration/IOcelotConfiguration.cs +++ b/src/Ocelot/Configuration/IOcelotConfiguration.cs @@ -7,5 +7,6 @@ namespace Ocelot.Configuration List ReRoutes { get; } string AdministrationPath {get;} ServiceProviderConfiguration ServiceProviderConfiguration {get;} + string RequestId {get;} } } \ No newline at end of file diff --git a/src/Ocelot/Configuration/OcelotConfiguration.cs b/src/Ocelot/Configuration/OcelotConfiguration.cs index b4f5d169..2c7e973a 100644 --- a/src/Ocelot/Configuration/OcelotConfiguration.cs +++ b/src/Ocelot/Configuration/OcelotConfiguration.cs @@ -4,15 +4,17 @@ namespace Ocelot.Configuration { public class OcelotConfiguration : IOcelotConfiguration { - public OcelotConfiguration(List reRoutes, string administrationPath, ServiceProviderConfiguration serviceProviderConfiguration) + public OcelotConfiguration(List reRoutes, string administrationPath, ServiceProviderConfiguration serviceProviderConfiguration, string requestId) { ReRoutes = reRoutes; AdministrationPath = administrationPath; ServiceProviderConfiguration = serviceProviderConfiguration; + RequestId = requestId; } public List ReRoutes { get; } public string AdministrationPath {get;} public ServiceProviderConfiguration ServiceProviderConfiguration {get;} + public string RequestId {get;} } } \ No newline at end of file diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index c5317a51..3ddaf7d3 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -58,9 +58,9 @@ namespace Ocelot.DependencyInjection public class OcelotBuilder : IOcelotBuilder { private IServiceCollection _services; - private IConfigurationRoot _configurationRoot; + private IConfiguration _configurationRoot; - public OcelotBuilder(IServiceCollection services, IConfigurationRoot configurationRoot) + public OcelotBuilder(IServiceCollection services, IConfiguration configurationRoot) { _configurationRoot = configurationRoot; _services = services; @@ -280,9 +280,9 @@ namespace Ocelot.DependencyInjection public class OcelotAdministrationBuilder : IOcelotAdministrationBuilder { private IServiceCollection _services; - private IConfigurationRoot _configurationRoot; + private IConfiguration _configurationRoot; - public OcelotAdministrationBuilder(IServiceCollection services, IConfigurationRoot configurationRoot) + public OcelotAdministrationBuilder(IServiceCollection services, IConfiguration configurationRoot) { _configurationRoot = configurationRoot; _services = services; diff --git a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs index 5159109d..1a6bb96c 100644 --- a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs @@ -7,10 +7,16 @@ namespace Ocelot.DependencyInjection { public static class ServiceCollectionExtensions { - public static IOcelotBuilder AddOcelot(this IServiceCollection services, - IConfigurationRoot configurationRoot) + public static IOcelotBuilder AddOcelot(this IServiceCollection services) { - return new OcelotBuilder(services, configurationRoot); + var service = services.First(x => x.ServiceType == typeof(IConfiguration)); + var configuration = (IConfiguration)service.ImplementationInstance; + return new OcelotBuilder(services, configuration); + } + + public static IOcelotBuilder AddOcelot(this IServiceCollection services, IConfiguration configuration) + { + return new OcelotBuilder(services, configuration); } } } diff --git a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs index 15aa79cf..0e94b2b0 100644 --- a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs +++ b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs @@ -36,7 +36,6 @@ namespace Ocelot.DownstreamRouteFinder.Middleware { var upstreamUrlPath = context.Request.Path.ToString(); - //todo make this getting config its own middleware one day? var configuration = await _configProvider.Get(); if(configuration.IsError) diff --git a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs index 5a6139ba..96f2ca45 100644 --- a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs +++ b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs @@ -1,24 +1,34 @@ using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Ocelot.Configuration.Provider; +using Ocelot.Infrastructure.Extensions; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; +using Ocelot.Middleware; namespace Ocelot.Errors.Middleware { ///

/// Catches all unhandled exceptions thrown by middleware, logs and returns a 500 /// - public class ExceptionHandlerMiddleware + public class ExceptionHandlerMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IOcelotLogger _logger; private readonly IRequestScopedDataRepository _requestScopedDataRepository; + private readonly IOcelotConfigurationProvider _configProvider; + public ExceptionHandlerMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, - IRequestScopedDataRepository requestScopedDataRepository) + IRequestScopedDataRepository requestScopedDataRepository, + IOcelotConfigurationProvider configProvider) + :base(requestScopedDataRepository) { + _configProvider = configProvider; _next = next; _requestScopedDataRepository = requestScopedDataRepository; _logger = loggerFactory.CreateLogger(); @@ -28,26 +38,51 @@ namespace Ocelot.Errors.Middleware { try { - _logger.LogDebug("ocelot pipeline started"); + await TrySetGlobalRequestId(context); - _logger.LogDebug("calling next middleware"); + _logger.LogDebug("ocelot pipeline started"); await _next.Invoke(context); - _logger.LogDebug("succesfully called middleware"); } catch (Exception e) { _logger.LogDebug("error calling middleware"); var message = CreateMessage(context, e); + _logger.LogError(message, e); + SetInternalServerErrorOnResponse(context); } _logger.LogDebug("ocelot pipeline finished"); } + private async Task TrySetGlobalRequestId(HttpContext context) + { + //try and get the global request id and set it for logs... + //shoudl this basically be immutable per request...i guess it should! + //first thing is get config + var configuration = await _configProvider.Get(); + + //if error throw to catch below.. + if(configuration.IsError) + { + throw new Exception($"{MiddlewareName} setting pipeline errors. IOcelotConfigurationProvider returned {configuration.Errors.ToErrorString()}"); + } + + //else set the request id? + var key = configuration.Data.RequestId; + + StringValues upstreamRequestIds; + if (!string.IsNullOrEmpty(key) && context.Request.Headers.TryGetValue(key, out upstreamRequestIds)) + { + context.TraceIdentifier = upstreamRequestIds.First(); + _requestScopedDataRepository.Add("RequestId", context.TraceIdentifier); + } + } + private void SetInternalServerErrorOnResponse(HttpContext context) { context.Response.StatusCode = 500; diff --git a/src/Ocelot/Infrastructure/RequestData/HttpDataRepository.cs b/src/Ocelot/Infrastructure/RequestData/HttpDataRepository.cs index b589fb47..94f7e9c8 100644 --- a/src/Ocelot/Infrastructure/RequestData/HttpDataRepository.cs +++ b/src/Ocelot/Infrastructure/RequestData/HttpDataRepository.cs @@ -31,6 +31,22 @@ namespace Ocelot.Infrastructure.RequestData } } + public Response Update(string key, T value) + { + try + { + _httpContextAccessor.HttpContext.Items[key] = value; + return new OkResponse(); + } + catch (Exception exception) + { + return new ErrorResponse(new List + { + new CannotAddDataError(string.Format($"Unable to update data for key: {key}, exception: {exception.Message}")) + }); + } + } + public Response Get(string key) { object obj; diff --git a/src/Ocelot/Infrastructure/RequestData/IRequestScopedDataRepository.cs b/src/Ocelot/Infrastructure/RequestData/IRequestScopedDataRepository.cs index f707178c..a1421e11 100644 --- a/src/Ocelot/Infrastructure/RequestData/IRequestScopedDataRepository.cs +++ b/src/Ocelot/Infrastructure/RequestData/IRequestScopedDataRepository.cs @@ -5,6 +5,7 @@ namespace Ocelot.Infrastructure.RequestData public interface IRequestScopedDataRepository { Response Add(string key, T value); + Response Update(string key, T value); Response Get(string key); } } \ No newline at end of file diff --git a/src/Ocelot/Logging/AspDotNetLogger.cs b/src/Ocelot/Logging/AspDotNetLogger.cs index 82115382..8c336a18 100644 --- a/src/Ocelot/Logging/AspDotNetLogger.cs +++ b/src/Ocelot/Logging/AspDotNetLogger.cs @@ -19,34 +19,69 @@ namespace Ocelot.Logging } public void LogTrace(string message, params object[] args) - { - _logger.LogTrace(GetMessageWithOcelotRequestId(message), args); + { + var requestId = GetOcelotRequestId(); + var previousRequestId = GetOcelotPreviousRequestId(); + _logger.LogTrace("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message},", requestId, previousRequestId, message, args); } public void LogDebug(string message, params object[] args) - { - _logger.LogDebug(GetMessageWithOcelotRequestId(message), args); + { + var requestId = GetOcelotRequestId(); + var previousRequestId = GetOcelotPreviousRequestId(); + _logger.LogDebug("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message},", requestId, previousRequestId, message, args); } + + public void LogInformation(string message, params object[] args) + { + var requestId = GetOcelotRequestId(); + var previousRequestId = GetOcelotPreviousRequestId(); + _logger.LogInformation("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message},", requestId, previousRequestId, message, args); + } + public void LogError(string message, Exception exception) { - _logger.LogError(GetMessageWithOcelotRequestId(message), exception); + var requestId = GetOcelotRequestId(); + var previousRequestId = GetOcelotPreviousRequestId(); + _logger.LogError("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}, exception: {exception}", requestId, previousRequestId, message, exception); } public void LogError(string message, params object[] args) { - _logger.LogError(GetMessageWithOcelotRequestId(message), args); + var requestId = GetOcelotRequestId(); + var previousRequestId = GetOcelotPreviousRequestId(); + _logger.LogError("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}", requestId, previousRequestId, message, args); } - private string GetMessageWithOcelotRequestId(string message) + public void LogCritical(string message, Exception exception) + { + var requestId = GetOcelotRequestId(); + var previousRequestId = GetOcelotPreviousRequestId(); + _logger.LogError("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}", requestId, previousRequestId, message); + } + + private string GetOcelotRequestId() { var requestId = _scopedDataRepository.Get("RequestId"); if (requestId == null || requestId.IsError) { - return $"{message} : OcelotRequestId - not set"; + return $"no request id"; } - return $"{message} : OcelotRequestId - {requestId.Data}"; + return requestId.Data; + } + + private string GetOcelotPreviousRequestId() + { + var requestId = _scopedDataRepository.Get("PreviousRequestId"); + + if (requestId == null || requestId.IsError) + { + return $"no previous request id"; + } + + return requestId.Data; } } } \ No newline at end of file diff --git a/src/Ocelot/Logging/IOcelotLoggerFactory.cs b/src/Ocelot/Logging/IOcelotLoggerFactory.cs index 88bfdcd9..dc96c649 100644 --- a/src/Ocelot/Logging/IOcelotLoggerFactory.cs +++ b/src/Ocelot/Logging/IOcelotLoggerFactory.cs @@ -6,18 +6,20 @@ namespace Ocelot.Logging { IOcelotLogger CreateLogger(); } - /// - /// Thin wrapper around the DotNet core logging framework, used to allow the scopedDataRepository to be injected giving access to the Ocelot RequestId + /// + /// Thin wrapper around the DotNet core logging framework, used to allow the scopedDataRepository to be injected giving access to the Ocelot RequestId /// public interface IOcelotLogger { void LogTrace(string message, params object[] args); void LogDebug(string message, params object[] args); + void LogInformation(string message, params object[] args); void LogError(string message, Exception exception); void LogError(string message, params object[] args); + void LogCritical(string message, Exception exception); - /// - /// The name of the type the logger has been built for. + /// + /// The name of the type the logger has been built for. /// string Name { get; } } diff --git a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs index ab029950..466754b4 100644 --- a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs +++ b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs @@ -77,6 +77,7 @@ namespace Ocelot.Middleware ConfigureDiagnosticListener(builder); // This is registered to catch any global exceptions that are not handled + // It also sets the Request Id if anything is set globally builder.UseExceptionHandlerMiddleware(); // Allow the user to respond with absolutely anything they want. @@ -94,7 +95,9 @@ namespace Ocelot.Middleware // We check whether the request is ratelimit, and if there is no continue processing builder.UseRateLimiting(); - // Now we can look for the requestId + // This adds or updates the request id (initally we try and set this based on global config in the error handling middleware) + // If anything was set at global level and we have a different setting at re route level the global stuff will be overwritten + // This means you can get a scenario where you have a different request id from the first piece of middleware to the request id middleware. builder.UseRequestIdMiddleware(); // Allow pre authentication logic. The idea being people might want to run something custom before what is built in. @@ -313,15 +316,10 @@ namespace Ocelot.Middleware /// private static void ConfigureDiagnosticListener(IApplicationBuilder builder) { - var env = (IHostingEnvironment)builder.ApplicationServices.GetService(typeof(IHostingEnvironment)); - - //https://github.com/TomPallister/Ocelot/pull/87 not sure why only for dev envs and marc disapeered so just merging and maybe change one day? - if (!env.IsProduction()) - { - var listener = (OcelotDiagnosticListener)builder.ApplicationServices.GetService(typeof(OcelotDiagnosticListener)); - var diagnosticListener = (DiagnosticListener)builder.ApplicationServices.GetService(typeof(DiagnosticListener)); - diagnosticListener.SubscribeWithAdapter(listener); - } + var env = (IHostingEnvironment)builder.ApplicationServices.GetService(typeof(IHostingEnvironment)); + var listener = (OcelotDiagnosticListener)builder.ApplicationServices.GetService(typeof(OcelotDiagnosticListener)); + var diagnosticListener = (DiagnosticListener)builder.ApplicationServices.GetService(typeof(DiagnosticListener)); + diagnosticListener.SubscribeWithAdapter(listener); } private static void OnShutdown(IApplicationBuilder app) diff --git a/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs b/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs index a2813c25..802b419c 100644 --- a/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs +++ b/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs @@ -26,8 +26,6 @@ namespace Ocelot.Request.Middleware public async Task Invoke(HttpContext context) { - _logger.LogDebug("started calling request builder middleware"); - var downstreamRequest = await _requestMapper.Map(context.Request); if (downstreamRequest.IsError) { @@ -37,11 +35,7 @@ namespace Ocelot.Request.Middleware SetDownstreamRequest(downstreamRequest.Data); - _logger.LogDebug("calling next middleware"); - await _next.Invoke(context); - - _logger.LogDebug("succesfully called next middleware"); } } } \ No newline at end of file diff --git a/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs b/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs index d04e76e2..44d9163a 100644 --- a/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs +++ b/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs @@ -30,8 +30,6 @@ namespace Ocelot.Request.Middleware public async Task Invoke(HttpContext context) { - _logger.LogDebug("started calling request builder middleware"); - var qosProvider = _qosProviderHouse.Get(DownstreamRoute.ReRoute); if (qosProvider.IsError) @@ -62,11 +60,7 @@ namespace Ocelot.Request.Middleware SetUpstreamRequestForThisRequest(buildResult.Data); - _logger.LogDebug("calling next middleware"); - await _next.Invoke(context); - - _logger.LogDebug("succesfully called next middleware"); } } } \ No newline at end of file diff --git a/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs b/src/Ocelot/RequestId/Middleware/ReRouteRequestIdMiddleware.cs similarity index 71% rename from src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs rename to src/Ocelot/RequestId/Middleware/ReRouteRequestIdMiddleware.cs index 403e550d..10ec5f10 100644 --- a/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs +++ b/src/Ocelot/RequestId/Middleware/ReRouteRequestIdMiddleware.cs @@ -11,19 +11,19 @@ using System.Collections.Generic; namespace Ocelot.RequestId.Middleware { - public class RequestIdMiddleware : OcelotMiddleware + public class ReRouteRequestIdMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IOcelotLogger _logger; private readonly IRequestScopedDataRepository _requestScopedDataRepository; - public RequestIdMiddleware(RequestDelegate next, + public ReRouteRequestIdMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IRequestScopedDataRepository requestScopedDataRepository) : base(requestScopedDataRepository) { _next = next; - _logger = loggerFactory.CreateLogger(); + _logger = loggerFactory.CreateLogger(); _requestScopedDataRepository = requestScopedDataRepository; } @@ -41,7 +41,22 @@ namespace Ocelot.RequestId.Middleware StringValues upstreamRequestIds; if (context.Request.Headers.TryGetValue(key, out upstreamRequestIds)) { + //set the traceidentifier context.TraceIdentifier = upstreamRequestIds.First(); + + //check if we have previous id + var previousRequestId = _requestScopedDataRepository.Get("RequestId"); + if(!previousRequestId.IsError && !string.IsNullOrEmpty(previousRequestId.Data)) + { + //we have a previous request id lets store it and update request id + _requestScopedDataRepository.Add("PreviousRequestId", previousRequestId.Data); + _requestScopedDataRepository.Update("RequestId", context.TraceIdentifier); + } + else + { + //else just add request id + _requestScopedDataRepository.Add("RequestId", context.TraceIdentifier); + } } // set request ID on downstream request, if required diff --git a/src/Ocelot/RequestId/Middleware/RequestIdMiddlewareExtensions.cs b/src/Ocelot/RequestId/Middleware/RequestIdMiddlewareExtensions.cs index dc29afde..67233a86 100644 --- a/src/Ocelot/RequestId/Middleware/RequestIdMiddlewareExtensions.cs +++ b/src/Ocelot/RequestId/Middleware/RequestIdMiddlewareExtensions.cs @@ -6,7 +6,7 @@ namespace Ocelot.RequestId.Middleware { public static IApplicationBuilder UseRequestIdMiddleware(this IApplicationBuilder builder) { - return builder.UseMiddleware(); + return builder.UseMiddleware(); } } } \ No newline at end of file diff --git a/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs b/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs index f804569b..6cb1af88 100644 --- a/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs +++ b/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs @@ -26,8 +26,6 @@ namespace Ocelot.Requester.Middleware public async Task Invoke(HttpContext context) { - _logger.LogDebug("started calling requester middleware"); - var response = await _requester.GetResponse(Request); if (response.IsError) @@ -41,8 +39,6 @@ namespace Ocelot.Requester.Middleware _logger.LogDebug("setting http response message"); SetHttpResponseMessageThisRequest(response.Data); - - _logger.LogDebug("returning to calling middleware"); } } } \ No newline at end of file diff --git a/test/Ocelot.AcceptanceTests/AcceptanceTestsStartup.cs b/test/Ocelot.AcceptanceTests/AcceptanceTestsStartup.cs index bae6fb34..ade7a3fd 100644 --- a/test/Ocelot.AcceptanceTests/AcceptanceTestsStartup.cs +++ b/test/Ocelot.AcceptanceTests/AcceptanceTestsStartup.cs @@ -1,5 +1,4 @@ using System; -using CacheManager.Core; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -8,7 +7,6 @@ using Microsoft.Extensions.Logging; using Ocelot.DependencyInjection; using Ocelot.Middleware; using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; -using Ocelot.AcceptanceTests.Caching; namespace Ocelot.AcceptanceTests { @@ -26,7 +24,7 @@ namespace Ocelot.AcceptanceTests Configuration = builder.Build(); } - public IConfigurationRoot Configuration { get; } + public IConfiguration Configuration { get; } public virtual void ConfigureServices(IServiceCollection services) { @@ -35,48 +33,7 @@ namespace Ocelot.AcceptanceTests public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { - loggerFactory.AddConsole(Configuration.GetSection("Logging")); - app.UseOcelot().Wait(); } } - - public class Startup_WithCustomCacheHandle : AcceptanceTestsStartup - { - public Startup_WithCustomCacheHandle(IHostingEnvironment env) : base(env) { } - - public override void ConfigureServices(IServiceCollection services) - { - services.AddOcelot(Configuration) - .AddCacheManager((x) => - { - x.WithMicrosoftLogging(log => - { - log.AddConsole(LogLevel.Debug); - }) - .WithJsonSerializer() - .WithHandle(typeof(InMemoryJsonHandle<>)); - }); - } - } - - public class Startup_WithConsul_And_CustomCacheHandle : AcceptanceTestsStartup - { - public Startup_WithConsul_And_CustomCacheHandle(IHostingEnvironment env) : base(env) { } - - public override void ConfigureServices(IServiceCollection services) - { - services.AddOcelot(Configuration) - .AddCacheManager((x) => - { - x.WithMicrosoftLogging(log => - { - log.AddConsole(LogLevel.Debug); - }) - .WithJsonSerializer() - .WithHandle(typeof(InMemoryJsonHandle<>)); - }) - .AddStoreOcelotConfigurationInConsul(); - } - } } diff --git a/test/Ocelot.AcceptanceTests/ConsulStartup.cs b/test/Ocelot.AcceptanceTests/ConsulStartup.cs index b9ad603e..f63257a5 100644 --- a/test/Ocelot.AcceptanceTests/ConsulStartup.cs +++ b/test/Ocelot.AcceptanceTests/ConsulStartup.cs @@ -22,25 +22,18 @@ namespace Ocelot.AcceptanceTests .AddJsonFile("configuration.json") .AddEnvironmentVariables(); - Configuration = builder.Build(); + Config = builder.Build(); } - public IConfigurationRoot Configuration { get; } + public static IConfiguration Config { get; private set; } public void ConfigureServices(IServiceCollection services) { - Action settings = (x) => - { - x.WithDictionaryHandle(); - }; - - services.AddOcelot(Configuration).AddStoreOcelotConfigurationInConsul(); + services.AddOcelot(Config).AddStoreOcelotConfigurationInConsul(); } - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + public void Configure(IApplicationBuilder app) { - loggerFactory.AddConsole(Configuration.GetSection("Logging")); - app.UseOcelot().Wait(); } } diff --git a/test/Ocelot.AcceptanceTests/StartupWithConsulAndCustomCacheHandle.cs b/test/Ocelot.AcceptanceTests/StartupWithConsulAndCustomCacheHandle.cs new file mode 100644 index 00000000..3b2f97d7 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/StartupWithConsulAndCustomCacheHandle.cs @@ -0,0 +1,29 @@ +using CacheManager.Core; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Ocelot.DependencyInjection; +using Ocelot.AcceptanceTests.Caching; + +namespace Ocelot.AcceptanceTests +{ + public class StartupWithConsulAndCustomCacheHandle : AcceptanceTestsStartup + { + public StartupWithConsulAndCustomCacheHandle(IHostingEnvironment env) : base(env) { } + + public override void ConfigureServices(IServiceCollection services) + { + services.AddOcelot(Configuration) + .AddCacheManager((x) => + { + x.WithMicrosoftLogging(log => + { + log.AddConsole(LogLevel.Debug); + }) + .WithJsonSerializer() + .WithHandle(typeof(InMemoryJsonHandle<>)); + }) + .AddStoreOcelotConfigurationInConsul(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/StartupWithCustomCacheHandle.cs b/test/Ocelot.AcceptanceTests/StartupWithCustomCacheHandle.cs new file mode 100644 index 00000000..817ddc7f --- /dev/null +++ b/test/Ocelot.AcceptanceTests/StartupWithCustomCacheHandle.cs @@ -0,0 +1,28 @@ +using CacheManager.Core; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Ocelot.DependencyInjection; +using Ocelot.AcceptanceTests.Caching; + +namespace Ocelot.AcceptanceTests +{ + public class StartupWithCustomCacheHandle : AcceptanceTestsStartup + { + public StartupWithCustomCacheHandle(IHostingEnvironment env) : base(env) { } + + public override void ConfigureServices(IServiceCollection services) + { + services.AddOcelot(Configuration) + .AddCacheManager((x) => + { + x.WithMicrosoftLogging(log => + { + log.AddConsole(LogLevel.Debug); + }) + .WithJsonSerializer() + .WithHandle(typeof(InMemoryJsonHandle<>)); + }); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 71d3fb16..9cdb8e4e 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -118,7 +118,7 @@ namespace Ocelot.AcceptanceTests }); _ocelotServer = new TestServer(_webHostBuilder - .UseStartup()); + .UseStartup()); _ocelotClient = _ocelotServer.CreateClient(); } @@ -148,7 +148,7 @@ namespace Ocelot.AcceptanceTests }); _ocelotServer = new TestServer(_webHostBuilder - .UseStartup()); + .UseStartup()); _ocelotClient = _ocelotServer.CreateClient(); } diff --git a/test/Ocelot.IntegrationTests/IntegrationTestsStartup.cs b/test/Ocelot.IntegrationTests/IntegrationTestsStartup.cs index 097c1b5c..91923d6e 100644 --- a/test/Ocelot.IntegrationTests/IntegrationTestsStartup.cs +++ b/test/Ocelot.IntegrationTests/IntegrationTestsStartup.cs @@ -25,7 +25,7 @@ namespace Ocelot.IntegrationTests Configuration = builder.Build(); } - public IConfigurationRoot Configuration { get; } + public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { @@ -45,8 +45,6 @@ namespace Ocelot.IntegrationTests public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { - loggerFactory.AddConsole(Configuration.GetSection("Logging")); - app.UseOcelot().Wait(); } } diff --git a/test/Ocelot.IntegrationTests/RaftStartup.cs b/test/Ocelot.IntegrationTests/RaftStartup.cs index 25015358..2b5d37c6 100644 --- a/test/Ocelot.IntegrationTests/RaftStartup.cs +++ b/test/Ocelot.IntegrationTests/RaftStartup.cs @@ -34,7 +34,7 @@ namespace Ocelot.IntegrationTests Configuration = builder.Build(); } - public IConfigurationRoot Configuration { get; } + public IConfiguration Configuration { get; } public virtual void ConfigureServices(IServiceCollection services) { @@ -46,9 +46,6 @@ namespace Ocelot.IntegrationTests public virtual void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { - - //this is from Ocelot...so we need to move stuff below into it... - loggerFactory.AddConsole(Configuration.GetSection("Logging")); app.UseOcelot().Wait(); } } diff --git a/test/Ocelot.ManualTest/ManualTestStartup.cs b/test/Ocelot.ManualTest/ManualTestStartup.cs index ac48f67e..a5527105 100644 --- a/test/Ocelot.ManualTest/ManualTestStartup.cs +++ b/test/Ocelot.ManualTest/ManualTestStartup.cs @@ -13,29 +13,11 @@ namespace Ocelot.ManualTest { public class ManualTestStartup { - public ManualTestStartup(IHostingEnvironment env) - { - var builder = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) - .AddJsonFile("configuration.json") - .AddEnvironmentVariables(); - - Configuration = builder.Build(); - } - - public IConfigurationRoot Configuration { get; } - public void ConfigureServices(IServiceCollection services) { Action settings = (x) => { - x.WithMicrosoftLogging(log => - { - log.AddConsole(LogLevel.Debug); - }) - .WithDictionaryHandle(); + x.WithDictionaryHandle(); }; services.AddAuthentication() @@ -45,14 +27,13 @@ namespace Ocelot.ManualTest x.Audience = "test"; }); - services.AddOcelot(Configuration) + services.AddOcelot() + .AddCacheManager(settings) .AddAdministration("/administration", "secret"); } - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + public void Configure(IApplicationBuilder app) { - loggerFactory.AddConsole(Configuration.GetSection("Logging")); - app.UseOcelot().Wait(); } } diff --git a/test/Ocelot.ManualTest/Program.cs b/test/Ocelot.ManualTest/Program.cs index 9fe2b4f0..5c7f482d 100644 --- a/test/Ocelot.ManualTest/Program.cs +++ b/test/Ocelot.ManualTest/Program.cs @@ -1,6 +1,8 @@ using System.IO; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Configuration; namespace Ocelot.ManualTest { @@ -14,6 +16,20 @@ namespace Ocelot.ManualTest }); builder.UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + config.AddJsonFile("configuration.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureLogging((hostingContext, logging) => + { + logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + logging.AddConsole(); + }) .UseIISIntegration() .UseStartup(); var host = builder.Build(); diff --git a/test/Ocelot.ManualTest/appsettings.json b/test/Ocelot.ManualTest/appsettings.json index 7327a7b9..930ca4c1 100644 --- a/test/Ocelot.ManualTest/appsettings.json +++ b/test/Ocelot.ManualTest/appsettings.json @@ -1,10 +1,10 @@ { "Logging": { - "IncludeScopes": true, + "IncludeScopes": false, "LogLevel": { - "Default": "Trace", - "System": "Information", - "Microsoft": "Information" + "Default": "Debug", + "System": "Error", + "Microsoft": "Error" } } } diff --git a/test/Ocelot.ManualTest/configuration.json b/test/Ocelot.ManualTest/configuration.json index 0adac11d..fcaf49f5 100644 --- a/test/Ocelot.ManualTest/configuration.json +++ b/test/Ocelot.ManualTest/configuration.json @@ -62,6 +62,7 @@ "DownstreamPort": 80, "UpstreamPathTemplate": "/posts/{postId}", "UpstreamHttpMethod": [ "Get" ], + "RequestIdKey": "ReRouteRequestId", "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs index 14c71b49..66c6582a 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs @@ -38,7 +38,7 @@ namespace Ocelot.UnitTests.Configuration { var fileConfig = new FileConfiguration(); var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - var config = new OcelotConfiguration(new List(), string.Empty, serviceProviderConfig); + var config = new OcelotConfiguration(new List(), string.Empty, serviceProviderConfig, "asdf"); this.Given(x => GivenTheFollowingConfiguration(fileConfig)) .And(x => GivenTheRepoReturns(new OkResponse())) diff --git a/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs index fa853c7f..9a9fc69e 100644 --- a/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs @@ -94,6 +94,8 @@ namespace Ocelot.UnitTests.Configuration public string AdministrationPath {get;} public ServiceProviderConfiguration ServiceProviderConfiguration => throw new NotImplementedException(); + + public string RequestId {get;} } } } diff --git a/test/Ocelot.UnitTests/Configuration/OcelotConfigurationProviderTests.cs b/test/Ocelot.UnitTests/Configuration/OcelotConfigurationProviderTests.cs index b1a6f314..9a8579ef 100644 --- a/test/Ocelot.UnitTests/Configuration/OcelotConfigurationProviderTests.cs +++ b/test/Ocelot.UnitTests/Configuration/OcelotConfigurationProviderTests.cs @@ -30,9 +30,9 @@ namespace Ocelot.UnitTests.Configuration { var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - this.Given(x => x.GivenTheRepoReturns(new OkResponse(new OcelotConfiguration(new List(), string.Empty, serviceProviderConfig)))) + this.Given(x => x.GivenTheRepoReturns(new OkResponse(new OcelotConfiguration(new List(), string.Empty, serviceProviderConfig, "")))) .When(x => x.WhenIGetTheConfig()) - .Then(x => x.TheFollowingIsReturned(new OkResponse(new OcelotConfiguration(new List(), string.Empty, serviceProviderConfig)))) + .Then(x => x.TheFollowingIsReturned(new OkResponse(new OcelotConfiguration(new List(), string.Empty, serviceProviderConfig, "")))) .BDDfy(); } diff --git a/test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs index f52cb808..3c686d68 100644 --- a/test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs @@ -51,14 +51,15 @@ namespace Ocelot.UnitTests.Configuration } [Fact] - public void should_use_global_cofiguration_over_re_route_specific() + public void should_use_re_route_over_global_specific() { var reRoute = new FileReRoute { RequestIdKey = "cheese" - }; var globalConfig = new FileGlobalConfiguration + }; + var globalConfig = new FileGlobalConfiguration { - RequestIdKey = "cheese" + RequestIdKey = "test" }; this.Given(x => x.GivenTheFollowingReRoute(reRoute)) diff --git a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs index 818a9e1f..52cbb737 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs @@ -24,7 +24,7 @@ namespace Ocelot.UnitTests.DependencyInjection { private IServiceCollection _services; private IServiceProvider _serviceProvider; - private IConfigurationRoot _configRoot; + private IConfiguration _configRoot; private IOcelotBuilder _ocelotBuilder; private int _maxRetries; @@ -35,6 +35,7 @@ namespace Ocelot.UnitTests.DependencyInjection _services = new ServiceCollection(); _services.AddSingleton(builder); _services.AddSingleton(); + _services.AddSingleton(_configRoot); _maxRetries = 100; } private Exception _ex; @@ -95,6 +96,14 @@ namespace Ocelot.UnitTests.DependencyInjection .BDDfy(); } + [Fact] + public void should_set_up_without_passing_in_config() + { + this.When(x => WhenISetUpOcelotServicesWithoutConfig()) + .Then(x => ThenAnExceptionIsntThrown()) + .BDDfy(); + } + private void ThenTheCorrectAdminPathIsRegitered() { _serviceProvider = _services.BuildServiceProvider(); @@ -156,6 +165,19 @@ namespace Ocelot.UnitTests.DependencyInjection _ex = e; } } + + private void WhenISetUpOcelotServicesWithoutConfig() + { + try + { + _ocelotBuilder = _services.AddOcelot(); + } + catch (Exception e) + { + _ex = e; + } + } + private void WhenISetUpCacheManager() { try diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs index 5781996b..31cafa28 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs @@ -34,7 +34,7 @@ [Fact] public void should_call_scoped_data_repository_correctly() { - var config = new OcelotConfiguration(null, null, new ServiceProviderConfigurationBuilder().Build()); + var config = new OcelotConfiguration(null, null, new ServiceProviderConfigurationBuilder().Build(), ""); this.Given(x => x.GivenTheDownStreamRouteFinderReturns( new DownstreamRoute( diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs index 1593b3f3..81b7be70 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs @@ -422,7 +422,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder private void GivenTheConfigurationIs(List reRoutesConfig, string adminPath, ServiceProviderConfiguration serviceProviderConfig) { _reRoutesConfig = reRoutesConfig; - _config = new OcelotConfiguration(_reRoutesConfig, adminPath, serviceProviderConfig); + _config = new OcelotConfiguration(_reRoutesConfig, adminPath, serviceProviderConfig, ""); } private void GivenThereIsAnUpstreamUrlPath(string upstreamUrlPath) diff --git a/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs b/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs index 5bf848cd..da854ff7 100644 --- a/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs @@ -11,39 +11,96 @@ namespace Ocelot.UnitTests.Errors using TestStack.BDDfy; using Xunit; using Microsoft.AspNetCore.Http; + using Ocelot.Configuration.Provider; + using Moq; + using Ocelot.Configuration; + using Rafty.Concensus; public class ExceptionHandlerMiddlewareTests : ServerHostedMiddlewareTest { bool _shouldThrowAnException = false; + private Mock _provider; public ExceptionHandlerMiddlewareTests() { + _provider = new Mock(); GivenTheTestServerIsConfigured(); } [Fact] public void NoDownstreamException() { + var config = new OcelotConfiguration(null, null, null, null); + this.Given(_ => GivenAnExceptionWillNotBeThrownDownstream()) + .And(_ => GivenTheConfigurationIs(config)) .When(_ => WhenICallTheMiddleware()) .Then(_ => ThenTheResponseIsOk()) + .And(_ => TheRequestIdIsNotSet()) .BDDfy(); } + private void TheRequestIdIsNotSet() + { + ScopedRepository.Verify(x => x.Add(It.IsAny(), It.IsAny()), Times.Never); + } + [Fact] public void DownstreamException() { + var config = new OcelotConfiguration(null, null, null, null); + this.Given(_ => GivenAnExceptionWillBeThrownDownstream()) + .And(_ => GivenTheConfigurationIs(config)) .When(_ => WhenICallTheMiddleware()) .Then(_ => ThenTheResponseIsError()) .BDDfy(); } + [Fact] + public void ShouldSetRequestId() + { + var config = new OcelotConfiguration(null, null, null, "requestidkey"); + + this.Given(_ => GivenAnExceptionWillNotBeThrownDownstream()) + .And(_ => GivenTheConfigurationIs(config)) + .When(_ => WhenICallTheMiddlewareWithTheRequestIdKey("requestidkey", "1234")) + .Then(_ => ThenTheResponseIsOk()) + .And(_ => TheRequestIdIsSet("RequestId", "1234")) + .BDDfy(); + } + + [Fact] + public void ShouldNotSetRequestId() + { + var config = new OcelotConfiguration(null, null, null, null); + + this.Given(_ => GivenAnExceptionWillNotBeThrownDownstream()) + .And(_ => GivenTheConfigurationIs(config)) + .When(_ => WhenICallTheMiddlewareWithTheRequestIdKey("requestidkey", "1234")) + .Then(_ => ThenTheResponseIsOk()) + .And(_ => TheRequestIdIsNotSet()) + .BDDfy(); + } + + private void TheRequestIdIsSet(string key, string value) + { + ScopedRepository.Verify(x => x.Add(key, value), Times.Once); + } + + private void GivenTheConfigurationIs(IOcelotConfiguration config) + { + var response = new Ocelot.Responses.OkResponse(config); + _provider + .Setup(x => x.Get()).ReturnsAsync(response); + } + protected override void GivenTheTestServerServicesAreConfigured(IServiceCollection services) { services.AddSingleton(); services.AddLogging(); services.AddSingleton(ScopedRepository.Object); + services.AddSingleton(_provider.Object); } protected override void GivenTheTestServerPipelineIsConfigured(IApplicationBuilder app) diff --git a/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs b/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs index 0d7c9201..df5068d3 100644 --- a/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs @@ -25,7 +25,7 @@ namespace Ocelot.UnitTests.Infrastructure //TODO - Additional tests -> HttpContent null. This should never happen [Fact] - public void Get_returns_correct_key_from_http_context() + public void get_returns_correct_key_from_http_context() { this.Given(x => x.GivenAHttpContextContaining("key", "string")) @@ -35,7 +35,7 @@ namespace Ocelot.UnitTests.Infrastructure } [Fact] - public void Get_returns_error_response_if_the_key_is_not_found() //Therefore does not return null + public void get_returns_error_response_if_the_key_is_not_found() //Therefore does not return null { this.Given(x => x.GivenAHttpContextContaining("key", "string")) .When(x => x.GetIsCalledWithKey("keyDoesNotExist")) @@ -43,6 +43,21 @@ namespace Ocelot.UnitTests.Infrastructure .BDDfy(); } + [Fact] + public void should_update() + { + this.Given(x => x.GivenAHttpContextContaining("key", "string")) + .And(x => x.UpdateIsCalledWith("key", "new string")) + .When(x => x.GetIsCalledWithKey("key")) + .Then(x => x.ThenTheResultIsAnOkResponse("new string")) + .BDDfy(); + } + + private void UpdateIsCalledWith(string key, string value) + { + _httpDataRepository.Update(key, value); + } + private void GivenAHttpContextContaining(string key, object o) { _httpContext.Items.Add(key, o); diff --git a/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs b/test/Ocelot.UnitTests/RequestId/ReRouteRequestIdMiddlewareTests.cs similarity index 59% rename from test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs rename to test/Ocelot.UnitTests/RequestId/ReRouteRequestIdMiddlewareTests.cs index 4d684084..0acfc862 100644 --- a/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/RequestId/ReRouteRequestIdMiddlewareTests.cs @@ -19,14 +19,14 @@ using TestStack.BDDfy; using Xunit; - public class RequestIdMiddlewareTests : ServerHostedMiddlewareTest + public class ReRouteRequestIdMiddlewareTests : ServerHostedMiddlewareTest { private readonly HttpRequestMessage _downstreamRequest; private Response _downstreamRoute; private string _value; private string _key; - public RequestIdMiddlewareTests() + public ReRouteRequestIdMiddlewareTests() { _downstreamRequest = new HttpRequestMessage(); @@ -50,6 +50,7 @@ var requestId = Guid.NewGuid().ToString(); this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => GivenThereIsNoGlobalRequestId()) .And(x => x.GivenTheRequestIdIsAddedToTheRequest("LSRequestId", requestId)) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenTheTraceIdIs(requestId)) @@ -67,11 +68,74 @@ .Build()); this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => GivenThereIsNoGlobalRequestId()) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenTheTraceIdIsAnything()) .BDDfy(); } + [Fact] + public void should_add_request_id_scoped_repo_for_logging_later() + { + var downstreamRoute = new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithRequestIdKey("LSRequestId") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()); + + var requestId = Guid.NewGuid().ToString(); + + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => GivenThereIsNoGlobalRequestId()) + .And(x => x.GivenTheRequestIdIsAddedToTheRequest("LSRequestId", requestId)) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheTraceIdIs(requestId)) + .And(x => ThenTheRequestIdIsSaved()) + .BDDfy(); + } + + [Fact] + public void should_update_request_id_scoped_repo_for_logging_later() + { + var downstreamRoute = new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithRequestIdKey("LSRequestId") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()); + + var requestId = Guid.NewGuid().ToString(); + + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => GivenTheRequestIdWasSetGlobally()) + .And(x => x.GivenTheRequestIdIsAddedToTheRequest("LSRequestId", requestId)) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheTraceIdIs(requestId)) + .And(x => ThenTheRequestIdIsUpdated()) + .BDDfy(); + } + + private void GivenThereIsNoGlobalRequestId() + { + ScopedRepository.Setup(x => x.Get("RequestId")).Returns(new OkResponse(null)); + } + + private void GivenTheRequestIdWasSetGlobally() + { + ScopedRepository.Setup(x => x.Get("RequestId")).Returns(new OkResponse("alreadyset")); + } + + private void ThenTheRequestIdIsSaved() + { + ScopedRepository.Verify(x => x.Add("RequestId", _value), Times.Once); + } + + private void ThenTheRequestIdIsUpdated() + { + ScopedRepository.Verify(x => x.Update("RequestId", _value), Times.Once); + } + protected override void GivenTheTestServerServicesAreConfigured(IServiceCollection services) { services.AddSingleton(); diff --git a/test/Ocelot.UnitTests/ServerHostedMiddlewareTest.cs b/test/Ocelot.UnitTests/ServerHostedMiddlewareTest.cs index 29a012b9..a5b8016a 100644 --- a/test/Ocelot.UnitTests/ServerHostedMiddlewareTest.cs +++ b/test/Ocelot.UnitTests/ServerHostedMiddlewareTest.cs @@ -53,6 +53,12 @@ ResponseMessage = Client.GetAsync(Url).Result; } + protected void WhenICallTheMiddlewareWithTheRequestIdKey(string requestIdKey, string value) + { + Client.DefaultRequestHeaders.Add(requestIdKey, value); + ResponseMessage = Client.GetAsync(Url).Result; + } + public void Dispose() { Client.Dispose();