From 1b3e2db75ff5d30c57792b925e297f385db33729 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 18 Nov 2020 22:44:57 +0100 Subject: [PATCH 01/94] TechDocs: Add basic and production deployment architecture diagrams --- .../techdocs/architecture-basic.drawio.svg | 462 ++++++++++++++++++ .../architecture-recommended.drawio.svg | 391 +++++++++++++++ docs/assets/techdocs/techdocs_big_picture.png | Bin 113762 -> 0 bytes docs/features/techdocs/architecture.md | 39 +- 4 files changed, 891 insertions(+), 1 deletion(-) create mode 100644 docs/assets/techdocs/architecture-basic.drawio.svg create mode 100644 docs/assets/techdocs/architecture-recommended.drawio.svg delete mode 100644 docs/assets/techdocs/techdocs_big_picture.png diff --git a/docs/assets/techdocs/architecture-basic.drawio.svg b/docs/assets/techdocs/architecture-basic.drawio.svg new file mode 100644 index 0000000000..8e3be76f8b --- /dev/null +++ b/docs/assets/techdocs/architecture-basic.drawio.svg @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + +
+
+
+ Repo 1 +
+ (with docs) +
+
+
+
+ + Repo 1... + +
+
+ + + + +
+
+
+ Repo 2 +
+ (with docs) +
+
+
+
+ + Repo 2... + +
+
+ + + + +
+
+
+ TechDocs plugin +
+
+
+
+ + TechDocs plugin + +
+
+ + + + + +
+
+
+ Request TechDocs site +
+
+
+
+ + Request TechDocs site + +
+
+ + + + + + + + + + + + + + + + + + +
+
+
+ Source code hosting +
+
+
+
+ + Source code hosting + +
+
+ + + + +
+
+
+ TechDocs Backend Plugin +
+
+
+
+ + TechDocs Backend Plugin + +
+
+ + + + +
+
+
+ (Stages) +
+
+
+
+ + (Stages) + +
+
+ + + + + + +
+
+
+ Prepare +
+
+
+
+ + Prepare + +
+
+ + + + + + +
+
+
+ Generate +
+
+
+
+ + Generate + +
+
+ + + + +
+
+
+ Publish +
+
+
+
+ + Publish + +
+
+ + + + + +
+
+
+ Local Filesystem +
+
+
+
+ + Local File... + +
+
+ + + + +
+
+
+ Fetch markdown files +
+
+
+
+ + Fetch mark... + +
+
+ + + + +
+
+
+ Build doc files using mkdocs +
+
+
+
+ + Build doc... + +
+
+ + + + +
+
+
+ Store generated static content +
+
+
+
+ + Store gene... + +
+
+ + + + +
+
+
+ TechDocs Basic (out-of-the-box) architecture +
+
+
+
+ + TechDocs Basic (out-of-the-box) architect... + +
+
+ + + + + + + + +
+
+
+ + Route handler + +
+
+
+
+ + Route hand... + +
+
+ + + + +
+
+
+ or +
+
+
+
+ + or + +
+
+ + + + +
+
+
+ Trigger new build if docs D.N.E. +
+ or are outdated +
+
+
+
+ + Trigger new build... + +
+
+ + + + +
+
+
+ Fetch files to render +
+
+
+
+ + Fetch files... + +
+
+ + + + + + + + + + + +
+
+
+ + GCP +
+ Bucket +
+
+
+
+
+ + GCP... + +
+
+ + + + +
+
+
+ Azure storage +
+
+
+
+ + Azure st... + +
+
+ + + + + +
+
+
+ Storage solutions +
+
+
+
+ + Storage solutions + +
+
+ + + + + + + + +
+
+
+ + Amazon S3 + +
+
+
+
+ + Amazon S3 + +
+
+ +
+ + + + + Viewer does not support full SVG 1.1 + + + +
diff --git a/docs/assets/techdocs/architecture-recommended.drawio.svg b/docs/assets/techdocs/architecture-recommended.drawio.svg new file mode 100644 index 0000000000..e3af4b6b5f --- /dev/null +++ b/docs/assets/techdocs/architecture-recommended.drawio.svg @@ -0,0 +1,391 @@ + + + + + + + + + + + + + + +
+
+
+ Repo 1 +
+ (with docs) +
+
+
+
+ + Repo 1... + +
+
+ + + + +
+
+
+ Repo 2 +
+ (with docs) +
+
+
+
+ + Repo 2... + +
+
+ + + + + + + + +
+
+
+ CI/CD System +
+
+
+
+ + CI/CD System + +
+
+ + + + + + +
+
+
+ techdocs-cli +
+
+
+
+ + techdocs-cli + +
+
+ + + + +
+
+
+ Cloud Storage +
+
+
+
+ + Cloud Storage + +
+
+ + + + + + +
+
+
+ + Amazon S3 + +
+
+
+
+ + Amazon S3 + +
+
+ + + + + + + + + +
+
+
+ + GCP +
+ Bucket +
+
+
+
+
+ + GCP... + +
+
+ + + + +
+
+
+ Azure storage +
+
+
+
+ + Azure st... + +
+
+ + + + +
+
+
+ New commit +
+
+
+
+ + New commit + +
+
+ + + + +
+
+
+ New commit +
+
+
+
+ + New commit + +
+
+ + + + +
+
+
+ Build docs +
+ & +
+ Store generated static content +
+
+
+
+ + Build docs... + +
+
+ + + + + + + +
+
+
+ TechDocs plugin +
+
+
+
+ + TechDocs plugin + +
+
+ + + + +
+
+
+ TechDocs Backend plugin +
+
+
+
+ + TechDocs Backend plu... + +
+
+ + + + + + +
+
+
+ Request TechDocs site +
+
+
+
+ + Request TechDocs site + +
+
+ + + + +
+
+
+ Fetch files to render +
+
+
+
+ + Fetch files to render + +
+
+ + + + + + + + + + + + + + + +
+
+
+ Source code hosting +
+
+
+
+ + Source code hosting + +
+
+ + + + + +
+
+
+ Caching +
+ (Optional) +
+
+
+
+ + Caching... + +
+
+ + + + + + +
+
+
+ Storage solutions +
+
+
+
+ + Storage solutions + +
+
+ + + + +
+
+
+ TechDocs Recommended deployment architecture +
+
+
+
+ + TechDocs Recommended deployment architecture + +
+
+
+ + + + + Viewer does not support full SVG 1.1 + + + +
diff --git a/docs/assets/techdocs/techdocs_big_picture.png b/docs/assets/techdocs/techdocs_big_picture.png deleted file mode 100644 index 8b8f7a2338100d49fff40371797eda704fdf4a51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 113762 zcmeFYcU03^7dFbwC|F@cMQTuKq5-5!w;(MbT|x&TbVNe$4yX*$qy<3)>4X~T9kCEP zQbI4o&_j~|p@hIaabCfB*ShPjyS{t>_zHpw{Bq7d`#k&E&)(-GMB{Ik7L?E~8wBIG=L7VqJnWl+#QbM>Zo~$xGAIqNsx> zgRd|%DGM@ko;|_ymzfA=k=?Y-*;Q`)-PEUrt7B_`ZH2{KCQt6KA?dr>#9R zF!Mhj$j^V%|M))@RrAF_VD^7Lo+e#^Q@r%gM>Es^`PKieH}snu2~FWF=Y>79*FQIZ z`_`SVI5!}LoN5XmPfku(j3f|$`QgV5WBNvbtp8GlOBDC=GF9 z`Y|@uLYzEi$#dpnix@h5NnPWO1!m7#Ej)gG7eD5)dY1Z93-(K!=#G_s!jzBS)UKOf z$#7y~J{!@ap6n>#gm6>~mr2i6j|z*5I#+LhIB3SXMxiT_jYv%dY;_=G0i7K-$J|Kx zgFwhu(eL*pG#Hq2j*C_}>Ki@gS~!?|~T@#49Aq>;y@nF6Q3KY#u#P~m8A z@2KXQ(iV%><2($3r}sn65?;ld@eUgpUTVorH7N|K-?vL1U32a?laP^-5foheB0nj$ z@|8#5inj4EsGfosZBZT=J9nnK3u?qzYZNoCp=A6rVz06&JKhW(U{;JWIEnh>?kf?jg7BqcwF45Md!8>1I3wVJURY&w6eQ@I5&C! zEr9@#P*;L03keCy%1+*W;8nH%lDD=Svbu;ayytYyl2t+R=Cw~`n;BMndppQPsjbt^ zYvPd`+pbcPjaqU@GMD$gD_-4`larPc4@r&!kqTGY8qIpO)yKfb<7a@&X1Kh4yjcbG z(zxu5BRcktb{4e_wbL73?swaHMfDmm)&S(Oir-=m z$+4eVgSt8v6nM`FJ>cvv?Ue6|Zj0_?S)NJEaoh)kgthmtwcfzPl>P431FQkumB{+_ zZ+#)J@6@^rXptY~RxE*d8QgFY+5|ICH{C+ zgPlHIyoLrQuo}AG*y)cMWjVu~62JlPuZBdkUhFsop`{WrFf&;dfFVNR>iP5M!ANWs z{jue9DdNdF0%g|)01Sp%PHn!pt3VP=Cbb#Ddfrw{?6*0Nc;8D+k5V$U&)ZLnxKWw) zJ9P81b^eRo35-VJZKrD@f`aC9jHIOsTf>Wa1?Ytn)ClM9>&xQ_3Qzk4#(tZbQRF!1 zrO_ZXp*&ljj!zr~VQ=?ElL>6aE~_k7JrITw@h-1z129gHYL2wGTdI>lcg^Kf2oj$1 z)V}sOzvfp@L-uO|@mpW^Cv~*N0(Sbe>3m5el&`HgRBq9@-a~G>eYnqp>v0$f*Q>5^ zVAHbJX4*ckG5<%xB5|u?>>>HQyRKQ<-dRi+tQ@f_*w2`8fl{|Qz8Y5XT{v2BvkS$E0Oy9&YZH-oS+Sm3iZAY$@Ye z1&CZ4rLrb8`9h!e3ZSmlm^pvx5gqA^FW6F6=%Jf32R&ll{jxIR=&tN~ouW}@ZT2RE zMq;E&+BMHM>s8`NCe7lD;NaETsR(&<&wfZkp&9z1=-ASw{+VKyfDrlui8DV8zjipuwS*{dv#T9Fa_9;&WOS0Q=k zoA>C*$Puyhx`oD`H8y(G9t}v=gWr10OxFR zTE0{rRMMX)8F(i?AuA;9-9GxkNC$XTc|(KmmA*=zS(`XX&k`^=Wd;{7nf@FKWE-|i-fuTO`|>L#Ix#xfujkGTI|=WA zP$RZwCll(~BM0OK%)M>bi#hWlx6baqtWkdJpA|Hd@06EbdkV$Y_%6QTjW6~vB5mWt z&CrrDB(_s*=)R4&)7si%L7C303LFAn?SUZPdJM>tFX1*d2tz& z^KKs?@z37&|2+XR?Z_H^ne90d^}x((<`t*BS^=WEzZO8cKm?>j`vj_YJaI96aiBrl zWW0a4_3Y8Ea06K4S$s?Kp_;>*BYpjSIW(Kk#m~=Fn>(hx9*35khl0Gh+ul$#?pNWL zm2Q$uA9sZe-MDen&%Vr21rul=Qyy+)zMvX{jQ2OM>ndDFs%>xx%F^*SYOO|57^*IO ztkOB1BGKX;Vs?G4uED*%3^&(>3fJ>Hwo#CUjrzsB-?JH@IK{ldgN)W=#oq-O=HHBAya{jE+)oc%L>k` z&bVj+oCD30Q5Ys0E{g85ei4|tRt`cb#yKkdl)bc`Q&X9!XvB?OJZbgp^9_Nsbaaa; zFZ%*#fO|Kq3W%-+DR;Fhq$ow(_m6MaV2#LORC+GE_dH10fP>116PfF9L+O7;En4OR z3Q+?rvgYZe9=ZP+WQ@qEKM2TpQDH#qI!w{`atqKqGv$ty94#-4y=W9192}mlC%2&i zUNpYGWL_+OLCEAi2jkKS>P>B7E0<~*K5=kD1;{%;`y{a@kie|`yssvlMm5#JHtDkW z>nXA~BIT@ZEY8Q(R6n8q582b&2Zq`}-Ol13@WLNV?87mymzFjY8q?g}8kfLhLecTJ;SDPwhSMlhBV;cb;5J z;Wch?`!{Lwy^d|LRva$-g0^M&n=H{c^(r54?60(3$SJqjx};Q zeuLKcfBP|Q2JB(b%*KYR;>~%u2Ov)2S;b)rlVj&(HB43*p9wKTfkG#eT3dkzT(IH% zn|xPIRdsK>zsjQ|`8jzZf@U3v2G)TKZdJ1@+IQJHywxDr1U#goEnlWXh4WVNI|+sE zK=^RF4Ke|d*4qmbT7d7A0=A-Tw~=N{0O@osL)}uu3SR9?m|50d{nIZif6FX^pm_R+ zg=Q=HNwxTI-<|+Xk}o~fKb)wuE(J8+ocpn$b?3haK+FP-gFJ;~P(Gdtgdxz{GIin? z2n5%u>KWa@#MiIqPf&xhsfElRV)vrZ0@oH?p&MWtUEK`y)T`Tzh&iB6n05yj+4*)t zm2(eSD#6P6q|Q#aD@{0(ZGQe_JY$k?FNhaSRUeuqXWf~vvaNIU&u08V=bh-$D)PeKk~ z&cF!F=>|9vzkokMEfsjD|LLJE@DC@BiV-U03$sH#OLf`$f5TsaTDB(gjG#L^6S6^t z%Mj1l^jFbQ#Z7+&P7nR5dZV;S8l#VVMroq_k3OJ$53_V@EkcmqF~)}Oz{@pb>a!I1 z=kwH*P<8s;?s(O=*`ZLsRii-GvktZ=lV=Y+-SjLxNOW5w0gDS*wu+tp z-pGl84R-v22m5XbeO=CQ>KSr~Nx9Cw1Nx&Xn2HysjuCle3D))~AOu);@1dQV1t#`y zMiQ*n=R^BTzku4yrdn;)+}^u8Z3EMuVPC93nq9Tujci&WijOn>nZ(|W?F}YQ43yrP zAS~P?am*>&1$Chu(0P6{`W8NxcscFfusbx z*29fM>7PbTc<(yfK69!S#*}4GVT+_mQ+ml`!@XDN_K6$Lb^3VRx5i85WvqCed*uQ9 z9k2Gy7y=O%9yMVySlSkj2myJ_Ax%A1OgM$=9-Jwqf$dD~w4K~qPr)i|B*X)W(D4gy z-4eWz^!^^Oei=*41k3dYz7BA~tL;R?L`CYS8BYr{LpL4EiLQ(^sz-y~E*2`|%vXJ` zx*ClEIK!xix-x+Q`L5&%oO!cg#@kj+u@lR3+rPy26U(YR@Zt4jAEe2#HYXXf=k#8) z(}e7upN4DgIS;i62x;&lriH~=1Vk8zFCqku8wv9}Fo3{$k_D8nQR||LTT%ios;gng z0E`f6>Lu1qO;?Xma|-r?KyM`bL^B=H)a&$(V=#-)`}7Cop%DG}@V7n~SKrNwru}I_ zIP>XGR+8}6V=6vusv@)WQ2U@R%BsV!gjQy8!DTl-ig^{n0=RzKy@$4M%!k-yc+x?f z*+t&2_sv#Y1Le@1$9s2MMt=JOVTEVu9 zA1^O#1AN+L2*RxHhI+kv#? zRgX&5EAZUs<_JrExXST9pHxAAU>VTua-+RV^~nY8C9b}FKed8un@upW9bu_f>baJT zbx(FWN-n8KkR{wjAFCGCGO-0W;l?B0)@%T*V>^wb!_^}Wc+1StvyZJr;Ii8v#S)@z z=$-*qt>4_38cH$W*?UtY@fftv<<_X(n#nS-W( zH^@mv*#zebYru!omO7`OClG|INF$Zh-)5uRS zKYcx~Zxz{*H4{(QQ!kY>{Tu7E%+Ow0K&;@?M8=IM7a39Mw3DK!*w-{9QC>yU<|O9$ zswEVJP*($!-O^(x@G0l6^sD*En<9h+C_7^tWc>C3li(WVJ0ti0>i3Evn1PrR>eH{x ze*!xSgAe%xe)#OclLnQN{+eh$H#z?qebE%H=N~}ORH{0*BQbkcz0_VWnZbCY`>uXL z%{x(mv25hr=K)15&iL-wAGTbd%AY`iexM!GAdTrD5fIjj{Vv^VSL@fxq6N8Wv;CPW zp5A(w{lr7Xa`{{!qBC&jZ>BxbL-BX}>M= zv!A~R_AjeqU8&pD*}ZnwdoP8r4aB6UEaP1UJz?<^oS!l;Qov_nG8;r(^@HU?5k}QT+UM z^|waC^q@5x_Hjg~ws@~GX*E0?7cVO(fx=u$W+m@!eChJ0&ZbztoT^?CEIZ=Uo-Dx@ zG}9+s6yU%6_Z*L+9NG43y^HSEgJI$MZG5gxu3n1wtQ%IZfeE@Mi_1Alh+sKG65}ituCHn=TBkcZy_?eq`r6MH!MN6k%x9P=< zy{T7^ciAdFKNLx_t!Awy!$Kg+u1Q3J77pSh8Gr5&gdb{S_RV$%n)NME(-fQJ{50u; zQ}(B4y3|eg>?i(w_DP+^)a|vQ*Ywg{wt#udjw+J2mL;zMBGu62}&$s5Ik$ZFj)k5BC?3aQIS^aqvZAj}_fuRby ze!--7J8TEN33+-7MQ`Q&TbzTU#G4AkOkf+X*cjx{n=@|DBKU6ZJ<_S)bzj|)_qkQM z&WG(~z4fiIe&~I~$25tfak-`+i)y3H!IoWHxN4@*;X!5&RZuT)N@bOpRJ<9jYRYn(ne? zGMvGaH%ZziWBn?dK?nA8=sh~rZ#12``&(ceBHYe2l&!K|#E0GTFf*L?#A5tNQT6ZT zNAgs7J_!b9F+k5Rs~MyZHwnUNx5C@>4qRsiB3#Z-c<$4!zOC{u(by+0fn`{*eZq?b zlbU)VP|6d;dv0e?z*Aux;Y?G_ZBFNpcI$3joY|09>j{*QkJe*8-t!Cx>S`bS^^{0; zJY@(o5}P%gf@7!t*3aVEdy7#eDQ$Q9Yd3aUy1UdP7K;2`yb*!V?Q~1_bbM6&sbLG& zzzXK_xCUT=5T90L;PqpWiZ5TplMjOYe3*p%)?HVtGcf9Zus>&z1S)uZq=SQ{(!K2r<4!gg3_W2B?FL@OQ<2i3q9xtfiaqD7m=H(7SYrJ}V zLOd}nTfJYTXN;vQCAx|G^e4$q8<1Ed4kp!m6qkLpWwh?1W#Fw|XIVYP_q6)nA1JT7 zI=liTHmz?Fu58yG8GK+Ap!Ip=Q}8-Cd>IN@8*YF34j(uW&VoV~+%q!mlE}8TG=1-_KljLaZVdPQFF5QU`zhqSBCi{2CpSMar z+uJn~X6IR|BqrASMXkn6>YTPwdLE}x#PuGQxempMBvA$0pv>t{fBa@`Onk?qa55?6 zp}AV@O@Ogn$)}W!J&j6b!?HQuMI+NKFd|pe>bzTHHAMeF_Fp+mXkQhmtzYM}7>vQd zFgj@6iR7aVz4YbeyO~1*9*f`_t@&@;|XpFG1kQ}+Cjj+*GDy7}IkLSTYuJ`VMRqS>9 zCzcyJboQEgAt$5&8P$#XjBg*v&?>+18xG9Vk_msC!3tg07R&sm{CgP<46EyiDB)Pn z21;{6%4KtJ4c7EZa#04JYny-log{VJR;N+jE!X?(EuB@#u>!Fo_R+S+75&oM4M+bTJ4MHN^sDC6~jt6m~gwKXa6WD>uNNW<%_ zgo(zU_iW7fW;t3fD3lS=SUtI0!fap1zlFIOfiP_{7RwaP#ThL(YCV`#x86@p_$5%3gZJbSMjzJh~5x#GE*o!Z-v#kuK+yV-{;C*XAf z9&jHq!KN>N5=dDkOi;>pt-uOYH1+&-!b#S4;7@0OnZ5eP#)gKKA{aBLC_9_S(I@>6~*=klwm0yJk!ylhi~Mg-?SX zGvpPK_q8`9W_e}}`)Km| zm^{zp6fJapB5()83(7V>2tNu9hN>k{$XwLld~UM&`}Tsm$-cwt`!1)twP|aapzAlP zX<%e4lu%XgGHyoUwZWs>9el;;9MaWJuVM5w;Q!@MGc(1%Ln#?Ts7j+uGJUenV>Z`> z)pTM9#aSvpV#->5b)?2pnslKFShv{|Ah~|Yf=2-(GHW{S&Guj+M)96h1k6!popf5^ zi41`-Ldi3TjbXr>twtD~)1O)|AC_iM@AJQH6mpsBZPIg!I94Joyxk-E#~T51ubH>$ znE!Lodha5EJN^!BaSpoKGeaZFuZ)tzRn!f-)2Tw4F$u`lSJ|i-cMW3Yl2k z5!Y@1TZ5mt%N9n&Dq=V&lfZ;y_y76_Q2rkp9Nc^|`ahS5{^z+;b=i8~ zee!xx<7y?3_O;1m4E&WADb zOuI8(n7(3Zc0+Djaq8pW;seb>jz2vltmXu1sG#n}I21x3rE9F2Fbw%stsz<_mLXoF zTu6{RSRtO_LhJo3eR}A7s-xmhB1R{@wX^hoAfi7@sg*?%!lXp33X(Cmr@oZ%U&CAW z>xWzMXq<{&rf>sUwneJah0ZLiK-;`P)g z!z@_@d|=Cg8Sc-=1m60eZ!3WV63Y-WYi#` ziE+po`D-6@*wR zxx%MXs<~J6;EuDEBl{8jL+xY(E=hp~r;vRE!0&dC?4x?~7Vq?ttQBPHt#Edm2f@}4 zo|&D@IDN=}uY_IbP~H>Q`S@)MCq^|YOmI*crv5_XZ1qNw*&VdJ^=F9$o$P~dLGKpY zaEUzV5Eho{rIum@?3g@wkdm>WA|zB$IvcMZx{Kk!}v zKQQ4x9n#tDztD$TY1EQId?}uxc$w+_u02SXj(^YcS!Yz3kll=Bg(HGhKs)B@@=w@X zgDBCDF>2i}HXOJRzUv1ID@3c8zhPy5gMKZiqrJFi~Ab^h8!syUw=xF4a*kgF|4f3&O08RP@Bs)1Zx1?R4)BvkKXJ%#5 zucgAH-CxcMoJ1)}{lpWq5U8#t2&$#kd98aLg)#1#zrHyY<4rgNfFhk`hmu`%29c5T z+WVyNDb#R~q)r^ma#3M~c3@uHWBY-93_zpggH8E_kU^(6#O&49aSdGW=FF2BeIZ2e zd?LT1iA)I%7A6h}88&oEGm~&B!}e80DyHrB!3DrMd18bi{-pLndk)-wqNh^jOufgI zx}Q9nV1enAdUW-y+u|nbp^TxIN@?c%t2Wjn$?{xX{y7o7x5H2VNkCrDXEyGa-H?dD zD{b%wfsI4dc9zSS^iSbfGMuT1y|MPmNDNrtbYIl+grK=fMg1liM-Tx<~VGtD~zmW4G#aV9QJx|cV;*%4-OWb&cRo}TvMtl z8yvelEiVt5p}R~)Oxo%>^IQFidg7>OxB*d7_Zy4x!$qo<>3P7#!;xgITeM|$vb@@b zhVN{%t1qv>7MNoM<{;2bL#q`h`}j?#O__EXIKBiKe#2oe3iQ_G`+p|fyBO8j^stst zz||m%!QWb`8}&Inl5UYr7u!%g7oj}C3c5FNPEQz@7b=PxavQu|$X&}tE0u=iFrAhg z*RO>UpMgi-EQ;&t>fq;!kweDgi;0QN^ILq~J_X)JCdQG^ckLIl(b49V0?b-9Hghvi zWXo)Ukicu|*07&IIeCi`{mB=ebe!JnH~9F`fTDIY<>~JGz>5rrtlMvoxFlE!Ge3iJr!;oSiicITr)3gHI{3@E5 z;uzu?jeG0W0G~zpxKw4>KMqg#Pf_m*$5*3Ao>a4{gIh4S{4aWh7cXw@Y>4YEn@50> zMOfRw=us^|Y9LqfQAe^l;etxb^jA2P-mM)OBr^#D2i{L zDO9H48125eUdmukUsPD`IR%ZGp%|mGo!>Y5-1IX zXrJS2x2fB`MbI~Cfgf{Uwyrhb9=_4p6L`0+n8Cc>F2_({Kn%601=rQI)qhx-9-5_u z`7C|+Ut?KzQE1LM+Dgg%f20wA{*=kE_y$AVMAg#MP) z?%^`I*blRHEqu9Vyh8%$h3J@j7DI9mb{I3n;VQnyNiY%^?8Z-2k#Tdqt2qC5?*p_7 zAO{cZH z{)#pyxb9@8K3HGq02D5u2-xXUK%=4<;*TY|OY+^TPA#ppw7oO=644g!W6n%=kQUO= z)zsJ3L|X+u_`XB&HV42%EqC(?^btk;HEu&K`PNt3>LL`=fT*)%Awxm5z(Qe`-D3pZ zyv^t4B&q7@&xnfM4a(BNHUW~`&CTtJ!<7}YTbE{=vRMNw0BQIIBa6Z6cJ&Z~aJM96 z)XXg|xI2g-X(`eoen63SWOcm-Y9etwyDFm-`w-X!wBA%^|0N7t+J|J_T86N}5I%jC0aSZ`$xReEf z0@X%Pbo+phk|*hnbotuW^|CRLX53~Nm!uLlp_T_=KetG}gn&()GcNB2(hsm=8#`ES z-(%ep)=2rylSOv>c?kWUaZA$dZc5%K4XM6JZ7xaB0=knbsc6xY;r-G%3AyJ#L60qo zIc+0%PU5D7gYjm8c(bdV7|B>Vd?A^D1YQVo=ha&}4<2ZFOI<_S$H%th+buBGo zb_d0OOw8FAPOz@bGglNe=SF8Iw%M>|#FxBkgX?HqBc-r1$1Pqa0XZ9bKkY{R7?+*` z&S)qkan=UfvIx3~F^+{3P)9Afb8csfX#k)mD&+BRmoy@TzpE?6D2H~0HkY+q5`M|j z$1MsM)!Z`Nma`|w5Yn1TiS1B%Hm7zpyCJaN%>fdW+4i1~@7e|l)vO10gX`sa=zGSW za`u$WtDFng0ukg8C-}S!T*Xj`VN#?x>4In-2S6gKs9Mg~-q|%jt~ieZG_ptea<_Fi z(|zIY<%@Zb-x4bt9MwTUJ3IQPlJo~FqCS1KK0mx_J~ULWsH-L6OkP$J`J^RxM*pLp z+&RDC-2K)+(ARFQc(ZrN13{^4=ttdmf5lcj+ z(FJ;yy}H zkZO3HP)iye&8lK>yfshCFJv=NWrm+uz5*YXI|+Qczrt9=wOX`P3ve(+1pN}I*-uOY zLyvhRR*@+@@h_==}g0&zmZ7Z=kOw6Dk9*I2G- z?@PH#qLgn^LU<_=aC6M`BUb~Hh1K!yvrUk`2UaqneSw(XuHg2EDOopVJE!PH zVyc;~>4126W6!o{^GK8qCU#I-LWIF)7(LM!1K8oH)!57w>GpZ8Kg^q@#6v!Q`@-_c zc%UcNCoNm_)9zB=*r*|jO!=lJ_LTI;hYpbRAR{dk17 zG_Pj1=`X!b$cw*=i5<{PYXWp!w*!HI?%^$2%e{V`1>X<+&jV}7jkdd>n4p-`imDDrfCoX|@T#7e(+(E2 z2f!}Md53Xdt_FFI7?iXLtvv>lApDa*TT(UA1;uw4_REPcs?YkTbgddjBQbrZcMD&z z>ZY-AGYdV}WXq0cLLnJJ0zjV^49x7)*WVv_AH^dnw%Omz)DowD9iGcnjdUQ2TAf6} zbAFOHYtagpLZ-Na&9-`tT#M1@%Ngq=U$CNxD|SE%Tu*!ynFwT{zqndM*8rs%5CaOw zIMTUXiCB<)cY6WFrpcJE!anBS%o2J-zD(hZVQ}&T!vNOBy}iJNS#}yppGYmK@?QrE zK=eK3BFd?hDQ9mGv?Eur680D%;KR@BS1s{dBxXrs?6TG0w!L3SVNc*kPq?`DbD4*$ z$G@i*_%En>!5?u4H+^I?1{Jd?SGTim{}aW5cm2#U%*6+>DTVBoX`6TJlAg!wb*Chw z{J4Ft^0H=cReT`pquF|Z)WM2-M5OQsa?Nu5#NPyomYa+~5B@Aa|7RC_VcXx*Bd5go zeSH6}E3tS#|BYQ;eqtq*RnBv}`Hy>K8rsc<;dO6}6z3<{Wu{1ZEJrs$Tf8HnE`;!= zQYjB;XVf>O+xKVB2eE$nCd-T$19BvbI0`f!;Bd0za)NAgX~XXm6R%8;*o;|K&5t-f zbHf_@h$qd7UD{WEHL8SGpsc4D+E)A!YPeFY4=N@HhGbp%Hxm_DOe2HxrfU6nMo@oowQ&GQ^ zsTDUCgXS2}wO~oZWs9wPV-2P6G>Q?lZ$QOnqkA90Fgrf8{3r2Q-u3IoR@hEwPynUkT@%!9A2wjo8yY zMXIoFXi0q^=Ki_Vhz8uxWj*BYn>Ap4p4&@R_07&QTEHIxS9gg+?+1g~{bGk7$K-l| z+(*$jGvX96#g)J+fi>ezFWok2D+b#-eRF_smeQe-NN>~{-_0P(4W57C1kPN~A#nOY z=!hK!N-7%N1$?{lfLEAzj-J=8g&G#33YbSi;VsBP=rI1q;wpa^;GN+h7$&x9YKu=0 z0yQo32I{pZ@MF*4^garStjvyw-91WsZsj?zsjj+*(VR9z&?dz(Ohc(Yu zy#jtd{-_|daZMq<{|Ofz_i(mRA;2G)ASqVnZ@{$GOLt5(BjgY4fN};&o^02AH3xZo z8nc<{{F|E8j6Sh#ahp3g9B1D42xIrLu>P;ywoHv+&Wc+=HRYS8WXN(7BXS<7OvrY- zy~2X-?W&W#URU(uyH&*o8*pV?;&AC78cFO%!jdxpU#q6%4^sw35H1!c)ZS7EkWou^|uaC+cyZYYrize1jr7W&Dszf;Pz4yhT zpe415AYWb4yH&%ak>QySOM3V4iJkkjD255C#R3H!EQb``>iv;`{;JH;r!Fs(S+zfh zmz?kG`HVcHAQj(IUrZCfWyAfF6t+B86eL|p>o&`0(xXvv2cqc|v+H^e8Z~#6Dg;1O zi}?~q0c~8@C9=uvg9u+(j#VjcBsg6vY#rm;{FO6A>DOqm=lwuN-I6_j86;#NRx^oW z5T;kz!40p&FU38~D1IeU1vE(>iMh2~Wjw|v!%DoBeYxcRf%~K#ls@jbru{%6P`Sn8 z0yKYlnsxz5xk=|TGmCcDtLy#}J`I`;Dg>R256kt|f5mcWp0vW}=!k%g1kyQrQGhNN z3Dvu&`vJ~r0Ll=AK4_ETF1`}?1|3tp>+~|r)lhrqW=@6SBRt3g5+bve0adq}5^^OW zq4G0RI`wvLPYyq*25UckNOvDL zHdJ$c%jVofGm#+r&bX;?JqKb3ty`@_sRqmz;5UFDcE>ZUnA&{s363DG(y9i80FeQ_ z&<2^N{nOhxbjahDE*y}RP2k4hk|^jWs+Y@duR4TiDnXu zzc79pSKK7eXPfG1zzvMjUKsITtO$$9=|LM=N<6^I;nHW% zB5>NGc!*St=gduA@cy67Wa@CQ;D|=8F>Z~9x6@z-llyfxY51&%@XelQO&p-;Rnh(U z^3*9d7$HNh?0^eTDdhuZdgkqHoeZN8EQ=x&SaDAFU4qFO&@uQVdF zWXJ8?N47CCp$YOE8-SFP z$e)z>;JGS1;-Y=*9%oR+^!d;MTlu%({fr$ibW)Z2?}%D0wCp_y2_y#T6g01ejjlG> z(4k5oPp8gkf*MaYQhUr&GbJd;TDmO3VyhTjI^NX7$_#>&=u3G;tCIE@SBED45}h&+ z7bs{5kBauz2ar*jF8cKSugfIB(U$A0W3bzYb)8u^Q%a@lUm5|0R=RjCT9lpLJLl6) zpyujhu>>vGu^>c%V!Ucwuc_wWIo;1V$Ch6YrPfy02k^QOC>;<`6JJt{@4u#47^16MKB&59U%mbfM-Nqj0IAfnnBHV=5MM-!2)kdubT2MPlABG=Xp1Wvb zK9sd9Sceyw-P4l6vrw`DZbvUECgTF^%X*v|bD~Xbom;jG?0s50Lr97A>>SFD$}cKRSojaqen9h-6!6aC}hsBCq8#}Bq~DY|WD zi;vf|%z*!$N+*~P$Y(%KnKwqP|HQ}V<)$G641HY4R=|%b1N-GN$U*Q7!AoPLtwWUS zIlfVg%^eU=2&!H866U*7@!o`FN`LC{XFGv{dmiEIS~D{y`nu_|7kLQ|WskT%V}{l; z!WEot0%p3Y&3;hE^zHc7gCuOBtB%X#R9ql;{Yg>yD#^v-etOZ38`(9b3T_U$!d?3c|Mll28UlF+$9#6=6=9HVgrIAobfr3e@X$YFMc`mP2% zQ7ia`h0Wg@$TVpQjO$E{18vonN0sSqnWD7BXP?-NgDPePMW+|u58X{aL$WfVy^ZSt zT?RHM2MKICQld!S`t5KWXp@w%1DStaVA}t9t%JivL%hAzqAp<@!X-LO=+E%`rgrg2 zK|VeH*las8`Z^sezH5gW^J0e)I;Re=tMW(7-qGNG{V>xhi_OBBw!Y}<(Gd{F%x}9J zOGC%r@+lk>ekyjSu!`s(5}*1lcsFP)75ps+zS;3S^^Bbw^!$&w$i#rE44G)w@~_JS z?TWb%A=18NZQ(zgdQqWCO~YB5wi3+bU6R zCahmLT;puabDbO6z2~5S4GdAbKrDE3el0Blafq~u&HX-}z>z)Ua5FHT+7{(Nx%c3o z&NY-i^SU578u&>Hyk=)GDM)&tjS*=|)Xh7I`ab6gFz3_SCq%-rphh!LV7_498#qVx z;qgCyI#zQJ6k!1XO{ZsTYiXiENd>BJ(EJ5W&~b1|z3B-~+P+a?qY4mNI^{54Q~hMF=rg(W;10v72` z7d?F7LrZC&qY!`%0LuyRUF&l)V6xebL+FE8$34PSKT(YDYXvZEgQ$eA#!;?L5?An1ynk!RXAH1Lr&at+ri_$eH3A(8d=s*21S# zj>v$)@8O));FK=LZxI0q}D$Xa0$4a$V%ysfwS!6VU&RZ0y~FRlEdz7nmxlu#-!|J!=E9q{=7rgvc_d z3I$)5G~WUAp$Ks_)QkUcz;ojp*)bAvoSCFV8++rv@Z%Y~QzdTAI*T$8vplSfi zU;8Ml9qA^2>JccaL9qsQ-j6y1+;BcC0P-@Zu7RLWP~|;c2P77CqPtk1<2RBo3b72% zkuT%7_|`}$kv_-4iGC9ht`6u1SCA1!t^+)ToT1zwdv$5M4Ej%u0ZjfC8o(LxWx7A^ zbp3a*Q@U6|Eoqbd zf3b-6gi?aYf6p*>qWuWsS=r!&LSA}4CUow>fKM4d?Qr9isC_<8Mikz0$}0gy9ymGU zDhmd2G^K}IzWE&)3Ez?NFU*1ZVWY3l8n6IO%{|bzU})hzBGrF8^FyioVk@Y=|Lc+V znM;Xpoi_TVcfU39un;CI@{l*f)yaXTBEpn#p==7Lz3<;z0MvF}i=mVKAY~n91j0un z|J16rkQa%H>?^)gX0m(kqD8bXzEc%&qZwoebdV5&gZGh(ASE4M=khYYqE^%%6J-7) zB43;CUc~}ZKi}OOy8QVlA-%fv+N8y7dISS{KUN1tGq*3Bb=WvKB)O4_!5aqspbRb( zw-`dv;ccz0R%2ck7U{$OTPtHF2Nt(KB_Olb*Hpp#W}iL>;Vx0-Hj`#>=Hc&$De1C6 z1~IR_J_bvf;dki8zZtA4mtTIRnni>7xHZUj$~}0z#usreS@x&(-nDCJ9qlO|b5^jJ z-uO_cH!LpOjREU#a%}Jv!z}gS+XUbG+%uLk zNEsq*5ZMa4Tej z!T1E#t2uyS3M}RpeuhPcv-ixnhDS#_9m`adS|pRP_9JW3ejPotJ;ZN&=mS4a3KwK2 zeSVPZpbPDxDE*))mLuP5W0L6Cph?nHA7Ix>cODVgNaGc#UW_AxrQa+c-Ifp1G-snT zOn%_=)ASX6k1*QWDSVwbZXe2or?syN7vkt?!Can-i^6_9k#D^{GFGSGH)TKWm-VYo zl_mq5l(~JyF!oM4hFSMjh$GvU_vTn=5Olc@xlf@YJc@K0i)Xuo*>{F4C=Tu34ck!C z)g9R)&-T@DWRJ5g_)h%=z#LYe)35^ei1YxmDCOlaa6_Pt!!UWguxnN)c+7><7e5x& z{Pn4~WJmO!>r~qdEgn}utZIce)`PC!eV=nj7ZP7xiY9YeO>aD%+E58`w=2wu;@SMj z6WIr1W_+M}lh32Bhu=r+W5)0}1bV(>Q>PI*6?3-fZZR^c36{5vYs$UFY?wQarq6e< zm2(h{6z*0jKSPbCf=kQE?C>9>YQ9%D$pL6CWPag`?|)C5wNXVR`>+L>re);>v?Sj| zH2-uL6L}( zkF%mvnX?6wK(!f?rH60q@vQZmFd@C=-b{^937i2KrROr-=>dRYZOaJ*SiIFKiMXkB z{6pIAKd!B(kO2_NfbyCIObn*u1Ak_OmEIe=Cz3!Zz4IqXl4%QZCDd(-{=~s19%k0y zQ8n$iXbbyTftE9x7olvcMGM8}se^Q=pvfu8zL$QbntlUXr2793Z%(PY9>GpG2tF?J z;qsQfPM4O;?;ss{jn;EI%(2uF2h>CcdS&3%UB}dPxY#1Kg)&~EuVcL>IQ~8G7Qg99 z_u#;>)cf^}`IisxbKEvja`(=h^1XvwmYkR`Q?uI(fB(cj&1`6=>CTWcfC_|rhC50D zqx#Pz`F~a47~FR-8$g$E?Ct~T!(|?A=!upHck@XsXQCP}U!)5Cmd!KqoQi5kfgy-Z z)+YicsD`opkFhh}pv+yrqnLt-pUHEBdcGgyL66!@dF;H#>Z!4tO^Q#i{+`2-uLNC96Y_G(oP}6{FPqvn^ zQ-mRuFIrd)wRe{CZt&T+hnWV{H5P?0{x!E;10*gX9`Kv9Loi^iOx06QH94?>0 zNl0sR0H0~SuLLVC_L1PKddQbYlTU6zA_cacUZnUEly9*@SXo_%xFL~FW7TDunOqgY z-aCYu!oot;T3+kUBuys|CnqNhi~ozSFAs!zjsC7~r45CUwM4>1i0s;g6xm0Xk?e{h z`&Ox3*|)J|Df_-JStpg9WErwojAiV5mf<~L>UQt%{k?C0+}rJH=KFn~=bY!9&-t9s z8JZiaUA=6!soDvjiPS1bqLI8#-~O!F21VGFKe-(^ZFrt3Q7UXiNXvdZ8O5#3v>yG( z@7tF@V6O_goO844+c*7t_wI=>EpLWA6zTi=HC$~dD*4Bp1ANJJf0=HPnVDI4sjbE5 z1&}_R$ncDe{QOywG-dhVL9^#VbD4Vs(&Mu5<>#kF2+ZHVbxYD@__zO^CtggLT6c>d z+^^yb?l9kl9qAcRIg0X452H*^Xn1Ws`>m>A1~jE|(HXh9x#{WNv7yBqs#u7>!aXT| zJDndY^uIrqO{R-)i_)VCUEneEG$OzX$L@QC9PT_k7ztR<*Pquu=0DZ-TM2La@VEu6 z%-Qd`mx<4$|3i@|HPuBw{c(d%{qDB5poMDvP$j8VcIq-AkQ`cEgqLMxWE5PqUI_WM z%g2N&f)wulvzANTT&BT}t4w^Yd-)V)g>(M;_Z(Z=Fk8 z94761vhU@1DDR)g-phz2F>VCJW+0Mmi4twc*g11UbONZ?|JSkaWWgnQC zjgO6evAP7vU#&mu43XFtCd$mj)K}qbub|KfPfA8cHdQqrtRyAs@-ydtdrwcAUdP|= zy1Kg7YI&@uHys^IT{%=l34$h{Q?O^&b7RK|jbUY_rEs3{!e#a|*nAP25g)f_EFJ_n zNWK$qvfNR$6JyA)9L&dSoDLg5ACc%gvWC;)fu`Z>IfZHr_(`99rwIOY4jWAog3Qd! z6%`fBbT=Ne6EoqtRw4vVCbypmIV|Vthij2;@$*Xj6+yof`U8*|d+66^>zp`pJxS$h z!Y3>io2gwawbEyE?%cWgS~%o2HP@HwIy1G^*94wHA&H2d*Kvh@ycBLUVjLD0>AAPy z{o!4ff{JY?G`g<~f3YIb`_~p*_LtgDbY41n5(BGO7#m%lb>s1xhxK{u*|xSlPoHdT z5ziayyrb28sye>vdFz~mtURpOR2N2X5q8D8*(kP3|I0fm z^WwbA$Z}Qnc^H8qS9y?`NCtV3Q4-xd{IBVicJ*3oTEr+!?UQ^;+;o>Smd%b53?@bbL?vX9?$ zp7TdwQd%rJMmIQmGP$Ew#-&z=gW)WW9WG@MZ66R%lw}ay=i5O9RDYC#LqORgq$q8q zq(R26{E1EM-PD=(xfh0(>G$T;8IPh?$K`YOKG`?)(O(KX9y;SbyIT_u5t3RzpWZ*Aqj z2#30a%mfxHL>T1P$Xjw{KLcV~g|<5@-ddk4&+xdU&ITw8fhh&|nY@_vBv`_N3;Aj$ zel8zFgngbkz8Vh&ceV{;U4}a!yaQt1=jSh=(_V`+*k351yS&8h1btxhsr?Oi;-PfW ztv4;kB_<|D^&s_(YlH8prC{}y9GB^mQ897x#8o3+bWeOVX)Rs<#6vb4d0pOlCSdQd zTl0GP4TG`E)1Z7;vxyFqfycLlC($HLD%6Q>sP3Ad2LZ#YC%!uJzWyTFruEz%%f^P@ zT3N+P(TC$31jp8=5-d}R3t{Ve;v36zcNNJexXxc?Mj$xbWx28z4lm z&AhEM$|-+5-J&F!JCw?4rz5s9+J_AtjkK3s`s5dEMbYF(lcK2p9De>0?m=Z#*DPuaN}f3?rNJezIW$E^A)Y4Q_VTm8zL z$=d=%FKW)ogJ0%uF0pW}*X!hjFcVL8@>4f>tO7E%|#;_8NK?c|Ps;Rl18DnG z>^;LEW_%^|DPfIC+D&H!l{PWt5a_GVMV4*HRww|Bep~>P*M>5)*PKQ}T-p5JEGPng z$$l@uLYFofW8Z!HvkfQFyum$Z#1uvEfdz*0K-+xL0pO6!_QEC+vFb%wkKDW9>{8dlei{k@Vg!pXMKEfW>JDOtA^MTFAEtj`XgRE%iI0 zB+VZa(+RH%0?28eM{~bN;J7l}ce=XKAFU3D@nQU`4gBn*FBFx)!afqeP5^U(;L2;6 z+P{{NUeiA1+@7p2R{6Y9FYKuzT%%)^S`xW}@Q$vOj*Rlcp1H4_lpu ziz}em(1`%(mCm8x2lOO#3?QZk51^kQ0Z)56h7Y;LhZhu>H%NdZ1)^%+}M`2|b1+JXur_Mqa9Pp|Bd_Lu<_F|X0Zb@C{ zY+qlU94lJU$2wC}1l`N)1Lf@RPkZ{URM`D&E0K;pAyRDLBIQ_p8YgbIpz|3seP~mI zxdzNyRpiJCll`CTvpS63OB|Z#OqWjQ;>N^VZCtaN-4vWdiDVS0B1<#OpQL%ldnfC2 z+vh=(NQ>f&#-T2MriVv9(Qi5FPsdz5={%LA*RSVd=f5rt)W`ez@Tm7R`DPCJk>-B1 z_r~B^?{}~H#H>iL)GRe_wU=$#FE__n3?$Cn^7~Zz5td>+EM=zfqqPGOPM^j6-=xxL zub47Iru-~#%B4o|_%_c(5Qu&u_+~al603IlW>yX!9v)9GUFsLL-W@x>XEhm78VINb zOe*5P3x5S!c%gAa+(4_{@RvD-(M<_q>$iN1&{MP=cSbwLD@L4JvI{>K8911*Re0ZZ zx7F4X0fo#j>;0dzj#Uc~;iuft`i9{QBw~6Mbm>E;S8*ghr zerZ{pqCRaBH_Eh<#9ALQCQ6cXTHdnk)aRnR6_c{OrL(}hUSB!eWGR}O8+e%5Blh_& z;wic>GZ~zpf3SDY(@Q7c+jE|jt{()F@vf7U)!BmqGcS_)lyj1rDhZ8c&jct{wuZEev2u>Lt(*ru5?OBHvwY;^tZC0_$MOr?Nc$*^ z@`)>!Nl;6ggj^B&r~(4Ve-AM0f{7NGZxO4&%hnaT!V53|1;+|Y1I4V7LlEjhhWl+h z!PVa_a?DlyE3?=!$>zjazuy>uN(e0Oc(>&8*|G2y_MUFdv&@Vo`WcpD!q%55t;}sT zB+H>krAAo57DPF`u%*#U=h>-=5vntVYc|X+Bv`TeI6dexiv1bp-Q`1%3hSxV$!t6Z z-hKNrg^PYCz1+oz)Q}*<`O3Tocti=qlMUN%V!LLc1r%8mg1;3jYpxZ_>~@l3598}b z2Qaj{w?n#BgOxugSxAb(W(Tp$XT#5;KReO;O|`EFrg#OxeY{R1KV(pJ-?heV<%TQ$ zMQUV2w64O2$nE=uEQQa6>m3|}Vnq3AG@A?;=p`IU9lZ$c`5uI?tbA!=U-OnC=wka% z*Pm~!YHPeJqUhcv57M`*u-ml|6u_~40ml}Qy`9%MKgcZ7!8%D%u~kK;b4Wi@|KQ0J zkW`#~q0eQrK6$u854RzWGaJkvj8Z9jtmKn6MM`qTNp&mRg*NMt6V4BR4qLl**5){h z`dsn0P5Axi!?S&rbO8T!qGbK9vg?`t!S~QzhiuTz3yrgFtBrXCZNX5*>3q|{`Qg`p zwFu1wg(ojd3tLyQY~<0RBw1c^`z@Qr!=Dq04pZ>+-3PH=w8<5ZH|M=urXz%?1mVhU zIz_<`_`|Lp_@T+7!qXqhSwnPukGOWh?e~qjii+z7=!6kFS9Rg53J z&G0?&YOT>{0}wB+{TOhrOiZu6MNathh$Ko#T8Tqp>fBNFZPa$X<6&0aQVG&FHQOnehHIvsA7@7|<0 zun+m@egFMv^X-a22c78W#9fae02lze7uz#(@c-AGj{RqmQ+LX>2$)0j#P8QbT+2a< z#&UdcV+2~oL*kp88#tqQ$t?)?7KtXnVG;{>Z~)$Y`cNC%-kD?fk-B7?H=NX+ionlo zqm!5|E(@q?BGuc~Z`T&5;BQ@9yb!kMJQzItT($)7Zk4Nlz6=glz$a-VB23`(|h@ z$0hRv!bsU}TxQ$-s%av>k_mhcC|d1&<6E}2wh}DG?m|oyo*(a4lGhB)T{;;sreZk8 zU2Ty2P=v|5T8DzGRWnd~{vx6dR0jmiIemN6jUOEF=FlUxZ3q8G4Bn@{DJ{KY>@hV^ z(fb`&7tw%BhLF_N&=^@+x3I8?-s%sENN2b59a?AeG=xT&8GBSSI9|BWX5k<}l_-08 z!%tzi2%tT)euF+IWw!AIuSGU6$HOAWFI>E+uwosRE}KK65XP$uZxCk}{t*bTw81+$ z)GiSQ`ptVe;*Z&!BL5#Bvmkl=lL3F2BXX;HRT5)<{Fr;U9-}JGz2cgdT{HA(-Pl@= zmn^EMl~=b^jrYY+dxZ{1SiAh9`ilMu?`p%po^eP^B7&!~J7p~-Sqt0q5F{x7=VFB8 zQuU$UjIgoci{!0&=;@zlN#406LGznf@jK!jI2qy_JztgLKe zlI&Ev*;iF{c_|Jy$b4vgd^|5NFD@>wv{Xzq2H=D4jKsw1xd6zueZwFmwB}NIH<-|q z&w>iBxKVRzoKss(?ShC%N+o+)Q2f$5X+?2T zh<4tN-~ZQQFI+hOY3R}TU6plqpVaKMGED}5B_$=YJ-ZvPKcI$Xu;S6o3P~EP&`J~M z!-j^YhI0lVz*`d1iME7vf!fxYm5CEdip@i(3JVI@Uu%Y<=S-h*^HB|r3-Z=%d9srfYp&y$S*Es|leLG&~Sk+7LPiz%`Up-PIacduJ zT-&UnfzipMIW0Bb)dtI;PVXKle$I~jZ5Euqsmp9}VX1CZqa8LJkqxXy#WFH7d8~!n zl4uGFiUxKCGh1%7lW2m21h4J+DT@>_J&y7Cnp55B360uAmF%dWNG`tXorBOKkjEDh z_q4M@K|ejE|7(5WI6>5sjBHni);%j*XpwU@PimyOBuj6k>qhlytlY zN(NU2u*%IHIPIvcwn!n1K1%s!w$Kxa%je~DhPTguKqT;{p*%nH{j|=Z_xq}-E>UgG zj#8Zz`n4$yi+qv<&1JnJmv5LY8B!Z=B+%2OzY>ZZg`}`Uhs(=9g&jK7q1v-D6>Ym7 zDa)eKD?QtPXlArgi!Ubs^8@a`s}0~V85tQ76|vXzZ;GN}*VG3a6qs*vqZ?|VsA29I z^3uIH;c+Oyq9>2uPG9Qr=Bo;djX|7?xnH?0_g7{HoP(Fb?swZ>hjmkDPr^%mq-q)n z1o<{*6G9P~2)+hf<3`C|Ir3j; z0qquC6`q`WDJUkSoJcf}FibW04hsvY_M*@|?^myUxV+4p+v4Kbj!O;Ay_-x+Ns(*r zS2Ow?tpMq+#^%_{da}u3teYTb6S!p=v>Dq8jcNfTmSyAH?Qq;~!Ni zDZwdxADuzgyM3SZfiq{HqjdkT>*jY?y?phm$BiI29PaJg%dYbMS6hJru_nB>fkUH` z?0E2Bh6-fDwg=^aW!h@>)_X&nPUERm7mLSLCBe=Qda{Gu#FnoTCnq~_chuF5 zCW}f+v|NQ+Y0x`ndwx;I53KwfoQ@~<5KsO6Yx&{!J3V+^gh?0@--jX+A|ikmwy!te zefaP^69r>gDA`nyWX}bbAS&DF=CqUGf_|{t8bzj{dm-kD@BlGcz;oKCGyq z;&#Au(GUNhpN0ih0w(^hlXS4xa)-9he#6k8vyXWHRpEy1Uq7e!4s)(%rc3 zl9OEx5E5un3Sa#nc)Qo?5>FRdrS{x=d!7*~1dN7VBjw*h>c)sOOPD03cR}@R;G^t2 znuvg6$s}An5A;t0D-3>9DCr}+f8-q*1X5lyNTRXx>?2*-I_CfZuz=dFL4W*aweo+oXucFfYJ<43UklxojVW>R5fiAGhaxyJ5K3~z zGlONoP4HE^Ai?<67Am`vXs80_O@!KY8#lK1iVu$2-h2zCrOF)wLjY+E)6Yn>>{~t@ zmMloWAcn3n$Q82agpj5dE^W<^P-0^GJZ2w`Du2E*aa%)!77n}fApqUM=S4?HuP*3H zGEu+;2;6cN*R+b^~KoRaFl~CLM+&M81huS|G;I$@C|6d394xIQS-oudzuF z+cqe}W&wfW@9Sv(6tQlx0SZ1#R9uepfw7WBiAK(_YgR6C-{HTK$QtX$pUMhLp276A z<>^-TKt(iC80}xa2cyhMx&gmD;_l`xwscG$gqXQ(_eYIjgKCsG1e` zv7?c6^eE~V$otGltQL!%(!Nu~DPEz%mUtr^j=z};kuHeE_qYL`JI|bP2{ry^Ee4*J z!@Pl&kWM9{R3ScJ*TSNTF*x5g-~HK<+tkt$8#O}zlvXL{i04OZcF6vH0b_G2O-ZTC z%Yf}0t}lKp+72EL&Lt(#piLvGtxBk*12 z9924<>i&ByiOR?h_)8~qtOsc3h(QELSv_SKPrX zXRayIE`Ar;jJ*gLcNCTT@L8gvj3n(c)A&WIq$4mb2;K>f(ZiO&oZMSbyiTc*_m5gi zWbI!33RUppnO=*&fuO27e0ZTX7gWQRAr7=SaPRmRsKLdZxE?zGCj>=nX+;RdXJvhM zc2QA6adX4{bHN+~d?m4Op*)%l$*HtCg|b3J0cq*!cpJRg^gx9(p&_)UUHi_RR~OcJ z8f;V2(}R6DYx~;Te#~)?m#MehSnM2Q%+Sie&g<7xLORri$0uTU;rpuKq9)C4$Fgjq z?6^Tb&&(hbWC4vXG$G=K~38H6QU7?>_SDpEoFp=G%_lu@K_3)JN}#umbn)dMMX2N1|5AjqmJrY%)CMBDs})c& zfg$Sk%m`?DxRx+rr8ul90Pj?|%Sw?3c1rjiu9N}#j+PK%CE)6ve-`(slh@hC%yUh& zf!|s5R_zc7BB88&`^*cr#S5T2*0}JgM9NG8G`fl=X+X+_n9?>eZb2N~P%eKj&T#bY z05H)k2aCHZtuqc`TlU(C?pJWtNbDEs~JULFgPy3B0Qlzn*u9ln8 zcG?gu)r4U8eur=_`sQ*GoG^Kdf!B#nt*||55=ozBO5doljmtxvnN`B1_>T+)|p4==1gMM=NPgY*%2@>cU3K9>8$nR8m}{<^Q;%|G{ad9r4xj6jV!-)D-1?kyrIrtEaBSN zzgsDqxi##0Xqj?auR4RWs-;5e*4CQaqU`XNG_c$zzqnpM;}NJ?r83f%>iFvErT2Q6 zzAoZpms9eW%}2CQw86uI7Cp*`-=}>^d=(O-scsWgnko>pq1-U`?b{o*tFF>;*;pST z26PAqHFvP3P>rhjwkvglWRO|d{FNGmv}r5PA+~g?CI$uupVA~U1^kmG>Ms8WU{2X0|WtO|(zvcEkZ?hN$Ql|P)g$p2+v&uG6l>3ZIzgaE;d?_&;}SLpGIdH+F#h(P{t8=f`YM+j`y)~-BhfvlZ_`bY(3*lnEm#~JOq8xD=+~%OpE0+V<>5DJb0yCgfg5>7| zJ(KYkXn>RR`KI{#n*g;8Wmf*WAY7Ez*}mQX-kSif3;=?q6(Ilg+}ELL)sa zH}r~^XeOq;T^paUq7H_-EyRpSMd@;mj!%^}5E+-pL)Z2;>;zl*)$22`qDTzlMyUW( zHKkg=R9aa9?JQ4j5-!f0Q?n54GREFdfn5T6mg!)zeHYJ^z$iokkYVVV&4{+)c)sc_ zjD1A7xOZymRs$G!JFTuWdRGIo(NP+J=CtX>NGQRh;#QK zo?{RqPT9G<0$LnX8`#^>sd_SWEC#;Sn6!9CX7eS&(utt`6WZ|eJxk=)vmU%Aq;@LU z`E1(zqz?%(63}YHU7boyYH@;=2O(}_X#R3C)G2$NR7qx=!hI36v6++iYKHZ9W|0uL z4$>lC(8p^94Yg@+-9E~Nshi{5eY<^uFaIyWwFg4|9BWk*n%N)~v?-iA+0$L*Nh=4J zH)uuzw;Co59Kz;(N_TZi96)hgyz#Pc{Z28m_$a)v&^GDI+0gVGR@2e`+LfeA)+Ibp zKj$9TIyLAIn{d-JF=eEus|wb40@8pX1Dye`6$-TlihH@?xk_P9=6$BO`@h{bk{A+x z?j&{J%uG4>+EQvndlBIho-MW}C?^$W8V0CkK;|1&lquia-ED@U>Xbyk{P-Ps$}565 zG;`DvZ%eZefmV5Fy(VPK4X;?FjX&d_M%R}TWe6V0%af#l4yvlPW9%agw;RC&8_lX^>`}40z$6MLmSfakAl24;QAYJCH5=3s^ePUI9WpceDig?i5Q?o zJ(TkE;hz*7uPI)O{hN!=97mDodk71N9&=E|eNnx{ha#x5Hq0H}#E}u>p_@zEcVH|j zQES=A4OHUrwztLYCG?lf0!`}kKDJeZ)H~$&;)OqR=F9@)ttHswUqI@wA8uMLc*c}m?SSmX&MiFs$Jm`q(VJy*MgdU}SEX3DW`g0G;x6GI* zcm_g9IM6lpQCh>DAPMf2R@)vTH%X78uDI0%Uh1EF5Md~t4^rF*_Xl@1gb1!xH}Mp@ z(^pfi;#*2pRq56F=jojnIoC*T5n3tFj3ihzn^hjXk=8T_=QdySzHzdEHHc*FS>tlM zR@=HkIE{uViw8t6DkROQ=7E6jbk5m|TR56EO9*Gv($Wk^QBFR6X@?nv3IGjpw^;nk zuXbq7XmOpi-2G~;Z0M1JDSW}qI~V%J7dF9??4*2V@9IVS=5@00?fkNiHCFK!;FGWx z&_AH#3wPOCP)@Ek-EE<1uQ36eNAA5b&?gVWd}DE@jr(Vra+&BFe)1_qs}HjSEPNs>B|!h@vUZ`k#w55_#yu_gU~S zk!j()fHi)92m*og2d9|+ z5*{@E)@Rc&WYuFtmH+yPA$X!K|2uiztk zDfY)df+$CRA9N8Z<8LhWUb^nQHZ_q=%tkt;*3-qOHKgO`>Yj*^OqCq0O$&BZ2;6v) z9`u<8*Ob&KxNs`E(rSV8AeynQ3aDXoiw31TkOZAPqz4AunZyp22%&V)J4PQeq=@Qw z*_a*`DLl3VzRp+eFimH>ZFFAUR1gjxYt9Ds2*~b7juHK{(M9F(wV+cALIe%YLxSz( zOQL`0krhU;FoPOn#Z>)ab#*mpZ&`Dda?UBhIbH2{BH@<~O;l=kgX|>4+e3k-;A`&? zZ2LYQLEh6O^6eGkV87(#oHBuj}xPRC#p{LxMuYv+QPw9GDz*%h@``1@{Nr@}l=vqa9A$ z#7?QLt41K-}&E<3z&ciy|WLT-W`48HLHCnIe)8PQferOU}0g z`Jwc_;A3mUM(SP`=iy|!LDw0mvaM!cnspu|Rf-X|4((-yKflVkCFjqM=B>k&G`O!& z7tT z*B!mX=eTuF&zhhL&%}Z6K3bvXZ#D1z_~yWF?!E+#TCjKh3?^#Om-qPAx-cSwG2KiE z$|@1^LpInF?oRdM?~g@UKydP>FbE@FbN--q3A$ckIFiZ*j0&NsS*evPnCigZ5wjwf z$-vkCFXcR|Vmp12{K~s~1Eq4mfKL zG_2%r#OaAWGzI{Kb=r#jAy?i>f}Q|qEP=RJexJV5yZi@AZ_GyDSo{SFJ|Kd)FVYTF z+~m1uN_@ZtfPCVJ*||=3J>3W6d7~w*>A!9JOifm3z2yb7F1CvVcA^t{oab$uqUXB2jA7n^dzck1cY*;^OG*be`J|5nS7+;8?b z|1l>r)7n=(G)IQpdUNwSAUOxH@I+PU`ySgLf%I{BWBcCE19zEM#_o5Y(Y06eMe!0p zNo1*^W;Fh_7H(?6t;LhLJykQVE9==)6W#Nlin38#wS08Eqyc*z-^Erne;3k#)mN+Z z)d_58_TW1CDlt3O()J+MUvjM-`CC*d+)DopY`D; z$~DYWFlpq`{>8ZXwjZl?{Wr0ltD!ekAD)NKzgDnRIOBN@JU|20U{H47l0SxbBhN?+ z{yFdOHj6vgsN#>BJJ+7?rYRev6@a3rFXcPB6 z5rHY0V8yw$G?VyZjOzyvBOFH5OJz0W*Qb9GQm+21)Dg)1yL&ee?AKcMn$JU@)n#xBLPZj?D?`zF^ZQX#KjdGWFJHDP)&(YTkB!pb=z z(o)NHN+xRrR-AR0bQ-x|`J;}!4EN1stJG|wtX$5uLr~(nUP-=MRRxcv^rw1!;sZG< z-{%iEiJ&b7)AiYXz!OQ z?O-=Yf*G;eEzDE>hANeqdE{sg97~i< z;z6e!$1KSQ%Tl;L;%SDR!|A+J!}%u-l|!lPXXu1ZO%X=O-p-D9az&EXTeAGFr1vLq zY&+DR0L~qT5_ISfKH|#r`twk|Y9-*^9ycTa$*aJy@u5ftXK6=`=JeXDfn}8gSh&=b z%zvy@u9MCw-Cu8|+;sJ_st3b{@q#731o#c&KET9W!C}l(!Sba1K>if@VYmmu@ebDT zQ=_V~bw(Y^8ps*lOC2eO)@^b{58TY> znu_WJpa|a9CJ-iBP*@q@XO=g9&>6PK^#R6z$d#krIkp2XmNAQuJ-Ep6d)xF+bArL8 zeD>eWw3DMic!FlHfvsj)#Z5oQVfO<60+0-sGd#5$=!yJwg%`AcA zB3tgF>tB<6y;tvjS^jW1gZ#C0?1M_a-5c9^oWa6)hZ-OmlN7L}sP3BrEM6e43g8=W zFH}DojERA!O7B3M!ySs=cnza{P}@vFkbt^VZcas=rD zJE`WIYvj_dKKr476~MW33w|wkUt$37FDtT?O~l6598h1r-)dolM? z5LRs=ydTcKYWC4JO&Iz#vEc?#vD zODDg8!5`QQv}U^&GGAX`sBs)U@Yv1{^2g6}!AHHTi%4k^N08T`{Ph|~w!0BzJO{t4 z7;3?YltdO^+5H{D_4^w^oDr}dfPO%*g$G2upT7ruN5TqgMu`Qy!TN`2RKJMmg;IE> znFVt5)&iJwFu(^ek)T-PLkTKfQ0P&+9X_9+z4{oy&~k{}uba(P)L;Js(e@Cu8>8hA z4p(PsUAFd}hzG%^APfP9O1+df61#kE$CSWSAZQR?aqsE?s*UHz@2l(5DqgiifCMKd zCW7<{uQagMzCI%MAG-kw>!r^-k#z33Hxu+t@{(2$g|Rh)RE?LIZ=@cW)CFOB|7)OG ze|~hFcYRw-3wp;1A5%|we*aks+7wVSB%B5vQHEGvd|mL`>;!Z4yZUaoY;KsoXFi?2XeP!(F?mlr~uSz2p8h1>T zHl;)AqTGvCEBF4QyEOlyQYi!)aqVpcBpEaXhMpxt>;G!dg>nGmPY9gIdTaSH?hw{d z#Be&Jb<~jX6}7jqq<>HQYCA`)y8k(yqzV|S0fR9D<0h{KLKOcfRq$N*i@TtP@r(m! zeXC1kX}T`npH%xnA+%b3Lf!xp(E%xjW5o?zw~o!s`J$r0Hu^#SjC+ z?T2oI_(IM8ZN-XIj*Gg_5CKhyzYEFWI(u6O>8u30(tHW@+3LWjNJY)SQLY+W>yhPIN>&1;*)QMLw^*ULG> znptKiqCK5rdj<;5x=kGOF9u?fI9`CoA1Yl60!>qAd5~X2Wz+$&zu4{9|D7@L73zzM zUf5AupwDklF!Pm-gSiEJfYv$M9TxF6p6(nMu~i}|E6T=z zoXNe!e4G$5D{gJ_l6+&@o`y|)cm3Ojl18IqOOD!+pKp3f=Jux^2 z^V&RQmexh&GY;T)Gum*dyLM?_>C8@$$V#FSkC6lIYAH96L{R|Qk}Y@ef6&E>W3VP9 zW;>UcoEa=bq$(56insGXHT>k!uiKq;3cH?OQc&>LAunmxE*)r2I0KVmz>OWzQez$~ z>F47u+LFIvW3vEdP!%P|wXT+cBMaZB!(Ht6%rC~~t-4wn1I?jH_IC^fg@s9lb7(}} zyQC~Z&ufu5M1@LZIewgmC`i%oL1-|tPpy!5gXskwYZx^Fe{%_{1C5&f*_JM6WqDi` zlkRIYcZjdXyB4Leph!}&P9>xV&I!t?VG6>~T$6%Ktw_5+PKAQlUX`As=tQ;wlg$<# zoqKZBGd+GI#^XLlTRXKo1n{g$7hBDtlYhvi!x4*|I~{?@JvRJvLM5 zLf>&ZPn$G!Di*J$8A*U!Yh#0DzL*K>{~)=XtMM7; zPgls#@OR=TW6jJ2s&7*?p`R7PJcT^6lBqbB=LW?rKBPq7D6@>eUxU=J=0)N2ERT<1 z1?awsM}tow(lx~bpHC5=J1@j1pRd%F&d9J$v+(#2pK>*OJN zly4~)xk&4JO42Y9bcB{dHH{cg-_tZ34IN{_)@Y})%7aMXJ|?f9fXcO_9aj|TrZx0hDX&IzuV+MnT*=n?z2e11FdUY zh~`sbofjxK^9L(6r{{BCS2ya2jGz*N8Pm4emybW6EM`*5II1=xAm4{OkF|JPt9h-uucT)U~S*?gKZ zUHWr;V8_-QvP;-s-4VJ2IHz-8nTg&^qbkFnfI^r4(u@uc+mg?oO+mHQMbI2xUDh%q4oxh;A_0xmD>9vgt9Ln}le_yHXTULRh!2-QfC zgVc{C9;>wZw7=47rn`}PzIJ_cd<#p?^0Uib8N>EZoU7!XT&gKW zH1X+;OmzB1&C?Cg@YtYx3hvAhsBGHtP(qzw@6K&grn?LtiRHMOdPod9 zYJe>RaIwl+j?Lz>t>_yPEUZjSlarHBk%3?GtBO&8BoEn)%H(W#mSMH9OJ68AGl9Pu zaaU`}!#y8mF+`0FDgV`SKnIMq#^_wYcs=D*nuO!7|k}mM>*3uHr4U_HCvl-A|js;{#YU zKoPWn6O_t(23r%cDYrCpLQveAN)>$bZ%EBIxX}!B4Q^L&*K#-#$lGS_MtFicVQ=ES zSw&&Ad9f<<`8#<|(ER1N-ItQ<@}Fov$dy*%!!`gV6h8#znXQd2k_Ox`SJqy#Qg}l9 zxvI8bS#EA?qwxY8G^#KnBD6a^x_Jz+U%@kMthw88SsNhwma9t5^sj}`4Q|6)X$&{c za&w}%0%^!h>QxGY=<$zh zphyZ1o`ba~xoSL{0AhxX^-st9AvPlFL{3};zmg_ypO(%K5{+f*kjE%k<)|^zpJDb=M*phf*5PNvnWTgH=(&QT}xw=*UvrXafk}q(WA#bSvOuHmf zAEb{2i>44PfFv8p1`1OyKRS*`P?3Zf`e9l_U zA_Ov1p%O+`YguacYD${9?fg9uVcB;Nzm2T6r zrXPRPy=~b;MGfijut>We){g8akE-WETK;C=wOo*9tO&7+Fd3h_22179IWSI2C}ru?=@T!-OLZb+TVoBac==3dRBOZhmY>7eBFmIs{TV~A>G%k% zk8`iOSr_N`6M6$ao0p60DY|!_3S(=P<$qDN!ka*c)F_;10q8?8xkns=oJS8h_(St4 z5KyYN8q^t7gx#Ba98>Jq{g}N+nL|>s1GqWpz`0?%foTjQMYpU_+V?Lde%RBPSE+_R zc`;nge7j6iyJ_?p+{1uf^Q&R)9 zv73`8ezghspSFsI7OyNw0iQHm`H#M`mk05#cxo@vEN*CPdzB@=rO>pOdb^Ska)ZMTXeeqUC7IW)wob%p+e5Z!=qhdZFB}H_409Z!}P+pZ=cS@{VpOYB`3iZtm*7ln^<>B zK;6>nDz_A0gL^RTUox{O{!Q8=M*-sfdNJUX^sG83zj$dB&Pbg|W1DFpgxcc|<7hWtKI zXh}&)$;d!}5h(O$T?ZZ{sWulTMUSS*0s%u+O)X(lFcnCPgUK$1`T44L=mh@F#v2c` zfYPs&ot`czfO_ zLPr2M$8e**%*2n2%m|9d1I@Fvw1*`5s@edQFoE+ohy&iB)*2q5=uw< z^C{VB*8Da=)$Aj`Sl7u+tn!1+QkN$pHmkr zs=(e$35o##!oWVr-E#R>b{rIK9LC7DmhGhm2UlIj$+6aqjktwXmY!`F_!K^G!s4BYQhDJL^WmmC%U( zfsP{;hz^gsymj8+pU*>`Laec=WZ$ea9wSj0x}pp<-LcBE_D&v&h95kQM`V-W`vcwI zP4EhpO@prp9&kpd8K+t*a|PlF>6o%nmV2Q+5=XGtV?t5`oc6(-d1y21$V0hiYDQF! z!zm+Mx+0ocs07m_vUId`b(^<3b}h{BwD^g+hzUVR2o7E6Ro{P3|AiUTkv+!L+hNzj z2bZlxbMUhRxL_2^bQgOCv>?eP5&9=3I(orxAz-rz#NrPIi#0KfotyZZtXE+u9}9DG zJLH}rXEESYLc$KOljPD0m6N7)a^L@*GZc|5mLVh(%@kJO!^q199cpSD-kB=VAn2=$EfwzPyQvtK@D8-EVeV-WmqQ07l-Czu|@>N&jU?x#qQN z)eDg#6NS7GVAq|3fTdAEDq!`(9hsCDrXF`Ey|ugw+UAne54UHUx`70@USd2%eGn|u zUHVIKfRE2Aw$?rq7#%Pg1mNK-r>m$|y^!t?)F|}bK~k#;jggB7#m1UaBDk@-DPJj5GwPXMF882Aq9M9q~}U~20gNB(%ZP6POH)U z$y3%YOQFDpC&v_N9=73kS=NF246~c3LZA&YND_wb0l^u%nh7vp5c=;IumdPil58A< zE6=6b)?cuNiMGJ<5Lo-xVNX%R&F#D!`{*JOEE~z>C{CgYGDB5t(58of2sXRyx`?_o7Z$$gX8=9qHxmKiqK7l14_^W>&HPuTWTKs{Ak=Ixwd7GdgK#0yoDcpE{Kb!7y{4!L+8ia$ghqtU!df$*KfbjqQ zH^q|cAyrVwftQ5YDVy06appgOD7~Byv(1o68C#gdTR+qo$DGkX%{Irbai#d;`4NzE z6h_WrEXRUgxk7`sQ z@PQl-Ye~;*u}4dPQ`tGa?z3l;?{%mYvCQm(+UnSc@F^&WW4Bx5e;uNO*ruGO z7OM})0ed34t+>awEj7Dizd9%Ab)QqU4otRc%rlgVqTz#oeMoeARav3D z&Q6x50Wj{3nGiZ4a*XoS>w3jSiami+gc60XsFVycbFp68ubmp^E));MfobGJ z+FZbU5bNZd-?EP2mIet_mpSA?@gE?I3Fwh2ymMSY^z;x76iH!Nj@p&iTzCow)ONS$gOy-q2RHY&om+O*VetAu` z4`wYJaf0R@TLc;=62C;8gz-OKXS`5=*1H)T`h11t3#**H3M#vwi2gtnHjQR-N=Wr4M^d0+1U{0oFOYi77V~Goo`Q)ATrkbL;--~Mw~<~kla~=i%M=c(pdQW zttd|vTJPRySlVhC*pjYQXK{$zS|tlmUs^qfedhKxZSSm_OLCt6WW@_(;kyUR?9Ve= zJv27yJY-YB9;@g;@v++5 zM?QiZcZza0W_^X-B?agEVg`74Nlmtz7;5Zua?fK8IVTP`<|_=@seh(Baxr{znz;69 zl}9*3n>9vE6$EYaYK$D>bEW}%-3|w+rSMwnT`mgxC|xe_xoUnEm6X5^Bj+$}yH4ZoP2C?kD&8d~mz~<=);(#AzsWDVgblum16F)F))T>z*X`h-)k&twJP7Jcy5N-Oc`5?}k z=MuVZ8ykMu&FAXX=ek0)dvTv=G;ixGK5(yqei*fI)`}ugWSklSjgoUs#HnExEB`#7 zq({*GP=;%{M}KO7-XRLC9b%7Iz0Ep z#B^%H)gu)(!?Ex$<^~(Suhed~a+Y~(*d=t~K3o#&4~(%$WW=!FfK1-ho{8?@T`s)E%WX^H z2^agTq&)G|?cXOy8J({InEnf@{fhNwkhbGa#Yl}DtA*fjVO=dJkB!)DF{6L+m3PNUKLAVv9R^qE-m-(=kn zGV}h2OeiiALZBz`8yDKk9^r+!{7G0}>Kf8c| zR=%Mpo?c!l;e!5Ca30Ih%NYKW`6JM5=C*hUlcs-PhkLYpe#vI9@cOF&+pvcZ4Fhsx zE?u82S6g447vJ&6{AcNqn%M(_7o(WZJkUjpuj~sxV_a2I0DhizDe^)NgXjyQRfRCh z{t+oJObBINvNn}CPVZOQ_()&3pf_sEGh5KBc}Unw&(W~N%4NF$n1k0^#xVbbE0bkY zA@u9ZwQ3k23QJXQZ|~8~7ko3Y%Y{G;CGxJ@$c11AG z$R*Bj{)xrXy9yktu(gI#uR{r(p)A+@`|qyzz|V$W;?$0mcf-6iZwpS4Sff2XpQJlg ztw9NqQBmPr;^hxh^>S^3Z3(AjwP!yY)(x7ddJ??-``Fk6I%`6m`hD3^7iWdf?L}_G zbsfgR71H42Tue(nooIcNlg`hETQ+`Jj#4iDEo|5cuYOrJv1=bW@em6GdRyCH)Bbcu z7pVj=)YesBJ;R2t7sUy%=%K~Kn(D^7jytD)yK)U@9WTH9K74T+PDrcp< zKU{R{h+FYIL*0z@Ao1#R{>6tYA6lu-Uswz=X(DzS6tO)#qxA#=VcrI8XCRyq!^6e3 z2g}RN%{V-qL)VTtrWx^`AK%=H^NIuu&=s&5eLFpLAVJ0~HdsTQ&bj#HWG=tD-8++>}#g_l?dA(N0njnkMtn{LOHJ2A4o zK)Xk;spu&qSy#u79Nky_9YCLD>IVuzJLY_ zX$2FbS?QjbVi?kGRz>$hVrahDP@ojf+CLS-jQASqN%DglBKL#`&U!_&N+Jgqk;WsI zAHDqjyWNU;;SAx1Hcxmve*8oRP6`Sr;VTXCopQ=5}-~bmdoQ4 zy|2-?s((7grusXhmM*&GIZguQe#i#%&uZKQQ=-!bA74hcp>qj__RVEOb;6{uiK!`8{y!jw(T}!IpL|1d8vwYr)aAKg$Zn+9;II7I&2_!yTJAx`GPh!0 z?svrdG&r#OtcC`K-6o7ACBBEpE3zWQdPGwzNzd1yRgx~X@f_DDV;Eh1&GqWl!;23w zL!D)jxI5tvA8XVZRmUSk*v$QS!>Ej8Y1^ofu$NC0SnIB>FY2SWp&BgsQ?$U)QW8!& z{<0cY!NV=I%(85!LA-B8l0JF^cGIdVeNL8vJ~<8d;DvZ*s^P4K7`@|*Y#d1)L!NG? zZ+zTu_4(!$*cEyRdRO%W);jC~;4Tp9J5+$C9sBg-wZadv?)Lh@!*o zVsAY3h!LlTd|X3kdwY~m$>Yb5>n_DM99f=713MEB3146T8>HV93Ovn*f4&)Ml4gv3 z%(~x;E%-ndEkHFiJlFtJ)t*BiWXUBe(Y!*2v@1(WORE^t z7|CsPBq;v%6QP~&UpXMJ-E6QgdMgd+VqcCn9xX z&D(BTS-o03f?FZZKDsU%c|KQ{StBxm@nr9jYH{iNxJk#e_s(n%93Al=EUrFt^aXLp z|MYUcOPeEO-RK>tV=-598@G2i6(E7rl0PrzZt~Gq8uNrZQ6%s_=Fz> z@k<-as@(V62(*2d(0~2NF7A@_Y*47NFn&>Fk>h5MXA&J}*cfqjdv5&kV7DrPQELmg zBtG#VZ1qtiIz{*O-snEURVkGZHzs>Ut%e#=8qnSRC9I&f@=VLwkaXgXe@1kl zlqVLA>AbyZ7y`{SRcm(q7MVqf97o4_8!awb=0gS-(!glT zQ`a+rO*r-uJ~bbkO?b!i*z7f z^J!9VtSSF%0LmMknZ)rjc?{0_K}rttYLaEn5?%{}8A)v)u*9n36BRrMpAzhZ!`Shf z5f|jm+^*9H-e`w#;)}bBaBf~}75p*|>2}skZ!odcsUc2B!T+J|JWiMUd7D-T{yA{XSR6ITy%h0XPU|p+^_*l$go(~QU9dJEs9c) z^9CTHZ?@xRRmlP&GyM`8@{zINn`0Ee54{x=0RI~&sO46zu>k4QIq;CoR>>tQV5{LB zkOO8rUdSn3iFNn$9SUp-=kxK%TP(8Wt`ik6S90`+&sFm8XBA;0!|ECJ4nX460V`Lnu#yTM(v#;gdv0Dr zxBS>A_em`p+2FL0E%xt<-Chs3Scfe=(;mK&MX-|vV*nd3E_E>WYR~f^EJ)C&^P3a3 zCsg%HiOkkkUPeaoFMZ(Ff()!wT{rF%pHw9DUBE2gn==ZUy5H>q3)D2LH2WQSW5;pF zM~E?xd>(Jgp3Ke$&f|hY*|ggAo|srD!lZ3`V2yH6x_`vIP+6G0qv z5e6AmSh%w*zTVOl0qHq53btP947_Ot6_m33rHok-=g6(V!8LXac|J>1^qYpE;RnEk z2Q-As@Cw^pSuyohw4iUXlcTlirj%MA_feT@Hh!^JMM|@FN0c2rYe-USGY7uq z+&%hR#r|;B@d2=6(ykcFrI^TX7LUwry=TX@v{+quOKz-~*j8V~2~1ps`QWG^*58cA zE&mAfqo-#0z8^KhSzktcY5KP3RoG1=dQ~-GE(EJqJ!DP0$A*@G^!iI65l zwA&%_jUueqV0&Q(=Z~m%qV{4uxEe@M>vUho1~ZX6&fyquz#gg#0mx{|Df2YeK&v!b z@`%)D%g9y}>yuud+lE=M_wb*{J|A0=W@Vj@e>`yT8YadhVP5`YJvkHZiNZ1259SaR zG7)zL9o*_$Rs3eKh3sDBlHrld8%WM<^9>|_dB2UA=kRh|DJ){tW}9_&*nN?C%5cZa<%6A)}o8w3|P^| z7yMdTzb$M~)5kXln8DiLp|D#7eO&OrJ_-d z$iVe5dtQT+#tFRLKL>FdAZSs4Ks&5PF&p01&8lqAut!E&aI#3|4xVJS2XzRL^ zX6|6$ad3)8VzQJaS*6mR?<4i0<>_W-1MhkYlhJqEt%T%OA{|koYHfzI?!ee&eVJz_ z&rfski<#zK*B(BphgPflB?itvb{GSiDu{x_wCB}WKM1ezWbeg`4~GSTXVOsrbbSPk z4&E>N+L;=sa?y*GtgDEd&~UEE8lBiPv@JjKvfNNnkW){i0vg#TNWyM`7dKqo_(A+;JW}8AqP}+J(mC^hq*N6l3cX7_aB{uOsH!vksRO`MC<5F@W&I2Km!*dfL11j9B(f+c^JW=7G`@USAkfZ2(W}0`uwThAs+uua_U->!_OWg6{rtAO5P=Wd z+B{IHT+Xx=^=h^Wdje8ItEpE6yFcAw+}qNr)JSO#Jat33J|+%gfxhlLKt!)N4~I4J zO%)z=4lyqN{W}DHs*^a-$7Y(r>RB@r)D2X*D{LmAX(aI1vmB-<)?c7ypitpTCg=G3 zAfie>K2hikar$I=la^uD9-hx{3V@hhKRhmdQ72UX4)gU3>Y@ESiIeLuQ$$@vxXvD4 zI}xzoA@Q* z7s!i8o_8JE*x2CmQc+UkElOf7=1_53AZvvkH)uL_yyYTcBq z)?mUhcts`0EJXJu)9j9zWf}Cna4T5{7`~qmZa1xK*26oyOJH)`D_W(DySuhsVU>{a zCT@h<$=@1UWw3hA#{H~KVpoV+JFEj~eAA7%+4~!sx68R|-F!yNjb;6rI*6bmufR}F z)fxdFzq*)g9^eGf2Iw`GGfbn?Z3www3(I)nU%sT|;u^Yv;U|fF<}|#4c=h2k1=*kI zSQmXCvJ{*Ez{?~gln9}a-08>rngiXwE<-H*oRP!Dh)cRFgt~-&0fhud?X_jQx=IcwWJ=qb* z_S?emKqzd9MObQ}c{g1N0N#DbNDxMr3U(;D zmr)BYi+uFS6p~as1}U59qhsObWlYh1iw#*SnLo$%9~OoU|2y(Ga0|?2{A#8tf|B6w z^Q@W3tDax|(Gu|M9nXdPHah*lPYW*cOS1iQp)|O_eVDK$`2k~gJTy=T4t@y_`1?-+ zS4OV<%`W_N6+K`B*l+?54ACmC)3tLZw|rb&oH6#w9c{&hXJCr9Xbte2Zn6?bYajCb z&x!u{0?A&EX+lBh0WuvE9Se_iMz4JB;y`8`we^?0UkxJ+bBfG4k{u!wGk;vk9kYCg zf1_U|*$7sec8Ib0w2rd79T-g)kIVnPs?4ktzBCL|JS;OZx`u^c;uWfuFeu{U7s~l_wEVS7)eEb2;)fQD85_{O`ze+m(Q!qXS6_s2GwIj1)4~6ww!Fgi#R6WHlpT z{5HVL5)NL;^a;9|1a$~_gS^*Y_7hqsG$VB4E3aKLcie9D)-pi?+aW;Bb2XZgkHLb@l(>Q#m0awK<3`- zVZHur_^jpg4rOb&vi%%hR-*xn(@*s5^ax|+Oh0xFV*G^;KWG9WY4$u}7ZR}H)WVjY z^P88de^P{9?EAYi1Xs!~pP%IrhNmOtI{0Eb#rHhfYqqC=9QdC-I1d~Bh75SZjiGxV zHJ7p+8&n8PVG%*YPmI3Ukzy21fGdHR!9D-7sW&{Gc zRzBBneAe+Agk(8pW6^sHlAVrT4GycymI|-g8TL)Z_omrx*8ZX)0H+56JxR1zIrq7T zUJ|X|2_Xd&w>=OirhbVN4}A}avoR*=rRj?}ZHyY#EKOeeO$3#phi*$I#t$ztl8Y^v zfpaRPx3^JSz!(HI66D#$GWwP=_Lyw65DHKMq;M*b`!<{0gmhQ_O1oJAR0LqaNLj*J z$E8DsLmI<6;HR$4CAYhuy|SgZ02Qc}%pK}?clDzqfB2E|KRW{Pk$u25sds<4A4y0Cg|jZj8uq>Yd-|fV zYN9Abh*MXuewm!=ErrmQ{wG;-TVTGQ#I|UcU(=|}lF1GuNMwhXIfFJbAz^(JVgY~% z^;R+l#c_tPqhzq9>Al|u&Slfq_$hvOoi3xYrJRggON7cp?oSk1%k3VEYP*6yu9*jx zF|7tP+@2hds?NsDK9ZZ9hCu;we2f}a`zXx}%zI+ego7^txRP&+uu;pOeii6>n#dj`F^f_&mJLl-bUUq7jJ z7^IIY?cXbP(U$qn$)WGK^-Apgur1u=dS7c{dc#~O$+t3Xf00u)pRwtbji|EaMM z^T))DNop$@Bf-VdDzPbl|m7fWK$R=}V111j&rD zkYQv*IidYacRZf3RalEGh!MEXM;v=Cg<+*$Q4N;+4%PW^3t<(LbJ(QoW=|8CIlD5^ zp*YJAJt~s^x!EMC%Nyk|PLCub7tG1Hm+2F_@h4uP!4>HEm*i z7cwQQE?JK_8&>pDF9z`DPK)vAk4!ewPV7XkxaB6X3V9MkMk}T*_ zFCjQcI0a*90h*JNf@hPdK*dpKnw0EQOGyq*J=fgjv^Eerh)VgqGXepQSc{@``?^UcB9bXnCl(;olThuzQKrz6D|Y#Ci*WANS`)wbP`^i_72 zb8baN*Dfk25glEb(CF;!`p+Z|d_l&$PBwg`te$V3r6?O7!-`3wT>U!IJ&Ho*r=-ma z=$97{&Eu>OrBvj7gH%IId|!M*b+j;OR$B%R+E}22&&nzLQhK*~Y%r3k>8>!_=l>m= zMGrjy!N1oV!eu$-yC>JPpPG{O88;N9K^XjwsQ~b(V;#cYtB$6TE&npW{UVJBT}P%#T^P?-zxq%#@mnhtT^f z0#7_tf4@Shf<~G0@Q{*j}Nm}QyMl@e=hh0zzJu=Wu z*@+%WC9##fS%;L87_f3v`Nkq&80C-wP#?ajNbL8nd2{%>DY`&Ln~P)RGfXhl0m39C z1O1ZsF%QM?4M?5_Wf2m`Q*ZJpEW{k(f~JhJ3c{%K+r~qM;1%^}f*p%S!=bSJ6TbRpf`OSPbCwNV#Wxnx znT&E`+f!(bUKK-1HbJD>Zm%Ad3a@YuVpz6Qlz1~^WoOutKQ5R3$UdP?1zcVp7&AgP zCO@klw3RaeG=6lZ6yn?DXnK5zd_N9YEbzN~0hPPWWdV#lR3X~v{UE@+Rp5`IBRE5e zibwN7{*vdC_qmS8puPKTEWKv zhMjV9%NstZJE!eqZ*gRP*W@J-%dC|BU5*g3xBagH{ppy zDtq@B)bl*wY-iZ(@!{!=8bKGu@oWaa-r&rtwcn>p-g1y4~<4*+(EKO4soI z`iT@u4y(Y+0OOCRlO`#9ZSLCCoPyKAhUSw9pC~b{*?A?t6}qN_=&*YLr*vO1Ld%H~;xYsD8yXNTWq{T>`~F{tXn+P?$V&#gxsf1< z3D(i>pP0x8Tq=COc};nW+)*P;zkkQ?aDW~E`suY?1tlfZA-c&W@u-lHkfB~%PBJng z(3>eKmDx5WBv1K!czAewD^&EslcHD1KI9D>`QRR@$!TbR&I@9Y%#Xs^V;gpSvJsRT zs}aL+3GJH+`=mIsYMh`z8$x`4z7J$nNccT(8eLyope%{JzDOmMa$5Mig6(8U-*%}X zxxsgki)7t+OEC1GspkA<-!u~r0z+@_4id3}y-yu7T(QCF+)943PP~E{$U!sR857dh zHCp5q%3x2|=VcTa4WU#_kUGx%Iw$8yYHaDjv;?5w4Sv$SGD$@LFA{u`<&e+LUH|x6 zqknDg@!sO4D=bU&YKv&|;vF_lz80~iy1@mFergjWsS7zE)pB|Ka{bF4))C917}D$= zRu##QdCT;A=azbxqkDiy`8P+uZ`gKQTgRUMm?Lx2WG?kIMl)qitL}%cGR6nh7Ys^T}U-X}7avuAt_#!MkJbbG6<=kbo+sJrOwWlzu>ZR5O-R-e33h1u64$JU@ z4`nS$b4`vofj>ljuF-G9nsrZAfJh}~Q>3DDmw*V>q-sfdrsCMSHkY*Tz_aw@^*Y_Z ze`UH-xt`Ui6kdI8CeG`j(AueVB8uYyvRw}yDqrgJ6OB?u?rw3S+iR+P$*>o`SAk<) zmcXz<*V*wAeo-|TKO+inb+IXKoECF!GDwsk$G~d6eEE{vd)76#4R(5v6QBR|ySite zy>DVzlk})>Z*QOZ((galK>H3Kc+mMh980UM?5ywe1C<2{S~CgC&3h^DpUB3QBf_|K-(u5^D*75ckl_+q{FXr567Q*H zlF^2hR!T!V2$QfY3pw_y_&-4F5!TI{jN=Ld>xC(~Qsm4+-?kQ?YsF%P-=4Rku`Yl1g{ouf-*B7EV41aG>vgCbKzcn;0ta|(1?vRu>uuN52 zcX^-!YN4rmW`Z=^=Qq``CM#Q3;*GDmo;&k8_yC)^_X-c?n{qq_oB{m0oN63Ojp7Oo z4^R2#6`b<0J47`&io-&!_2`~*n^sZDfOl4nXSA|#r8|O_eghMHKpVSP37q>+^@P;l z>IpcqRIX*3ne0~VKM|E3F#J_#!Tkl%bc`9MV95{fElS}z)8{1iX^4C|>E0P`? zC(Jho_QD7v51G>}g-RC3JD*TH26qt>wU!>|etYm_nKWMHQOqvdQa@MYL~@4b({p3D zSh})bsiKLw3S!i({QIUyp>sP~kyfW16nREnY~>vT|5NfXJWvt5HTaiYQ*ood$0x>* zs95^9Oh?j4L-N7vwlyLFTL`V)B{v;L9u307P*7E4+E{^cNl@PCGnTTX$Od!_h`KsT ze=7&@SLim08wOi+;MMehQGZcH{sS8xv}t>?i2c=Bi*daNx5$d57eT&G_hSwf5@_yF6(S0M;oE8^!^2uyAGv@|_s)-@< zugj+0T2yCa_KUxCi?H@RM;>;|MKj;w;#IhD*i7EABB)s-nN@i11r#}@^O>s-ze@c5 z!rA)xwt1!;-@YoZhH~PCCcP}*nV@{$^ji6T|NL2F(ymC*?q6z~8Z=x*V)w{o6n(x4 zNK>dOf%Grs7s<`J?Ktbn+dax&eHr)zKkhcq2k6$Ee*Ym9XNRN2u-o#(DgRn+QN?^B zGrNAW_<1cO4ST{(rbB4!Dz9XdNCRWE;o@_`D`uRbP*IEM`(4z6Kh(RU#NSGV*_5qY znRKW_Ri3X!wQGR`KSQiyds|m>g10q6*$?p|DZ2pO&TFNPAFX--HJ}*2kHjjlZJV>l z6P{8V{VwB!wD~o^UsL~d5NEr`Z!eG4H)Y~D2i|p|tFCGTyzStr)TJKx zCeZMrUwcAo1@hi>EmE=u5N458`xC57<`Oy$ECHOIBOPe)J{BoacpXFK1A-B;y!zf> z6-Cv;|EkI4q%1KfrKI$P*v?i+=Piu&>l;9X4p41#jOvLj#eU6JrRTdimk%lSe=LZE zYr6CXuC`as*difuraat!0ktR2xKZXV?lSsO{%nr&K*b%ZwIz=bN;FaasSl=zuyS4^ z**E!GS_^cHOuve;)#S9%1{sAMp;p_=CbGdI`8H6MT*;4YDk9SMWF+L>C~-(!n*aLQ zd%tMYmvJUCH9^Sv$w>lrs0ngWBP};R|57@FO5EiaMjig9=Kt49(AzobIS3%%1E|Q8 zj?ULcVw?`|n(J8Yh4VP;Bx1b1@5csEC*s?fl`X!nwcIpwWEpbe2GIhHy!Tpg@+h*e z+QVY5Tx^7TC`(JzEclGjhsY~=3MdOG#6GJ4+y^Cti&z0eyBIZbf0vcr>g|eHoV5ah zI%jY*`}&F}fg1Ijt*v3g(v4KAnSt|@K=ML+TI61 z4v0{az;Ax`X4J9^{UI$n2lyn4d`OwL#H|1U>UOy78LQh&AWz-P%8EowfKE*b$^?E! zFDoVYLNOjbK6t8?2Yw(gs+Wr!3) znTvG;!sfun-5FyW(6=6U1PlpLpTPPcqb%9tD(#vLuUfIfsZ%R(9tj}g>H8y)@A9OC z3TWm^tli$d=|2v^RKdESFJPDDPlauRx*OW|Da`${;6>n=?JUl?8y4QE|q629tck1y+(HBe_nH z8>0S&r%E@z{Lynb{$2gBOs}@QJ+6Pe=C`Ypzf|znPFpaD@F7Srr1gaNyxCv`pzF)$o{6?3ma>A@~~do{79UO%_Qw$ zIao91PI8&q`?GU^#!}bIFn54;HujL6X>CC8**UDsaKH&s#jZMAi|C?3-5bfAq%2Gg zg?pE3SzmgXWxlzY_lqtKsa`H-is&~toFo=Wao_g*M`LGN+AfC_Kza@pg6rWQI?Skb z>i!Q~{Q&CbH2syyXG^1fr#Huo)aKlB8Ob{hU-mg@V5&dQpZ+M|qeRhuY-&1y$7e~? z0`o;tTVp5htup{tbE#a-h7EH+F$t*;{ZlD^bsedpRQik+vAF=$IM45ZR6IK~LG8(% z5iDK;0^zm<^I{I=jj)G0QAu|s;?T;m{}UmKv_LWFQCP}2a~2NI_kSM7GH9Pm_5kUoLA#`lib z+)#2|t*c_v2>=m}1$=O4n9Buzj5X%u4?@S*=QR}ODv|c5e1zADRDUXr)YlP-o=qY~ zkeYMh2U~>b%Jv?fGlLSg3k(DbcR{Ku;6I?ir33E(=TQ{{Ww+RR1t=7^z>csJH_({8 z9lzD|TqOTCun5()CT!|W2+&$FQ+nU(n$}Z9RhJT%o00WQ9pMaM}xF4ST97ZnM2seIA zv!t{{jfXREIeE2Em-Y;5IE8z7UJ0@x9C#6;5(T>Mm?o5IRC}e(VHPzbkUKUqU5O#O z`j_00gj-8-vG;);K9E*b1B?zeWp4f*iR#2iu_XKa> zOXmsy(>9G5WKS2c-fdTJn*MOAXXeoMH;<&G;WW9~0*jKx$S10;B|v#3r9L}t;?$PB zB=Gzl)&>mIq~>qPXHo7hs?NY6C>xvu10dJymLc_oz--VhMjXq{WfB_#N=pG+Bprs9 z)}MD}aartbitfAOD4)RrfdG5v4Pgk#pn__BFLzXl|VD|Lkj4(-sl2eq2 z$kToMY)fus77;rd z^rhphZSIn8;Ez z7P)kbdEHnDwP`K`o}ouD!PdU}KGzlIKzR!EhKSOpJlnpNl=oDGiK5--k#BC~FKql7RKhY}-n%;~z4BuQ%^#WN}?~u7-X23+AKUEH;Z-LLrpwWkk4SYTJ zNsVxone!|hL6bNWYQOaN&AA zG1Jk!YPbG`wyiPRrj2ZXzP}_Mi)cqjIy^)DZQmah4~=u->q1yt5paNe_#F_j2>Se{ zo92Zcj&o**G=Xld>y{6`d3k8ts{$r6!k;x5+px4!W68t}^rzS({`>C@DfTwyIz@F7GX(u{%-BvV^xIo4ij&&*kaZ@|7V1E&(=pNXg% zt_rVj!Z?{9FfNhTr%(5FMaij%<|j!m4{6Y{s$Ru;r3$8?=gYZK!>0tPMJjJVGAfZdM|=pIi%Gt1#+bFF6itOKU_tQS)1n00wShrDO@SOBG)=uky+=l z`58HVh}WSkWCD-t`UqQ;pwvxtUWW~W$iHZoC5dr>5OAh2 ztIaViEY6dYqxS>Mhoysq-M|wAWnBwh)lU+f(a`7XqQwOfMO)zu>Y{&1ibJ}JRD&r)F=k#2*$0d^WocS?I?SQQ`6=+g}hm-(6D3%bC{ z<{}P$5?704C75`ZO#<%af9~SssbAW`7g{&F4fZn@=6gLIKi_^kUl>In<_}z)hFv6o z16(c6{y56e8L2kpc0p5cs1_NaqXYXNdcVyr$?#nxoX%9bMV-W)1g+j^a-12J>jfl; z$>GphYwE*{q4q+X0BA670*QFCuS%*>#$Z(#bp+`|D(2+NiC?paQ1m!nm36Ip`~Nvu z|M4l<;+w69V-A_9m@e!qDCU5bQjc>t9*4*f@=PXRC>>PIWQdnJrrvVpHRJRBey4c< zuut}Ls`*{gpFAESG+ya81jINZV$0v=WXCIg#_`!pSCelk#GrGq{IM-QJ#*jsMI+a; zT+N|NaH`KI#~eJ*p@rOJ&`sS=n%~T;K0$8;^d&_5F35s=Je58~g*r}gfQ{Y8Aa}mI z4#ieH_~?bE520QK@v&*j?4bCM0H`CIu}B-q>W2>Ln*8Fr;aOyD^x(svE&lhz53I66 z1*|V{$?V66?-{mugZT>f;tsQq`Pg<`4y!GJ>Q`Nv1rN2)6HzE45{XQmxC)n4AruCh zZ)H;NHPk|gGPLbH4cRp-eUAV7=hM*Y@xQlzN~;nplyjTuxb(b57Ga(b0G;JsD{OpE zbbIqRK+6C{z8Yjt_0Nzy$X)gZ=7v74N+&et3Bx`79R^cjmu%l;{w3~rI{FNM+qmr3 zRuGq+=(-B+JomdLrY2$IZP_?FsSGNwkvqs>%IU~-x~Y;+iu@!}1hJVQ?CKQ;KlD~_ zWs{cp{woq``-hfj(AYWK`^k@=wz0jY@Wjs;^EtWudKC)z?w7R)Zn1{ zFMiOWM9__Pc732mAVS05zyQXD+)SM21KAPU1JKT9S3A=`jGC(0 z6zL*b11tO!@lyp%dkTgR6=73yeYPKZp(_M_-U?i9S~?pjv7Z0FA86#hHFi9Yoa4EO zSJi^iY8iCtE1<#IhE!IS@QJiB4SJjI>z#k|b9R{Xk0EpRfX`TlN2F|QtoPF!z0SHF zn!3&4)?d7?xYX}XSH^CXBk8r%aYk~|X$Z#2S|sS3;}SdyU#akXyC^axz%MKW60HK( zG*1jd?Q=8Lv?;uTHu|s-(#yboL@V@uj_2%97lVZfej)a6iyIGk*oLIv_uD_KpRFy_ z7g<`|2=W0^qn)X+ALaP=ccidES{1?1?3gJ+mLZ%KklBWulI$s0oX6(2KCs3JDc%7hDOjD6aw>yQ zujEjsLvAN^uW-Oy5O)oHk~EXrIKgWW?|xE7M*n0OPzi8Pvn6BMbER80FWoRp0Ra@W zo-EX3)IV|j-Bx-S7Y5DohO6kyr#bS7JwPeg0m zA81p`2zLK$wMV40P)^AR0EEE=>>7t3AhUE93u_G5h(&OO6x)5Or(|aSDKaUq=EzPK zP&xERsJyz67z?$Ou|sf1^3+uLSY3&cz-Cf8vK~P5u>W}gz}78J$Zlhk1LzXF)?_dC z_#BV;o^CL?sGBTvyBQc5uoF6;T$eu1>_^y*zf4g1= z-5$_cyNK^|_a(GT+d5PewLvNs8p0Mt$}24`g;s5|3&Cd9nvbB!LGZ3>oKF~9Y6I$; zze)?xa%vg>x0rmb#Dd61WpZDRb6)?fw1B+&o70XVV&gp)0%;0X&wQFq`dwS>x! z)0iffp2^&u&}Y6Wv#eR-VB9HR_};}6Y6}O1QCpvvZ_4G61$R%od%Ej2aYuB@CP^X= zXG~lLLF{7b6!-tU0=O0!#?tS%Y(T2{hu>z|8nM(7$a`L2ghK@2RVJ}D>_^gHz3@xY zzvWipW(V%S?g^Uek8LC{%d9kw!?%bM`wgfYPJ&MzR$b30Y`L{a+sw2W|NaxWrqJrCuIOtJs_5btNKRQnjGHkJn+suSs{xd~wngm@THpafFTQ!@7mbZg?|Hqw_9o?EW zy;*b^$3g`Lf>LGRybB9h7idfWwJt#a@Gif7e_TeSxjryyANn-aw{Dr()YxwtyVjXw z5ZBghmdtlFgKT<>o#L-6_RoAKd;gNhZ26;fQ;hfb%nG4?CPKq*#m0(I?SNvrzDR=` zs7T>n@HLWhO9fDlF%>U*dq=vL8n=2gZ;N2bne3z+gTOUXu=%Uq`~H9~mMzVm|1nZV zGR_s$r^hv=Rs&tnLU;iAFe_`5&5&N&(_@;TX1x+V0PfSx@PGvwJd2mt5|9YFk8-R_ zDDD0IiN`dnZhqMo|GljkKe+ISib4cYzrz7ZuMhoqj@&RpsJQ9yR9Ivy2p`}W;fRQa zt_k>PIBNnJJlmeUdk3u8@$USiu&sl&ewQ}wd$5J$(=XM1a3FIoPU?SH{%|3ivub(} z@+-GuQ<2DIQTue2Sm!d*E=s^qjQ#oKT?sOPW!!}=9Qda_bhx|HxK|xBbwdvg!Ej>X1D$sTh|)Gr5Q!wL!q6Xi7{fK<s!OD5R zh5%WHpP8AV*+m=!WHjk>@)sCAg*QA$&3U>;5^Yednl^UKV!#jWG@m1VPrV zP+0CtZqV4|wt_R`eB%m!grvi@r9}sSN$tHi0k1t!vb<+SrvjI^OWUf+d zTbe;QzomIU`1Y(PYJ}$LtW^_>RsqbARlx<^ftn*8-kSkmFO5wkaoSEO6#zD88k zsa`mQij9?prJBN)okW|UJbQDa;FXT{%J6t!xuSbP&=6~msJWKwQ#CA?U+oPs+_&^D zE(SPPDT)lI!Zdj&e)<8!Pyh5(TkQ610_=%X>AePGsEo$@55g{%lrgtFb55Q)FK<9N zd%j8bf+})k4)twIcgQJ`UN)#w0LvejO_NR-c++?TP=d#!!XP^6{V z@!C=5SsoKz1yFp?`IHiTiG?BlWMjI(pyMQ9UB_=xd?EEa8tU0)%`h?9yC^zrI{hAk z;=FGG2U6PA{qNTq?aNLg1`G?~9E3Mcm^xC-v^F&?3gs`xp+3}c0bA5!SG6TSMl2w^jHvQ{!wlIC%k)pUW&HZQE+R9!Po zmF_iyU)I&fWMp2YFzBA#3v`A${jN!e=qu>OaVrMHuTgZUv1NVvbh7C>8=m?uoLd~F z0eaD+N@Om4WR%6oXzb*B1HQLVoR}|o%7-Y?S55A7nwuKRE471LEs;%J+ze{fi%*U= zfy58Gm>P69W5BjQv~&?dk+TLB6{KCHh}kf9^j% za6_!x=#+{|1>k8oeuFEVILnAM;rty0w!Gc}2yE=wh4d@wQ*wm&qmz?+=XMXZSM}um z@K%yfBsi32&fcMFSaKS?Bq}{IX_UEOLH2j?mKm4xIG6kQewN;drgS(wb>0me0`Hn* zV;9I&R##R?eWb%y_fvH}a7lkwIS);o#Lk7MQlS$y)zNpRW%1Cx>-$w`4smF74g@S%hnf(M*Tl_<=^kjudX%%kK zB4YRa`}Yt>D_p<82~pDF7roBOzA~a^o+`=#@53t0>%Pc;3C{V;)jY(_ZE`E0i?dOi z)y;Qn_k1r`ZEbCpcQO;H-bpde0G)8MTf#8A4P@mBDqUmQrOq|%hN4a0a6jekdSMw! zXp{}#v*9BROD>*R#cg&SztA3=NaXKu<+u~lr2uSQ=(Sw*&?j!vgkXy3Z3snSy^WM3 zt%j?QY!Qx^qtWwHf8i~HYs09~4I5|h>srlDE#XVl+Z zZ~#T4Nkr4GO?~FysN?4@)Jio%SHEt>`z0Cw`1OAQpC`!V;y^s_xT-K`oHhz^|`ur&To9b&-dB0K~J@-9h#6F+U;uc z4^k0`lq`bDCJrR<50t;rT-caTllEgl0>1A5B86m^p{D|X5>|L0F1{0)K42^%<+~c- z?RsbH0Y?3kaZp!td!ftO{a;Y>;Z+o1ZeQg|2mcjBrdA)$P`QyS(mp{`Jl6=PN(Hdp zcEIvqL;a(n-u^JBo=_y71%LTq0LVqQ)uH>>eb7XZa!r&KhekoADY*!9+j=|7Zfx#? zTnUKm0&M%2&-1+p1T{7P=LQZOgg60Y8ApO68?KEDW@Z+G(#pm5SZeyc08_9#C;%iU zs%zc7U0<cw=?6_uVBreTz8QJHTMZ=eK32j| z|GzeLR?3$>d*A=%en<_+olPVI4H_PN2kNK)lJ8D7^V|pEmO39VDKj6is;&>c#B_N`d`eCf!Zcj?WZ%Ax~?C<&EJU(rn zrEBY=mK~aKR49>p%<0| zKL6PEwCOmAA~o5~@6x*7qQ~!;&`DooA(k$@NH5tXjuBkp2@#~t)V}I;WZPm&D*$ay zNm-~&xJW?YTrCH;S{7!@P8#<*E69!kg@t4t8p&dhAH zz?%{a*hy={0u%Yhxy93M4Ezy8uOT)Zj!%O(hMN2)&eVX!h;Pjj`=HY}zXtH`!4o#(Q$6 z<7>-Rf?M0CyL`-A7E7zne>CqXu=|3XOLT{s^rlX`r?83@vX|o9pp**{d{CVHaF@2`bSEDH*dPDXBP_0L$w!6`nJq z3D;<`9&&QB3u8k`=$D3HV%o=H1!e54)HDf9Jy>TTV@U-AaAH=Rmyn9&@8ps8Cz44CNbk!+*S!*056d%! z`l`HSz|M4dSaRoVh^iND^17_S?{aS_S+dvG+L}G2 zdKyP;;u5Vd=R{x6JotRKE8ty#z}ggS&U3(cnb_-mrLQj9_Feu$6gcRto(7t+5gb?G zndUhISyT)~M?Fq-p3~R*%MH^BDcavg+P{N@*-}8Qs}=66%#r zVVBZA=;f+1+VBa!5?VNMolH9X7po7)76k2ai*e~Ga@!9Qsj1;&9)N^FFL&^Fl^1h2 zsX(TZ@ldjpOk!T~?|F9G6=<0lw%V{frl;d7kV2TwL>(4dnGqP&lw{;VPt7NHFv)4& zE;N!cF|{3F`wq^(_gy2Hz4a8TF9?;@=co;@;CrgA{rB2g1Q5LQ_} zd*cBJ)*l!dBH+Hug9aNYifam%Lyz(`y&y(kO3=$?=SgY-VpdWwi1(mhpc_IdbKGPI z=b%Xd4?VZPttUbd#^x;==9t#TOjP>NVneUUX$h$uOdoBndg_1EqAxMxXGt=G_(lUZ zT2h!<2ysF{HM2KKm@;U;P}n=GBq$dDQOq9z_O#9-g~(mt<-Xu{BuFQ5kcdgab#jew z_dyn5-&9p9u1e03+tU=rRmNECUYs;QfM}PEaoPh zoN#gK_R`O?aMy*DyDVQI*D-E{5C14s{m4WKhk6|0jco}y8BWNb6zAg*Umi8%tkKXY zh`If5&-2-Z8o$f}y^O;bnJy4o3}6BkS0dCaQZCZ5L^a~C)n8?+_o zkIblO%`MHcGZFkQ^3=MF$5azjfEOoeuw;#xm0*clGXMrYd)zxq6;T@Rxf-awT`X~F z@MDx{&V!xLD6c9H+<#MB3as8`Bn{6a=`#OHm)1wtRVR^^puM`q``EKPu%hX3w~Rm( zCL+jKeA{LMTX9o9zAQ_e3>%*iylsLsZBxbEu;u9(F}aun=9Xw{YGNVRgy}rg!`;wP z3Tij48Eob83}|Yxz_Vv7b)L3tfjnhRXor4EcNR$cn85nY%)UJv@&Q$U*A8mjENCA9 z-kEr;hu%?|Q%4A2Jl!8c8V9bXq@B2pgr;VovdBX|VoM41rGccCp#Y9Q3FDue>hrj* zzu40zzSW?cgLAWcR52@)H*0!e^ejpujwT^b!ACaRVH{3m=zouj;1$4@2=y76>f^=TVm=UF(j- zjWF3vINTW>*0de6sY&Y!seYCzWGljLKbg~dY-21!#7tGEw%UQc=hWw;xu9hL3)Ek_ zlrBu0E|!JMZuOc(;YpLXL#)9Z0lVUxrEmy|be~p*EXO8Jvca)skpOQv;D<0uGPgv! zR&1FeA6bBFZ0Wa%@nZPrrrPU^XyJ$|j+hCEf{SSKw~|Ufp>s4lFc9_w*@f};1y5cQ$%5gRsEn@kl%HXnb-6G_py+``SWVIJ6Z6o``25x z454p3*))Wbp?}jg-~P&krY*w(@;HAoJixGXl2cLfWkK{}-fgxKvM8DhprAv$4G`Oi zPVm#V5NJ_`Nw8{jAxv#M(C^d(LycsS!Y<513C}}BYeJYe5?K9e%&86|)}9|rjtyaM z%laZi2st+j3d$sfe^{g%VA{Z0f3IC!wxR|!o69$#0b`J&?o3m%W(Z3K_yDQ?_#fXw z9hI%EtuG7I7aCAl_CM0weD?z+Qbj?*cn6%>Qoe0H2Q}?2F@RseaMo$TqcnUT=e1v+rPAQ z1Bd4FGHM^?XZ-e82D@P%lr6qi-85fct0LzhE}71Ssyq4(NqIfVtGA+Y|AYPud3%br zeVTr5EYj4 zg?U&}J7mfMhDkvsvDe_z4bi1nKV14^Ko9%t^3Ptma;3N$TKf3bQ=DD;8P*hyrY*P+ z)u;!=ZHGY7m=knL;nB8Daj^@e{~psvK7>?P7}X$_YFyh#_NtKmJrv^X5b5ctN<*XQ zss5hc24+179S!SY#of0*H67wRr==RYyUqW(|9%^a1ggf}92p&zM)E=G3@D(P)>ov3 zZr&6Wf&LI~M&h`TRdQt~9PCv*^jjtOqJryQL-YC_6cl>K&=Xn87otSSX}m7k4g~9? zUr*!aSvsoWJKIiK+?kB~s?b_JM}wcfE_M59E<@~-Ou*o<_1a~H0@TegS)Z14<$=`S z^|tm+072jukh}9Mn}BHNm7N#-02XTe;0P0iOXsQ0kCG7@iS$rVcte;Nb|%Gr;erwG z>z&(IO+^na;=`bQ7Z-h<;r1n!!C=Bu-*}nT;ce7(OOD^91(=4mc0|p=3(v@l*eIBU zIVQc6alT}ff)FxbAD0Sv^!J5rZv6Q6x8y(y95^ARe8HK0Lw^rr@wUS=cnIOB3-|{Q zc0}z6Lr#`GL~>)I957d3%c&J{vpqmOt`rmo`@>F=7YT5iCq9A;!0mU&9lk97Dp?5ev1&EmH{SL0s^cXk1+CAAK1 zvIbf*YZxS`7W)oudt{X-Ky0RbUl(PrGf5px;gADZ?LJR4S=Lg_*$~9Z5^WP5<6%4UP zph8I6M+hZ@cKbHZ9q}x!Q-xp)R5&yIWq?RR!-3TSAEbx!z17U@%`=0P6JY0;mCyRa z;0$Q4Fggki&3FCY_7Fj1!R@p}L#8E9dHp@62UU^Dvi2g(tkArQjyXOJtYS-;*Sd zInz*S-}@fvX&u?eNk?V}n_EaS04`>Qg)AzF7sG135%(Zye$eb@Q-B*>FTIV7dO);{ z!W(gS#9v2m>PTQ0Zb|C=%x*of>x_YxADH+;e0@d&1Ppjz8s4?Y&dXCXl(@e zQ++Y`B|%|ndx#Lx>$Q*SYWw%^r>8dZyl`)QC$t8^84@qy@3X;8&oqmNU|gw#1)fqx z%Q4x_uQQ4X+bk0P9A*=KIL%#wLDd(V80?+N052(rGxSf5h&s4W@`{ z3J;bnW2^Dzg^sKK0?!LqD|-&bQZu>|gJ|WBop%uS)7LtGZ+KCre1(MjD~%+_@Ypx?kNd3UCJy)&Q~rDiyw)x5ssMd$liV) z4~9>L5~VvOeV9e>rmc)IuAinBc5GhD85MAyTdvkF=u38-=$cCw;FAV zghKPw@$mBb@!GM;);`X0JA-C;N{&vuNZx|*`q)$ea!ZvSku^!fdY;&ux;QK$eeasa zPC?kIhkU5Z$mC2=8!@h=R?KqjBY*}*vNE3Q?}LcQsy~Khh-z6=Q_rV?8+!%lpga&o>E+bOJw$j-6mU@389DU@UEN zlXZR^tc(nNWtpLp&j~&|=Ht_!a{Wxr$6|5O~qvr1L<;ni^N=GzM0Zq)U zvd5y_npa|26$_^)IBiJw!fT~k{;|^%dT#adv2n^+^yM7OTD+_V?*-=2n z)~UK=EATAh_EGgti1>6D^?BHKE{LPk_-L>~>lP{+!u*3RL=^(KUoN)>PL@ty!W0i! zd1O3}y~zah>8w)hHum=H%)i7~qW`}ES9Iq-K9B(7gkIoy&7F+aRNt+Q=tOejb9o$0 zS!)dxR|I^c(-U`ZmvI+<6#gvauUF?<5j6#_PUN)bBxV~7BDV5D!M={^MoZWfr`;~LmB!CVk~sl$Y{5|SRbH$ra;l`gGBXBWjFz&>b}r(D?Z1dN&T3ZMHOFNYV&o_cz zW7WjCiy9|eIjdu%O}>zRTziKVApXLuCBq&op`2w$J{4e2Sv z2JzjJI49WY(i;V?|9*OGdd| zQSPK$v@wKPK_)@%Gw65EvEfEJP;asGxAZqSneGP`5cGvZ%j}EP>^U9aPhb-|E-7Dh zuxX>i_U+;-UOMX&;H#)pua!E=(&4+H--fAHko{Ct)O}O>t!FW}&A$lE=hx~EuD@bD zpE?av_*K^#&ZU53B!iYp=|>!Q5SUJq@e8W!0H)_HAv;lngBHdoOiVB_G4cPc0F4Cl zRtG;PNPn64qv{8#^ij&XT-!b>%BT$lab#)0F3`EXs$CyV(Bp&=C+r@mzyZzWzBXvB zkcWED4e=4k+`6z=+J2x@`k0Uc|Kaz|Nmb-{)Z_ej|1lpJzl!qdWF|-H>j#mXpem>#cSoo+E z)F1ihg~Rm2EeNu08J7wkkdd$Q#9BxCLJ%z4ye2lzD5Vez+_p61oLyGf@7v4GNrgs2 zLL*sDk#P>oIka9^9Oslgo)^Jd>{j2WC+`RYwfg~0EW#OIAAr$XHw-9K5ef$#@xx;+x?ma!=E#6DaJ{|z%Kf%3k7)Bf zC68%6?)gFOL)ue8P}7DJkh#udQW6;Z<)npgiz2Je6oZ~{y$sa1Dt_3{=vtw`dk(ZH zj9VjUvYah`&q?-9fOgY!*&uQ^_p)_c0Ne(X1NIQu*fj*1br_c8PqNB$R`CojOn}_V zU#w(Ee(=0t7r51s8V$tbd(pJwU=)tG;08TDuDQF__U-wls-CD=?I5#9r$swI7OsJC z@2xn~EMBg)^PA0a-z_TZPK5$GSnCWG!hcKtj<6d~s**r}4A>3IyPin%q22p|O^(Df zY3`IBBUA3C6^G)@JSBay->sgKnUybyZnQlEuWj-iG|P z!&SQ+ca#3;(I=U@kVp;U2@}b81{H88@p8_j{rZ5)=?KAmlET!0K(~lf?=09bp0U?t zJ2p}YDae}aog#i-leqDoOx?kGM9KP82SEX83+rpf*E!z*4JOa>Fh1oC!-u;kO8{{^ zHVn*E?S7RDn>OI{$dB(G4oY<~20#kJI zMRQ$al+w-bee-|7BzCaLqs`5c`F~J1jz(KSU0p860Aj*Fv#*{~={gUR32mRrJ#(EV z++|dxS=>V#!6<+uA`AFm~=_AWJROreWZx%g4HL*TG1i;;m7ISY0cvL>tk%F2g+o3#cR_$O;4c!5ytz1 zE0&HXy!ggVWm!&2*Xh-6G1ut~#nuP<-F$E<*|}Ed!ae#CNpS^r8382j7BucpJE*Vy zgCU%efRi%`z1!N^361@}><>WgbBlhP__3AtyDd*)%E{Byz{sHrNoLnopQdwu_d8Hv zpHw}@-}S&l=;36E69kJYuZ24QVC~~R!;nQtWsYIh*uQUI;9V)pGpNhDr)mn3-uat> z4hnQI_)cN?jQl%TYIO8%Lz-WgAguv2icv?}yStH`M~fVjy9R;C-h0Qu1Mt-`a9{4F z>e(h)jv&fk9{_$drC z`{b|BUsX#*A3Js|(S)OO$oJZ>fcmtzTGt`7WwW9Iyb>@tmW~;EO@0T)3|W5PQUl)= z%=Cl$M=2tS)T{yC6Mz5nD?j*y?gv3wMuQ{3kORCgg5Y~s4hWONoH@&OZ~ihOZ#H93 z6xRO);Gy*gph{j|`@_^QnO`rVf|^DsPXJeM&>uSB2$9Xx0CBM$MYvRyXubHxEesWZ zIbLy|BYWMTFa^LCgU5q=3jPkhKscW#*j>K@smhaU+ycC9#5_$XmV{xgA3l8eH}ZA9 ziSe%h3$%sId+>~<>zTakiJne_?o{7lMUIeB+`0&Fhv>}uptHs07#j}+MJLQ10YdeVQ8lWDE zBY@%PQ8YvoV)DVItp#ID!AUEm%_00h9_Wp;7=t0hjTHe%X1!NwnnBJbb-OH2T4rWDEw>I8W3{`N#}`Y>PeAbV0ACjnx(7qwUQ1FHG)H-v$}?Dk$C&zOC<<`BH5Aq7^ zs}V?EZ(D#|qL9V?T;+7wBHID|5|zTjIdud^0v{+|=>Nf+^ zV{xH6nlB4II)*zJ${Ko~Z7ixb_BFgsOg`Vf^OV$d3TN){N}QIyd`|_Z^HT1tFw9qv zg&Gv6-GMq7``OQ*7i^!8$5~!fBlFS=WYxs#E!Q8Gc6RS<+~{b085tR&{oI5b-5FE} zTIHAs-;vP-hr9$4KlzCur)hv$$XZ5G3@Lpi{c)I;iSArWzj<(E_uA*!1T()0R6>?0 z=BU2?iQ+QKyIMxtOhqek^0jDm(_HR_NL(mhNF3&Q)W_*SSyTP5V3Z z2V!QQMKKGJR;xsdiY!rMJ=D7jCj*w4qs?RNO8q086aUk+GnuYnOI{rl9I`5{7ParW z9CX5T1{+c=T2wd(?02a|Udygt61!m1WuZ7CrOFXWAivi*l^3!aTn%ax-;0Xyx(r1A}N2@`mGY$;{B3ED+To=7fwry z2$%UB@;ikA@vj@wldn%J(BJYL)KWmH zTaJcS@kv6A+d!RZ_f?2x&Zwv*;k?$G%4>u;({)4Bu;CZ2OI=q@8b`S_&wkw7e-a-M z8}cSTeVHl5iQBTIG(MzsNY?yV1h0thq{M=mMprsTn69*vGV{vf5>?8iU?-Q$SpG}` zzq~)|1W@HGL$(Ah3MA9dJc7`l~h>^<;HzaJ;go|3K-sWYxxq{t7@V07wtgaoj7B00awYTD&XL4 z?0OJ1M{$=VoeQY)s>)(VWxFf#XzJ(OvMvoMbn4tGRL;~Q$iLi4iZ-`-w zcu({ykftDVW+B-fuO>Kk3J&CX5BCj)Exa6nwgXCIM;ds36EM#)1cd0t62lXXuG1TN zRh@VXm(T8IDE!OT0j7-0&?XVvOv&qN&ZpFyhwV)GpGcrBI2(}@y$ z@vTKWs|s;mVuL}R$F&}(qfi@wN4?b3!=2qL@WN6yE-iK_uj+_-fY-w8xqAiCktujD zlRE}sIP}ohv#z`hBA2m3NhDg$1`YyxL!yr3Uilcu53_1fA=#^jdWCAhAA#SMdiOz^S`UCJpmm5)E2^?|iRgRgW=PN5 z_$H>vzvB`wK+j*unr&CKn+9eauLt}S|4N=%IkX~F&OPcD6B2v=DUV1S0{0;N7j12; zdXJ&|;kGu6^&}-4ERbuyT}3oWCaAE#*D|^p<*jlx=27Ut=tyX>#XO0^x(X*e6#6cI zGQ8N&eOo2oMFnvPXjEw42LI@QH2rOwn)YRicASuL0%(_0_VZxV#lnE*WT zvTk#-CeU^_OiQkMlUC!E&ZGXO-lV&Dddowdi-lpUcRH85&BI{Mj<(hHT&noozlQ4- z6{BojIp=@JxGRUo`p2DsQrSWNXQ@ip7WT?ByG*3iaQv04KV31qpAGssCD9Sa2ZwD* zvdrk^U5+rqpvI+TjB`fp*&kXC`*>ea_o>gOd$Vx8^Ac&w|XUp>2k zh{8ReM&f>RKv$PF*N}I^C&+Hu0J2O)|Bm=Z*@d{d52^%&-P6*`t>5Qa4La`Q87y@owu|5pqR1>e{F2@X8fNn4B0ycav!J#)h>SrY?`YxXpMT+*4Wge2-n; z8qK^a6uu4Zo(fnA0k>n`U1PRfz7mLF1H!2*@jB3(dp;$>@+;mu-HHiA`j{^FIl&ub zn_Duei7Bal{i(mn7ONlq<-1QsIMu|Y_dQ;*D0$h zv^+`Dk44b6)EvCbS?}EjyymK`)MLzVExim6j}L-=PHrE84W<}TBZB(l<=aqM7?h)N zZ?C2=3ycPktj2lS?vUyJC*ulCJ;U2o8ijU14+WwP+L}Xe@I1oRt>o!Ad|GR9p=$`X zrls26hgvv?y5-Z+g+ggHk-8_OSiUCzX18xv3SUQ)MY`RX0b??-V)BZ;cG^6S))4W_gqG20CWwtx^~+P)jU* z#21$-KLHI@eVkWURXHluVO+WV(N3PFJKj&7GXowcjn#x$+oOg*uM$UqPNpCXI&ZJ$GUlNP zjqD4puI3Af=Rb=gG~QZ{1kG_3kjy8{)+MpL!EJz~xzsv~y_eMiS`%Ama*e~N)3#;n@q$5aM`38vT( zB%D372#TN(B#d@1eU2?En(@f=OqjJSWjpR{4@Dn50T-P#i8t8Sa)BKEKN3GIRXepB zxeb7t@DV=-T9Bv87fCEBVBfQ%;L3VjZiq>kze=P1ln35iC*At!O4r-n`3#Fvu=Y%E6+1I!qBQ z3h)WK%qIrA5%TRuO{}P>5$_diHkxNri$oasom*L&zXR?TX z(AzC&;!}x)tN+IJj`NmJV>oLNu<9sktyotVbgMZ)A6r#Q6}e?rTNoC<-%+n`azi@M z853kyy&xmHv=%0o7%JNyynxGxAZN+H-I{o8r&GHCcuk@0258+BpH264lvn`2P6_oS z(6|kNP+Ca!iBPeQHyHaHc=0u2^l8GHVstAsY7r=wSxH#+xsZh0x27=QO+m1I=ryMW z117_RmkVx&1h$HXKLI?Dl$UjO9?gysa-X$NOyxCj$TC>AI}4tzYyRCqTBf?h5{B744bmyGJcmnUpbmP{L$AR=l&V z0C%io)gPKyLJq$2ewg0JZG|PqnS3C*j1Y)?VmkJdpxq&ymN+TfjG0jZ{n%p^^gbV#1xIq* zR}!x$i(Eq~~fApfW_Ll^`F0?46@f$j;(G>8K-7 zZOExE{R#m>E_EKs(kr(eM~IN+!f6T~k&ZNh?hI1VeZM*EUb-+t0Ue)s zQ@O^)BVx^62rS3> zbvBw)djWp4m*mZEsJ%@Lb=EJ^sdX7caTct0gEtLJ4#HEsCEMJ+W72t{Yb~l!9UlNx zU`)0>DL%_?`b(m{Yt})4+p?=;Ar3m2g2VvcATfaNcRPlyt=O(|b+j(b=S=_&C2gvV z3Idyke*pOSVCAyDZ;V^hAc+FR6S?CD1;w{3Y=l=3+-8w-a1dLznhJz88b%~qMsYuP z%k7atY}L*j>}HXI4zxwcJ0!>g?%NaiMONf#edcCec%^(3QkHz-2?w@4fz;~ykJw8j zsS-FGY?O`cB>cY7q}ug5Q*}QUqFPqswBC=06tn4rYM9V>%?(in{m+dY1^Etvyofc3 z$C4F27)ezKxqmd9YSL@7EzgehmoX$N?@w9s>+Sf4^W^PU6~h$Vuk3qd>!oSDHS2&N zAZ&RHxDvr!fhO8N0tV%szEPQ1WCvT*I{G7D*z(d~VW>QO5A$zbnQ7gDtSDim2t42$ z>P-qD`5LIkjj2?2#PNely;{D7E zZIPl!q)3_{h}r%-F<-g_t`DR@i!2TJoVPFY(wjqj$@lk+K=WdgoB?o#aJiC=847Fg zAwIbZCV_gPjk+%j6f!|3@}SQTD<-}mFU>L+drB$|sRI z%;!@o1CNXOjuIzMfMW)b#aga84XKt#kiYsTm?zGMKlH6~U|udEd7lYl2w?O90L-xBa&pH_vGkM>p#+ez?(Cz*Nq+A8_JZ6lDtspR=Qi&N)CK4%EfjZ+jzB zj{M7O>}}2TeOV%M$1Hxh8nTd}Dk+VkHHQtz4C;M>gKQ?F0?(IEK*m=8_^1eF&nHN& z(^i5eXab^1GzTDS^J_XNAf=NU`-u~bAyAqHZ62Vm4hVNMA(Y~Cb!;ObY$`N=Kt{Ea znch}th+<@rV07RBu?5i85~=ZTOZ;I6$ZM~bLuHyVNHcICBU=C08&=!#9P+;X6CH9t zY-=e)4RC~i6H#k4eE`q5twAsr>H7@@rDwK(6$IP}YGr|REjxUEV17&Q)q+E8ITpu} zUzI{Z-}PR$?qhqjBdVjK$HipCmfKucuCWO_IMm)*t`~6Gp!A#`Rxer!nvLQVv30u6 zl;qGryHKC)vcN<1&^pVc{Yp}v6a=@Gkz6s?nViRqoKLM87_Ly$4%FwmOwx|kSNY5@ zPi)k}p#=4XTv>Ep_HpHYTv>8+=^2)<69xyo}Ey$%-L= zntrKwTWZhz$_M#3=Zx>g%Wkg1KDe3I1xLTT364H!6YS7P0Wu$+Zarn@K_<}UEocV;v+kgN zcCe7mHGrbw>$gGVZ#P98bRftFRO8s9b|*aq)dGFAaVNe?$ZwpbHZeM9iRijABEQkD z;{E7Jh6M;>(0NAGnqCzw<^7&f=(_3`iTt|Re|w$@dv&!C?dm-IX5IS>QsAN8K~v(~ zE$F6AQ`{|2Gnn&rRjHW}(tATlgA)2`HEK{twv_m~1}77Ckb1ZE*W%$~4KB@b@4fE5r${9+em=yxA5td?Oy!Q4rf zivAD(6(77%p0ZzbCE-ymN7XkXC@}`A$sqb8)v>212tNo1Cp!R64D?<5j`~B*P_5|j z6S2Dda*l9VEhOBKf5!-T`iff5x2 zm@(3pGc|t{t--G;+!8(QT@O8x;7Za$DlQWq7s?{2!MzG4Oz;3!=Snb|pgWfw20-8t97A~k4!4()adES{wzE!RHRg7vcE z)p512d(iiM@~NTF8mYexZDtKF>aMRmYfxUK2~z~f?BFPm(1Rm%Ptm?l-OroD@4!k_osvJ&eMA~lrVVAqK!kCo!&}Q`; zy@@ts2ce1bS{3jNy1u_pc(yA;=+t_4!p**H^J2X<-hvh4#^gloM#+y0H~k6tcdk!+ z3z7Dk4j>I59*Ph*mznMoOvYdH<}G=*WQCl;e_&%hEKb`@m8YAqg>^}jAZm;K~PhBeY|A;VwYY$^S+;Qe3sMTD*MS3{c&do~v~DPJhSpQstv zNGu8b1;R6C2M!pPLwSoilpgyb=!2AQUl#WL^`X=7&@4Rkv!KMk56`|`3Xkt)s`~Xv z8~`>AmgU+G6Lyvh_KT$fmuUWWacMsRjNci`z$Mg?4hHHyF9u1rvnYnJN4u$Enj?%^ zf=-yWq%IiOQhcDMyeCZ9-h$Ls0D)upfpG5y`|1AQnBSYoDV!Hd?+{(HIE)3nT-Hu0 zS_jN>#;n^w4{bo2w;{EK(K+@*54&C9R~bg4+!#Cw4#*O29E5gE84L0*KQ~%;k*sYQ z7=xSkGc8zuDc~vNyZ%r#Dtc`RWt_r4+#gC1Q8qh$dJ;EeNb93{> zN+lIdf58<1g;8s31`=I`-Gb=J zN%);1A*MxDE?d+n4|++4r(jNv<8=mkHa-?iLX3zUF4 zW2wD!ezOTqqAomM8Dl^B% zx{}Z*IH2fQ6=XmagMb;#w;An}-|#@>G|k|5B9zfk6MM!Bv%l2}L6<}CgkehZnl|>v za)}y=#oSfN37XAr?Z?d8GU6;3R(w}4qkOFQzDcf0giLUOKA?izwP^XgjGjY%b78h!$RYuQ0n%30{F>VD0Je<@DjxcKxmz6oY9dxyzY z(kJBR=AKQ6X^FU8P*BkGH(~r%V)C1XGs-p1@p@%yr)-pys+3^20`MwxXX zN5O9cF3QOQO-)VA4V`62V^jyd_T}-QC+Uzov71?I5U9Z}i0CKVbiTw{bETj);^0VoTYwjI`z9@vMso9yu<9@i{zu!ct5{Oh(G6Wwcht_!4MJ9nMo z@o_3n{;JZ%W0Q8CbzBh^5aT00lBdPq^8jAg^P+cupAk$}0rG}k2b=V!pk%+*PY7C7 znOd!aWX?5mJ#;!1roj8L%0HJoneICRgr%HWkq3-KPs5VWgf1barB-*jw8f(lW@sz` zldTkd&8K|DTX*D``G10)A0mY9x${@g*IPp8jDf4}<`J;e1y#X|Jc{bdVI6{Exfwez$HV!jAb4}C;XI-vd)JIVuV8+gzDRk(9!&JdzA!%@A*_c zL_Bs0?AQF~G#l+HQTypT!$L4?T3_IYv3{S!xc8mV} z31HjYg{U`r#h4h9CaJ>%sIp{bn7l@T&{V_n|`e7p+Pb> zaP*I)K<}w@%Kji@V!S7mT2H`W@x%K0Pj&K}bsYZfu?kalsIF}z?0|qmrK+!a;ga4v zzCq~T1H8DFms`42ncwnWMH8_DgdTv@%W^TENxZSi6ogu%peI0|RX#j4xxmvFf*Baq z%;7L+jQvoUZJBRcLc%DBsl|>V(SRTfog5Ndqt{m@t8?#DmS#0Tzk$JJfj5A0wD*pr zM8+fjqUfm+MAlnXH<{4qop(D&&{l2=4N^iIC;A7+P4tc=`ma>o)F8;-^~x?ul+NFI z+xw5peUWAD=afb6$D11KCHJEsb~(|Ec5E=h0lD6lODO!Wa&L|ihE8p$id~RETjJGe zwA^qlf>8Ba5kaZ#-s{^K)uoV4yT8c(`JT48>|HkZs| z_OADROU@qs;g20wF}l9Wy1cVHoQ6+kzBsw_2JwxOM$D@>k2AQA$I672Q0t4w?|l55 ztdJ`?_ldaHDq<-AgaC#fdeGc(K9=9oU`hF!sI z&e5#qbvj!|(p)%6Ei%a8`+0SBH9I@|ktUzA6;r52P+x@PI7LOpv+@4U6uLgwf<=bS zTBAP4oV}&d7wGIJQJC}>icNBOjM1M8d;9}-{7KJ_k2XRoIFzIPs`IBpD^pX`R^7}B z+Oy$Qyeld8rLMXkkZ{8--Bg}V)Y$4E12yvdC-w^N^BF%67jNma0jlv;Y{=|)yJwYH zEcQ+Yv_U?uQM%JnkM{Iw={ta8{QjkVnM{HfEcUoM6L&996azhdmRTn<1H9jmlI?eV z;8bvMu&<98fBoJBR>cJdAq}T1Jy^wRz7>kzNab|uzk?Qfv9hwV=WsA>*u9y({lMtx zsNVtJu3G~V#btkXHZ{>O%da-eh=d>g&Cf?z^v#_=p%}R}RuI{jOI_NMplI9R;NT>Q zY45!eTn3CNT~#*$F}tKNWR%d%ht|q3n(JLl%3QKyCsURNpJQWnFZ1#0Ke5u;id_vY z8LQZvgG?xiuxl|NVq#+8Sw~0y<&nM|cV}?lX#2l+u0_AQqklF}H;b{%Tc-bzbL=ZD>V6gtWhcCONWchl$3uY@;l8(q9JzvE-H$CP1NLag$IGTQXW zNDLuH7S0ofdSLqc|E>Bwng!~^yJ`)|R`!*YL&@Pz7c51>Ctuvw_VUy%>GPugyU~m- zw1k^u7n~aWc(X=UB_H#y=3aWpD(|i~A1z(@sWJ4u#g0e1?0d*gOqJ>h>yv2ulW48J zr8DVa+{c!!TSKEQ4(!6`+{U11XqD65xL?KJzE`R>CKGpvrYw-Q_90z1!0=-rwe3Vl zYgZSOS$yy6{cP{D_$ zrlxlmslf!xnDvW84#GmV6JF}4wNq9P<=N_VO{g{^YEvU++H+_CNDQPzSRYCOiWrE8~r<{KfOE&)CNDT zZ0_QG&1d-WHYsp@{{e%PYk=wbZbn)U_ENu|&fzIZxK~Gn&t-9bZfxvaTF};hd~VD# zL8ZB|aoo;9#m$syb>4Pl(57Uh5tA-NT6TEH!jFfo`gMG18}i;M(o?!R^Dsv#fyt?# z`FCm0Gup#WbLvUdDeJQbliT?_3N4xxIf`!u1>8MR@j2+Q#AV8&qci(op!BBpy^0H^ zb7iKPpZIY#8)h^%F$-(A9lAMx$B)vZiWVWv*L-WK>_NCvg0kCHdqQ<~$8v!7pVWoP z$rDZOg@2=yTTT{sH94)s(%U$%1qiszc?dR$8ooD%u|3;Xo#UX z!d!zBttE}i$IOjLkoLa{!%G^8*bH^Mu0@)=vTEJKJkqs$E70kLPpx2CP^YVQ!6)4Awds!OjtDip8l)t;zGtMBRx7qK3;j{i(KR}?e3 z^oBno`46&lIzdk|{Q!y5kmeXYi$O9Z=IALhC-2g)#~3vvGkiyV%q z)t;Kz!hB*vh2;%*|5w?j??gg_8g5_vkY_%#u(>HoOcUZr~E9&*k5HJru1n&l;e~LApg( z)BMh>S)s6Q$>^U~2|R55y**l&uYEW;ovE;4e2`2qG<-tA)s_3Ov%?mMr_-`1Sg! zMyiOEgapUPnaqKV``n?=xHH|kbG$56R%X_sSo8h&VE;zfgnY#e1Aa=OB8NeJ;Fi%b+;?flligX zxVwGu=N|Pgs~Ps9x9@cACVKc%26My~KiT%;*S};#%zt)ma`Fad8g}3`C9N&DCIp_d z4a-v3l!l^pO>LhAi%etF^h^tD35kv2MPFmTPRZxvfN?+sxd{V>>cPQj_BSFAqcG8b z;KlNB3)GxrN!8z}zutsq17(8q%A9s@OlW6vTm{g>ik8X*iu_G)#j$HU=x#`-j8U^E z0l7uvNB<<{kB(YfTPG+d?e&lr;a?_EJFf`EnI@k_C#xwH^%3>c4yl|_K7^t!5cV!7dM|)y>-UMcM&AWXTQy6J<1a$h-hh^O{Q)J$b-{v&I@LfPwIa7or zPUCE*MWWs5V@=j7E0#Vc&b%2FV;?ki!t--diOG(n6`&BUVJF!wP^fJpYH~1RwH`o5 zp~wtfuw0i{?i-M;DSFr_{*qqyzJ=*h&dw>~!KJv9ui${HVcY75J|r&R5)828Z8mzS zYd0AeaDv0A!y(#tp)R|~PxG+@px0HV>-?f#pGOacm%_>WI!wepWZ1tJTp&EBal6QA ze(CvGQFFY~+_}QCTlSe@+;k{f^`tRoMv|xnO7#@9Kuxe@5B4&(m=-$&TkF3FNPpAL5lDc$jgi4w z%vw=}0VQTz5*)6vA!MS@?IW0?BxMQ?PY-QDR!Fgs~ePfz(sxD0+QHzne zOhdqWxKk&tm_FRG)iq#J9=kepwZN(;G`M1wPP_lJ%}qu>w-+l>9&Ky$Lv# zTmLt%QpubWGBu&H5i(C5DizX6vCBMc3Pog|I)qb5kuCG-oG6(#GK&ym8yX0iGG_=| znWy)=_C`9-d4AXPzSsZyPuFu_Cx^Z7Vclze$Io|ITe41g}YQxg&wy;t`Suwl{`x$rhz(pZ_B`Xu4QTmq6j zC0Qdaf}!03Trg%_jIM~tLv@7-_FH92XEu}g7=i`dPSzYA0K#Uq#C-`!bV(UDiq`*H z@4Ib~^dKeJGikDmc@LWR9Enbzdrg-${M7lXqBx_93Q0+i&-iW1rw#SyqrnG67~PS& zK{al?or)QwQTHv$!gC7#Wnb~ZIbJf5#QO2Mw&1+&YdN>Nnmp0Vvb-qm)}571(0gTc z$uYiv;5t^zg`>Opq1%n_LjtCH^l4KdBKq^gFAy;EZ9TtQmQ{3}%C5Q?WhmG_7(G2P zV$V&-g30uM^)gjE?dqwDH#S*{T-v8Ad~(l(c*lFR6K>7_u{2)V0W7UUI1aEQ@2(A0 zCGV4em+8!1a_^ZdFD#vy_|3rSqN4m&^{90Ji;VhCON&jRSH(H&EjchfK56DIS1iMf z#ds6=+tdm>@3RONe`=JUw!P9quobtCaIXzjO6fOTmfcKz%cf=D>et%S21{tH>`I@s zSLK#Fh{&dV*V2qr9Onry(m3r}wz4Y5(co$DRG-WCQSHI*F0uQSn)p6~QGCM)%zQYL z=$v|=5Hg-X0{GXO$f?Jdxa(kxyykpBE96~hSg-#7bVh$2alRAHN|E%9JH2{4(%2=7KzDYZmepzs(vT_)aq5_y%*9QF z6?WSPOT0);72fh@UqOTCk2AU&`1ARZ@lwUJ6`RmMZy*J@8-|B%%;u%DSJL{|y0qQC z=*Hq~#pK#aUEl7kKVds&Ga6Y5%POT)tAP+8KlLPm8f-Tt8PM91KV7*CdEmEVvG;~$;^8SI*b|fi| z!IZH88>*QueX}W=i1Yf!iYD_U!HhIN(-T}aQ9aTT2qTBRxajdWO8C^?`N|bJcHK+c zhUDHVXRwN{x(h%RGCQQFs)WV0jrSDY&akqEq8syp{p|V*G)|(mR3pF!L_#q?;Y4Ccg zXc%uoVU@_uFvKlNz4&(|z}K|v2OocwH~P`c=hwF>#E@}25<1b@j^X^7c!hTbR54H! zai4>Em-^w~$@Rp=zB*CnL^?L%g;A$swU~OKyLBT$s}B@*jC>jSo09kJrh%f~e0#7a zl?T@t20mM5mZ_Nn_#KpqmS>$*s#0fe$FNc0cIdIR-(B&i6$1$Xj0Q9fB7p|)67}ej zw8KyX9FqkBgM)+V>FIz_yJmr3g~nOI{)0&cmC=T7CxH~+zJ2@s`%On8oIE^j)B$MY zI900EE>^_-rtlv;7|kx3PV^c|#LfsiCjUYh86KvamXnbgY`Tc%TO(*dX}Mo;c~;7h zB+Cw$8If{rE1QJdND?J1popl6tlKfLE8No^XJh3#K6O1-@i#X&x7>w!nvighS4mM( zany-?qae`=_7aw7d@EN)`}C1|Dz(BjH8p`lV1Gw8THi?WLX#))(w36WiueS>D7c#;GfDXsXcXlq%boqNNjH~=@3ITSczkOpvU!hMQyhiiHmv?{d*s%j% z27Z#JlD6TEGi}>p;r)hrWN*G1I|7qYv#}1uDKMxTH&7n3qnwM1{p$sO484vo@9vF` z5-~l|tp16%Jp0FT+8mhBt+7J80=h~Q6Au-ap*N-w+#X;a=N((Azy;q-1m^xrH5fq2 z5j(50hlUZZXWmXhQ;hk~o=YVJZ+cF#~fK-q~~uF4JbkgjU{Ug%99vn9|u=DV`bl zH%_Ly`FvxNyH~NZ*Kql~?utCvOa$1oA>ANzfI#nW!_CX9+5Y+M+xu`Uq^8JRS5z#r z#hb6=HAq6ekP@*AWf$($#75Xbm9}1po|%k-0=F`f80*fbqgZ@@8p*^NL8%j5& zmjp30F|(7Z!8{D2=YiiK3u8XNWZH>reS9vXJV{PYPE_;(<^j+XtaaCNT?gzC4JFWU zi0lf;?5lG`a*gW#U%{%CiagxkWT~2)7y<_7-Gr3DNTn>X6gfW{F?3{cH=7(TZ#%c*b3u7vzpd7uw%s(64nJMg|6}r6#2qM7en9{ z2EegkGyFW}Vq%k;bh6bIJkTaQ0Ki}_qwJgwq%i}6NS|1e;q}z%-Sy=0NkSsfwtmsNtcZ%a<>Qiz_g#iAX%i{`z}!TU+$v)pRVqQaiwJg;~&B!PeN+w8xKub2lfasZ*gzruDUJ z44iOV8-Sz$yCpq<_ol_mc&?veFgB@R?OrC|_HJ%y*r{yf=mQ`YAeC9!*>w)>d!jgd z=7=+YF-vP%?L)KmXH`a{IHz{#MF#*-57hVWeru909eJbo%>!n!q(grJft#-oIH-E) zzLw@T3$IPfIu0nSeaaL1VHg}I(I5x;Hr?`QZxW7X z&43PTto)ft>}B0VGmQF)>T^3nPl&fU4?TJG_=e=sqh`UhZYPYP22fvLS68<$>rvH} z-J|Ky@*y)bv)XEQsp!ob>mYmbP0Okrh1D-|+}qDrPs?#uC*pY^oUf@kYc@2|0C>Nu zFCea3j=z}IM3Z{MQ&Lh`>l7j`V0M*lVg$wma-14z(K2o~n;7-R<97{X(kiE?FMEP= zN1hScxCbj5)}O0zezf3m7vClWjC-P%vU%{>?%=Ia`6hbh?7OVYbB@Dc4eytlRUmva%rRlY)Yb)40U=70P}+b_6&IZXJ`^z*2RPYf&OkTa0F_ z2ZL&W5T{(|KwjXYU20z-%mz$-TkZ{0F#~Ov=w`?f0_+`()|~Wo(Y%`zcXj0Ldf(_( z7EUheuW)HM9TVlw`f{Y3VGvkhh}$KsnHol4`p!h<4gIpyVMl%`8r9#gSz9Ofmw+)^ zDXwAgbq5FF(=B3IhhHR*zv{oCT2xF&mQ%dD7WKiDr#iFZ>^BWl85vGzU9quAD+p;2 zd)@nsp~`-Bq2JZ7GvlvjXJ?zwo3u!Tlk!#XaV0WeBJx+U8}_vO@;$-@&wpxZ;TU}* zHSN2&WI#kJk1Xg{`Jjrf{078=^!p%6l{g~y+F8Cak8wKk)1F4QvKYJqm(>(k#32Nf zGhK)(?6bYlTrI_i74|_||EK$04Y2qlI*GAj@?Ii?@*DbUo>2)dR!JUf$;l74A8U|g z`c#?QZJ%myQ1SMWM-9q0;OV(rKOPq)d9AwRtTX*Zo}`aI#C)Srwh$3B9gM-b882J~T_dIr`8~e&*o3opBUtL~Bjk5plV7j~B~e%l zC;G`*T3H3KfrfsomAN(Z=M|#@Hj!YdpTSPOP5u>RwWWj^>}RiFml~?!#~CL`Tk#MJ z+h~DB+1AD-W|nPY4k3h!AhPI8NlLP^v8g}V@<(1?9#{y9glf&Ul{WyBR79TY73;zh zNx*Y?W(Qe|3=G`eYN(M_b)kcf$RU_q7Z^_{EzvJKBoGSSz0b{(UYkc?9+2{J!Pinn2zk_g@rOgGxq@4E4B@SzPTCWQTw-adUHnpHmO~emci!ThTbxt1~x9OCJNKb6kS3ly!psO@CJmB8S`5-tHm;r-BkU? zRIzry;crTwGg0u5(~DsPdUz(&^RCD$O1<#XHz#^1JH zwLgzkJPPB&)Kk17LluaYBdK{PeFq0vAA1a0e9?oJc5kE-{02mG`cBLnDuT0jJOEAl zgrmr^X8eT;)D0OMLvBrw=t;7%wa(dG9T{GvghlQo**iW@z7`ZG`O4Qkh)yw zi~XoDd%-}09{XU50W#2WXNQ2|+?T(4HN9quh5!t7hiWQDL`QS+@We($knYaHBCq<6 zw0Io_r}lZJA@VV5nP=mfGp_ly-d{KOTG`j#f$)OKp8*;D7BsvlL2Z=yzkx#_Z*vt3 z3fClX3C1SYW}JZe*-y4ICH^x&0^y^pkc65X7J&1ngpFvfV9u#;C4=(-oSLv3S9<~mgF_x1kEeB9VZDdRG6uDy|3i;;==2gPn&GVvf=H4VF=@~*|4YUR9YWl~ifV2`Hd;*HeAyzH6|hp>{2(wFr;9KP3{=Scv$ z<~8f(_E@ov&P1Y>_($LZ&G^cy_SwaMy z_FSvXPSVpT(Cs;w3Bh^cZ<0v2&LHdTz~X*$3oUdVx38xSKN zVhk@*qm|OGw0(1LVbGRxS8T+Dx`-Td`QF5iF@3J?3cz*7up;ry7J&L9@SXgFiw zc_2W!Vyu|4{OO|J3KX)f!Kn>yfXReMulRUP%(wW$g+%mn#|t_a zg&h>t)?q|;d|QZSs?loFAFXQc1Gt8d$;(z3@P=&5%LW~kkWw@NG2rTr-%4d zPfuQj5eJ*0Uy#(|5t3Xh8hMr)kYBY(;Rvd!pgXfGyGTcF$ITH%`-=bhm}I}TWo7uM+CoJ)~RQ5 zQgndG%z!LDc||Tz(6#i+P8_w`E(|t7`zXJ6XIY|UZhqG{U^2E0!weDr;HWuGw=+_1 zYFZyIZbod-d?r*Es9{%5AdG0txZGl>gVOB91He?h;}SC=AC7Fnr2Fc=$O z0AKpv+fl56xi!t`7>L|x?>-hfJP5eNZxR)lX`o@Ols`;pk}X}80lQ7g98g*VR(t4X zRAZalBav%!L##O{bE5is4;Mn}si-hu4XwM?<5|8<#$po?Pge}vWOLQJnuo&yV$88d zVzBjF8h`yRmy0E0z3(qhPF9_gX?kY`LN;@nhv{-ui1vQS{|JAH zMC`T|J!ukLOaxTciV*lN-Rt}D7EF_e*|=|AlVg@rr-vC~La6X#V`kFgER0_FS>ZG+ zHt-50Av||kXzX-r%P18RnXxAu6JVRI2qS=c*FAv-Kf?{6JqXQ-{Jfig47~r12ke(y zVYBO$1V!)HtoU;CKhI_!wYs``w5)@rrRC6Ed-?b8-vI)w3Ke+y2^2cOiU`mFs|R5@ z$Z`W{^Ko%;vD{w(U|hL!#nn}|0AY-iZb!xnznyiaYLW<;EA?4-;hG`&&uHqJO#=2J zXh;Ns(BK96u&`^yS%GNC8-7uy;?G!HQ`3OKp4HIcR`PUrcW)be4geLJhD&Z<&cPqJvCKl;x8-yUjGgZY~wvZiwC3&4#2;vtRQhPC@2WWG%PC)&B1qW z*}4@@@%Qsfc=|L`*H0}(pbnA+;ba6}4S|0G2N*npQwPEo#p%}U$#pZk9n-Vd7~0qM z)8YX}g(K0?8EQJ9XCZj#4Zd9;L*N-&x_-kI$9G3w$vqIQC4Q(yVv!LdCWs_p0zR*R zI?R}GTLN^lj6QpCSAZyU#pZ|K81XP@VTThsWlezm0zngCO$x_x=O zx44XA=nDU_l0`Dv#0>5Mf@iJBBt>2ynXSaU13IF0a#%mXgpYc$a!$fGD-ZkNCSGL% z>ZK2;m zp8(YrgPF`sv2v^6V%2|usIIX5y7e75{*@5zMW7dWO#kkNEjo@IeRIK2-LK{g;NyEw zA|sN_gnTeYEzB;!2!R3MXE2wHSg@|NUGFLIA$4@Z*`Pbf%1~1OFp_~Yeu7+u=f7G3 z!@0W`Vlj1ixmGj@Ywt0A=0js+V_Tcv^i|l<3Q&WtVf!%1zrC!ZlVe=AMI>8A`GMwZOJB5L>9JQ6Ez6vJU??zsxx)7f2p(pLYQT&<69JkXC^2On{U| zNVUi`*C2DAshBRnrsiqRfcAms+Yk)Fs@ZjZqEVhTpa8;L0@>#dKJnbCeHua(&S4PO zM$kI3{D*G}spxpKrB(HW14qg>`utFe!n*x^g{zX=n@8vuIr7}2SgAhOS7J%VpC6mb$ zNcK=ek|f2%-p9Oyf}f3=%B%AVfcBJwxUB4DLqpz=+S<*@NlAjo5wO88!n9Y5Ots|L zKvkm*R3AWCiyWxwga&cqVqzxOR;iRS2(hf39DkNeQ#->>8;j!X?Z1tTSf^v#@*S*l zpQNT<2P6n^%OD}~EktO02MM(Rt-;aOR%lTln~_s!KTro5$pGrYx;2Pi?O{N<)i==r zFPBxSv3JFpC#|mC#u?`quY9Do8ftCCc(91k-&>F7j)ciX(d(gtaB!ZRUoq!o@QuRY|coc zB6#0bxP#k|T3rc8vjI2^6&S7=|0EWT}y1IhayMnhC0CD)z;sMFa?_Q+E{~T~x zTP`H0M_J7vbn=)cJ1Z-!-3X|L!d>vw&ZqFX5r0*@givB+=pl(T3;-X$mT<8sJGR=Af=8MpqJI zehdkxu}CWGT!4QwRy0P2PC*WkRo+Z^Jo*7PLWC&H?%=w}^O#*dLp3hc#Fmy8kS1mv z3_9wm6FV_%Re!_1DFH?l`mkFMSIBV18u&!c=~-oih{8?2_nTW^>d_8~}*OVPKtOF79gbej(+LM-_ z4!Z>_PQBJDK=lwm*z z0!j;C^-AonEHuD{7jip`Oe;HOXB>^YeQd|j6{BTi3E3{gpIb@MG3f6c#-fW@6atU_-uB87}M$YJzc;v!wQ}9+g|%6qxpMgP4o;C zTU(uO>k6v7Gw-^IA3X}@cVhulF=zIOqWjU_vfhx&@#CHZkIHEtkCDN+rI9`zuU{Q~ z`{3^P2Kv=g37D6RB2Io=TRdj2I3G}1_B?ETKmWj%x(iYg2xa}*?u7Ig0|!F1Rmh1W z-}5eP=9-(}=DpB<76RZ~tojIG#TRI$H0;p5E& z?T6YfhUlFjCHf-E59B*1i1H0y$>Uk$8l(nw+BqN98xF7P3|=Kj~k4f$2HH$)c> zvSPeo{q&|=mzRTtP7xMhvdgvX@F@fma~7$kygX**)=g)B!18@Om{2Q*&|gx4pO_g1 zJ1@)k^(NUk@M(@dvb+uZ_k3%JsdLCC<}vcfHSNg58*#)bFOOsOT3VFkrxG{+p|scn z^n#IpV=JcivYIk-I^`d?7xFWjO`*y z@tz~!{M4PqNcQ2Y6OyKKfo#B`q%J<2Bhmcgvk!Z|Y4kS}9*K2)e(QWSs_3^K`0#zDa)O1Q{%*=FH%yvURAZ6&wz=O78hgl8|ymx!hAQRjaJTzGFA zpxRKqvl^n`}y7yO4 zF|nQZPiM&ahS)_tg;eUy=6i=JL`FRC5Ai(O4^`y6JmGR~Zz`&QuWRuccJIEKG*7FV zeOz()a4SM|Wpw161FSuWIZ#>sv<~p|u+4qFV=_lQ4=0pZj^;!L?Xc)vxYkm6WVXvj zGiKIdNcCxgCpT{%GcFLR-&Jgg{K5gEWD^#ByppO7K`KNJ6lH%M#V3os7PhUe6*+nI zJqk}C6p0cxBSTH(tA2{{fOQO~kyRduVoKzPk|(v|Yf^wufDp)1=qh;EI(L5n2~^k! z6wM>!*N`Skalh>yDF8vDhc`euHo5LRN{f(ha=^=V@R+8s(xUT=`wN4R-(){K2!S&E z!r=1axK!T4ixDN)BBjMN^wGOzg=u4S>P^h-2Z$c_PkiW9S~@=Q6pFlhCq7us@-A&$ zI+`ZMU~@=qScs4Oi-f1zN^T*T2y2!MEq=(`C=sClj`P%NHiyVCE=(W9!N?XAJ{i(( z@@3_eHyCYSPM=}{GD4|$um;F$prj>|r;ZlVN!M;lj$2T^B+m=N^WZE>Bn_DZ6rYWp zf{c*e+b1lSmlk0ci(y#UA2SJ=p5%a>Wy)WF^<>78dqd=0Ywd4@P#`3Yk=Jh}fC~@H zN)3Alb+DK$>VNuHrlI-@5ib-P8fw4`sjzE{72V{RDs6@#anEFvvYa z^g*$8VCWcAHW35Z9YfJ$?ge^nnWpNOZqs0>LWl}n#ooRsDI3Sz*p)lS??m7fNN{3f zluc2dY2`iWL4lHK?|Y@FFCC2#YD(3guMw|zez)=*G96N@-QxTDeeeUIVDK=#Dv{Eb zObf^|tdCXT6yf0E=}0%N{B6@#L{3;tmKf~D4G%qg{bO63bs{L+HR6TbBG@R6o_?e` zeSgc4_8+i~2)CSLU~sU2Na4-F_Zf4hU!Ay1LQY(V77aSdVfI25iw=4 zy!l?SG`6=3Vf`i_CK~0nEn(;NRS9OIt=ZRfC$q#JMMoD7#JY7~l)T>d1-CNeWQw6y zOWXA7PlvhhgRrLtIOAi`1_Bc84A|=$a$zFi!}xo5WTCTjT8M)ANEZZFcop3T-^asu zUt4p=;QhNMGA-);tl-*WXgAWbzt+zuZL~E9b~*NdJ?vXd)0YM{y<{Cnnmb;)_5Ag+ z`%GmuQ&vHNoMHnkNkaBECL@?$(g~V+9Aurr=V3=e>xJD^EAMd+kB%}r9c}V~$z;?N zlJ`K--u_F1rkU!gnInUPB$JI4;zS@Yif06zlkOOVjWs%)dx9W_3A4zh=su$v&a@?J z=Z?!*N7Us7pc&gRSYEzNliwX-!y+{U(PS-prv`AEJs%co7gpL%!gV9<1yWfrMBDn| zZnBWQCJBxgYb35sarRtj!JQDNCMXB~Wae(vRel;aqoTPc1_r{+XV~l*j?!fXdHHD9 z18&o!3{3p^&kaHcmiLC}umP(3tKhP`dlPJZa5$|9N5ngqUu(^FQ9!ZxRpC~`a_hvw z11|^-WAEeDx|GV6JxJZ%kXe3d;|YgD`r{pSgJoEd74TCd7XFO zUiuPtnXY8p%ZgEHQtx;JM65C&7=9aST{&%T>*m4}oqsXO(5Pq%U=tbrlqDDesi4$q z4b_@0g)a6IPvg})O7TLv&kH+YYU>S7YiWIeb|O+X9c}O4rL)7XmRp%BbSi8Z|0XH0 zmrFBBn;Xw3u`+Q<-@m*yV(4C#ExQi2-?i=sMrF0JiXO6gX5-!E0ej#1F4HGTYE}Ni zFwG-E&m@so60a5>A{F9c+2X(Zy0~JRHWzT2T#rdydZbar&FOY0&9i4yqm3>d+?frl z0{<<}PtTv&Tq0KK;`VI3S94l^_Je{*vBRsRy{ozka+Zuv2ULo+1F-{VXf+DK9OBxb z^Uu)9vvGv&6s%B0ET9A~I5|GM$zA2}|C=V8ZHfr+w_XE5qH8q1#3X7T`_M}wuRp-pH zXFL$ont=5uac(CY2IQ1??7?th{oSy~5FKyNZ^l|B6Tl_V1}f6(w2Az}RY`2crdJiZ zXSr*k5bq##vZ%$S=MH}Bs4i?(3poBLa|ww7ve$4%WW5tHw12)Yf?)dMj)^mwm) zcli#raN%vr?w zh0?ZSujSreGeBGA>x^B>5vX1O;%G=}7v?t{2c}bQvZPhTS2}&aAN8Y!cWzG=-zZ>< zJPMgb+-FlZIdyypkyakD*fq6XxXFmasxL^Y&(S`mjPV3@>%k3u}2 zbt#4FAL(cZy$qXLy2DhANbc2y(d zKUPlXcUBO(mzJ{7W?-N?TjKGc0ot8gfNfNs%R7&M1_K20KcQAAO_h*r*8`H7O{5iO z2U;a1xgV(#gjQg-sTMbawu=^?YixUWv0Z1#e0r=?>By0DeLXG3US94;bmsL!no^cJ zxdGXX4DXb`pW=htJwPheQ%bQTS`*yoooD$k17*5=aqBGZhW%jKpI1dR%4ZR3&jl5LIBYBnV4(oBQlsCl6l$1Qm+I?k}GHhuoGas zP{;#Rf(tH>bbZlM09ztuwE92gYPD$QcX{-24_CTm-X5_mqwl~-Hie{$d$&P>3ox;` zm8+D3HN?+r30ZnWfw4$^3SPO^x3oB)HZ+8CR0T2v14Im=>6-KGq_jVd0Ax=ffnXGWDo>hHbE&%1P8S8RgMr*GQpdyjJz2#PE7?}C$<@IA$I*9!l-I2m_( z@k`fOb~_OZ~D;G7tLm(?35z3;mG$?Ib6`lRjr!{(DEu*c>f){r99A7N1~ERbSb4)df#gNI$b zSW^qGx69VDl-P+<6Q$1B)7d%@&sCU4d^S|m%x5x~sod6-{qD6X8 z=rrVGrl&q<7=V^oImH>Yxc++OP~} z>y|Cu+&H&ph3U2(UilzVKxxF14=^ePhOMV$`1g0WJ9X+$-330>;2F7@YINzbHuOvZ z{)FlFTAXZhb9FU<|HCU+&afHYhO6(>_Z+SOqrEn~d~0an9}pKNnF}f{*_Jy$t*ZR+ z?%e}-T9790bH<5Q?&Xp;9b9P{FiHOYCkQoC*dm1szpJs9MZWQ831G9qeqhd7SPJ%x zz7jDhwRpnRE~v+F`Ku0SxTvK+NeRVQFmB4#f?J5aTQm&}CT0dd=zi>+)2YSrRvFy@=<1djkJpip9RPWpF|Kj;EbQ6n@vXlW7Q4xl zkyy$#HP#uI{G>(vdc|#g?d@Ntj%2qq{@H>DGo^btRPVzjNvw8iSQsxVpyoN*S5{x4 zV*^bGTbkruG80eyN#ae<%ad^{cTRY3O;BIjXbn3;mfAIr9L6$ZtT-^Hh6}^R?GL4+ zdV0MI^f(MXhYibo)29}vi!JZ*iMS+G*&2&svlU1^#OaCR9$zK5tDS`qL=W*VYSNy% z9j-HfOUcP4Gq<*^b8#ugDMx;Gghl$WwOEPW%We3IM|^A;(-cUzd8;H#F0&y#(Jyos zjTNy6<^pk|5&LpN7UERuv3F5{=nk+4MBi?hl8q>;d?9jDKpJv-W2yu7>K z_)-uUm3aLvy9;{sDzk&8L#jQ~U)D}O0(uFlERGS}2h)(4ft|Y7t#yjKD9erzm^Q1I zWZE;=)VoDzq@YUo>}Fz~L6tRMt?)2)Nd_Lun+zr&k7d7H=r69AEhtc<<0wv_E&1N4 zOUuQYdK>OP^WF``PCz3GgBD7L7jDXu!2$s;FaBuF6cO{3yx=knz#wxG`C zcXi>N3r&3A>korzSivsdr1$hPxuS}|Ufow%zX27?R)sv2_s3z+M<@jM01w0{KrX*Y z`4+tFi;+Tjz4#2PW5{Q^(NmFU*AH<*WH;4XlS+Z|X@d!rlx)2@-{|>WZ}) zm6;4NB3xL9)c6S8rY^*Eu`_yC3D8A@t&WvDG+vSwW7?fUU~x*Fq4mZILA#F+QvALu zPqXzE3x~iA)SJC$LQuaZ6KoWr;NKS$SNANDUZ!{}SMY`?`Le#U&TG1d1qe4!HAkX= zq1qL{3Qhf$*CHbWS$J80oZk5e`UsM$Q>Vu?C{-;PaX-)jRle{I)awkyQ-_;={b*Nims6dvW+9-5U=8d(abA8sOO=cc(Vs8kj`qQ{dq=qOrUm5;Wz z4h2P_mjOjNv^s(5-$o0@rj(?hU4u{Gz{jHxV~Pvry?R+OJ&?yAo1shqbyc>Qu=MS2 ztysYY(b!%)k`xg}bbJ zIcO`DteWh;uO_3!4g_|i2z`os?mHMJvJRjFDVVb0$lvP{3R~=VqHi@jj%q@*4&^9s zROfb`2B!(iG=Lv@bmq2tMBr|9-(K7A?>RuB@f z2ShbkJ!PepW&*Hj3s!qaPWS9~Vr**0xr5g5f~l&pwVoG+k0vv34-HT>odzWY-u}~% z1b)Njj5J+|l8MhHKDyb~hunyy-^vHrbxwOuWyYl}9PJV0@ab#hDVB2S)&vp5h2hO_ z+pM*19LDc;(PI%wTLaJGSAZ|hcO+Kj?)tXe-WI9X!6sa6tf3-pIlSQAN3>ZQZmpy< zUYd>fSXTIz0ZDC5<=F{cFV9`qfGG<}F-nV_UPXyHOIZ_}?n%v?cfOlGtKqq~y>Y z9V?}`j6f4i6$xY>kCeugWmGphlcZBc!?%zM@B9jWEwET7MXP~6v&b}buZCn^U|NH` zCkf9j;SNK4a;mH&E>B`*uzq&(O*_U02diSsK-f&&b%JVGSl=oFv_bJ3TfQ<7s?=^j zpdjScF=K~uKbYQ2@{?fja&jVqnh7Z!g^nH3#OHH!Z-2=YJfvdg0Ulgpe#EtORvz1l z!LfzG*qmTC1J7^AV54{U(EufELS$cg0Gr3{UF2xr3V2s#cce)M18#3j4ZiDG+Ex)a z=%E|xuOsBfNVGL-22;(5XMm6_hQ+rlUJ^tu`-cyq)t;owI329tglw@<9>6~hrdVjm z8m9W1o1Oj3moL9>JtxFzBDj^$7G1Q6;+K~uBm2{&cn6tP&!ml zdtSyLM~|;EclrEk1Mg!1VQcOx9T*wlX4yqf_z&s(18iYv6op-s!KlR34G_T4BqH&5 zzVFqmSCex7INz_};)l!UnfqGtsp(>T>*c?x`?{fZ4M3b2AW~5n4#K#lH2m(p#sj@( z+3H2EV2CaJ>dPk2i2;+?)FGAd;%JLRPf%rJ1gE6+c=3Kn&EdtUY%Uy}xN*+Z$J(vC zvZWQ-^qP~Uy35aHBDSR|U_GyEl(8*4eo!iwACQ4`<--S>ZdDXLlGVY5G3Aa(cFx2x z;a_H*fOdMB7X%TyZ0@2T6<QEGUf~W`hFRz^atFo|3x#*ouv8 zV+xK1v=-_Zw^Pl|#(hQXR#!$G?N7W018GJ%e%}>DzZutkPXwj!=WlwdO@qIoVc|P@ zkHpK@!r$Y|de0zAe$Fbx&u=T*W1Lp=K(zW%u9MBnZOj<9vO+S@$!P zvuLBrL(e2IK5$j=hJCPqpS1-nH}Q#%af@N0cE(q}`R-S*jph~@KA;O%|I85Fu;tv7bbVoI1pz)rWQPAIF8WO?3Mo(S18T+2yR z9jf7k9QjthR$brUrDbFyrcx`8Y% zFFh|2GkdQ5_z0XjqA6--K;ptrx7lmVX4CIO>czx zlDRMzI|S~i3(vWOI7pjm05AKz!;wp}nQA-(a~5}V@bbT0J*<%6`OxEmX?~sX!$OnB z>({SODIKK*0|1Lk)gsW>+HJ5}jpHb%@P(ILv}Ys=>1IEvGv?49=8bz3j|1%o z8z%Hr+~*vSRkUw6Dqs+A9stwr)9}{R+Wd zOedo9U7xG^sb1{j3WCv*N-V~%z=@=La88K4^2oyVBZ?E~JsImc<7V_M*couH@I5#M zRh+>@sR37Opjl}5?&@BWv(!rx^uReabAui*?UGuW?qMqnxpWlMsmv;KLO7641#;KNOD;bGTx zBcnZTjB07k@54quKx29Jr7vJqkc)_2pj9iGzX6nl9vS2~p0Y;xWo<7;HGE~{r+Tn9 z5GXEv%6bAB$jXmDXDP3b5>aATSsx*(4_t6SBMtt<@jeBe@l1Q)x?m$U6*-(0?W3)# zS_$?UNDk1J+;zrPc8fYwOwcIg?{5Gcu@)Glum=>RtqV`B1kF&lmg7GasjUxkQ3L{< z3Vlf6b4rMGrAqA|p9t;)3RObF2AK#~2O&|uHai<3%89~-kn4jOW_{?6937!(7!XAN zC#R8H%KT#(we%4h6?v8B0;v9NyfwMdXj`(`3PQMnN}InVghlQFD&?EtgUBRGVS~!S zb=edDKRk7p3gP?Vy*YH0F!y)14P@G*h6P34G*Cv`8a*LAnG(ueSvIIi&0(Ve^lKD= zQrFORYpV@~7GF;Q&A}Q&F3O7ezgDeBA$9b)g(+YG%A8Z`Cn+OX$7(=K;5@9Y0lA+G zYD%;kfTvuBK3-37?UaJxl?2rl5)270euJD4kU4;p!z;fYrPKvdK)BUWRaF|At>cB2 z@bhYt<^T9P|2NOxx>2sHD@8AjDtytmchg|5IVhkN5r$ z+5`Q|QV0(jae&40aSwKCa?*D`COZ22Fa@7j6{(OLkaym@@AK!+H%pf2781Zl<($j7IGy-cuB^M(e&it?q z>PEJd=H9aFD6xZ@B5RlLU*6rhb0;w|5q>()Ln$laQtl|sVwnRIw`?z&T`4(EWMYP{ zDgMx}v-fZXba=HbZ-^v9>bmbJ76+vs&e>RuA03n){3~Y?`*vZV3?i6E;WB1drY_i9 z@=l|A&DJI+`y84ey57jkBI(c$8UCye7qD^ozsV83Y{>j(gc{qzPevTjr$V2BW7nBF z5v*Th#J{rlP7I_NSDEIoE6i9GB1`*3)Cua`!xG4EocRcWhXw{l28L}Q%0Mrn?9$aU zz5C>SaL@b_St1m~`=Ep(_dwnoG+npVfdKx-@~VVrsA1;fQV)9r4W90^gmR!hN2F3n zzJ@gP2M-=}bQt{0Rq(-oSXDw9(kVsnK6mdvz|){I@@%7e4$D|t%MfNw2hXKCM>!SWwTWC|It z<3^NRjqbmu;y{5Ezg(vjR@(#qT}s^PH2AxHghuc$()PeKqC+*Og%ea{mjyN3{M#I& zrVl?8;HoD4`#{%KGlCgK8Nu4f|49v!S_A&Z25SlMs}>POovs(6t9k3Iwi=QWXhtZq z*<_m1i0)sAON`98D>rIZprs07B0M^^lX6|M`p>Nyke&aNSw(5bKt&}ON@>M_vJ68G zqLu{Ikorcgc4p-G7MQv>;m3{1Z2hYG0QS6zfEd_~$BGWnQjCybHyN3heBfc&lD$P>sfqPn#coQi@ju)g#T{9{){IEUQa9u>?mY|jgAWMq=hSxO+$4| z81atZCg{LNf|{Dw-N@cSTd?7!)&_OEB%`?4E7eFwl6=5>6vBQO4b9EiDMBhd9UNj? zTLPT2QnZZAJ>b$^18u|LbWzdmsU6Uw435su;*FYigCE4;?zW3GD_qytz3uO(m)v^*sJqYtnA(zPIbBjqj zyKr=5 zL`ES0@$I6pCl3ting~B&Z4vs;v!XqM1FCL8kinjPLI7WWyH{n1L?Y5x4gq8lew;&bO zx^vY2eZ?u-zM@wR6aks-!joX)Mp5%xRtFZ?s{zgLi#6Wiko)I?QFIRs3({?68&I7a z*qc=6-2VIb|10LB7uuq&s9mz5fc~N^4!K9vKYxinrJ}kIZK^20y@A%#l;3)hEkZu* zki9@Y@{sw34sydR@^8nkBX=7esAm@CxBuSmzjOCrLZR@&f4Snn66OCH@wu$@inchX zsHyWKP$J*W6WE((#y4@Yrgk{^AC=e100MBs). + +## Recommended deployment + +This is how we recommend deploying TechDocs for potential production use. + +![TechDocs Architecture diagram](../../assets/techdocs/architecture-recommended.drawio.svg) + +TechDocs Backend: Responsible for access control and sending files over to +TechDocs. + +Tokens (if possible): Read and Write tokens. + +## FAQ + +Q: Why don't use Backstage server to serve static files? A: Not scalable. +[Investigate more] + +# Future work + +Instead of storing frontend assets in cloud storage, it can only store texts and +images. And all of frontend rendering is handled by Backstage and TechDocs +reader. From 36b9dfd41cae69b32bdfa78d0fc8aef757e4f3fa Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 20 Nov 2020 16:20:13 +0100 Subject: [PATCH 02/94] Rework the search field behavior in the sidebar We just tested it with another user an noticed some more issues: * If the user is on the search route it should be reflected in the sidebar * Clicking on the search icon alone should lead to the search page --- packages/core/src/layout/Sidebar/Items.tsx | 12 +++++++++--- .../src/components/SidebarSearch/SidebarSearch.tsx | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/core/src/layout/Sidebar/Items.tsx b/packages/core/src/layout/Sidebar/Items.tsx index 498946577d..b8786886cc 100644 --- a/packages/core/src/layout/Sidebar/Items.tsx +++ b/packages/core/src/layout/Sidebar/Items.tsx @@ -26,7 +26,6 @@ import { IconComponent } from '@backstage/core-api'; import SearchIcon from '@material-ui/icons/Search'; import clsx from 'clsx'; import React, { - FC, useContext, useState, KeyboardEventHandler, @@ -212,9 +211,10 @@ export const SidebarItem = forwardRef( type SidebarSearchFieldProps = { onSearch: (input: string) => void; + to?: string; }; -export const SidebarSearchField: FC = props => { +export const SidebarSearchField = (props: SidebarSearchFieldProps) => { const [input, setInput] = useState(''); const classes = useStyles(); @@ -229,12 +229,18 @@ export const SidebarSearchField: FC = props => { setInput(ev.target.value); }; + const handleClick = (ev: React.MouseEvent) => { + // Clicking into the search fields shouldn't navigate to the search page + ev.preventDefault(); + }; + return (
- + { [navigate], ); - return ; + return ; }; From 26484d413304cdfacaa6a53af9a7fc6678a2213d Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Fri, 20 Nov 2020 16:54:10 +0100 Subject: [PATCH 03/94] Add config schema for the sonarqube plugin --- .changeset/long-flowers-wink.md | 5 +++++ plugins/sonarqube/config.d.ts | 26 ++++++++++++++++++++++++++ plugins/sonarqube/package.json | 6 ++++-- 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 .changeset/long-flowers-wink.md create mode 100644 plugins/sonarqube/config.d.ts diff --git a/.changeset/long-flowers-wink.md b/.changeset/long-flowers-wink.md new file mode 100644 index 0000000000..7f9cf12315 --- /dev/null +++ b/.changeset/long-flowers-wink.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-sonarqube': patch +--- + +Add configuration schema diff --git a/plugins/sonarqube/config.d.ts b/plugins/sonarqube/config.d.ts new file mode 100644 index 0000000000..5facd9cef7 --- /dev/null +++ b/plugins/sonarqube/config.d.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + /** Optional configurations for the SonarQube plugin */ + sonarQube?: { + /** + * The base url of the sonarqube installation. Defaults to https://sonarcloud.io. + * @visibility frontend + */ + baseUrl: string; + }; +} diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index dcde8fab14..be8a878e50 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -47,6 +47,8 @@ "msw": "^0.21.2" }, "files": [ - "dist" - ] + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" } From b3fb9a08cee409ec309b041e96db4048ffaa41f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Nov 2020 16:41:01 +0000 Subject: [PATCH 04/94] build(deps-dev): bump @types/lodash from 4.14.161 to 4.14.165 Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.14.161 to 4.14.165. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index a90d885927..bd1757cea0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5434,9 +5434,9 @@ "@types/node" "*" "@types/lodash@^4.14.151": - version "4.14.161" - resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.161.tgz#a21ca0777dabc6e4f44f3d07f37b765f54188b18" - integrity sha512-EP6O3Jkr7bXvZZSZYlsgt5DIjiGr0dXP1/jVEwVLTFgg0d+3lWVQkRavYVQszV7dYUwvg0B8R0MBDpcmXg7XIA== + version "4.14.165" + resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.165.tgz#74d55d947452e2de0742bad65270433b63a8c30f" + integrity sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg== "@types/long@^4.0.0": version "4.0.1" From 4afc446c24caac9d508aa879ab8c70861558e066 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 20 Nov 2020 18:17:58 +0100 Subject: [PATCH 05/94] fixes dark mode issue on searchQuery + changes its class name from searchTerm to be more consistent --- .../search/src/components/SearchResult/SearchResult.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/search/src/components/SearchResult/SearchResult.tsx b/plugins/search/src/components/SearchResult/SearchResult.tsx index 1670c13963..a5dc11aa89 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.tsx @@ -31,8 +31,9 @@ import { FiltersButton, Filters, FiltersState } from '../Filters'; import SearchApi, { Result, SearchResults } from '../../apis'; const useStyles = makeStyles(theme => ({ - searchTerm: { - background: '#eee', + searchQuery: { + color: theme.palette.text.primary, + background: theme.palette.background.default, borderRadius: '10%', }, tableHeader: { @@ -107,7 +108,7 @@ const TableHeader = ({ {`${numberOfResults} `} {numberOfResults > 1 ? `results for ` : `result for `} - "{searchQuery}"{' '} + "{searchQuery}"{' '} ) : ( {`${numberOfResults} results`} From 8d948b243b778378622ffa76d19b0d6eb8d76bec Mon Sep 17 00:00:00 2001 From: Brian Leathem Date: Mon, 16 Nov 2020 14:04:24 -0800 Subject: [PATCH 06/94] Implement a generic OpenIdConnect auth-provider --- app-config.yaml | 14 ++ packages/app/src/identityProviders.ts | 7 + .../core-api/src/apis/definitions/auth.ts | 14 ++ .../src/apis/implementations/auth/index.ts | 1 + .../implementations/auth/oidc/Oidc.test.ts | 152 ++++++++++++ .../apis/implementations/auth/oidc/Oidc.ts | 183 +++++++++++++++ .../apis/implementations/auth/oidc/index.ts | 18 ++ .../apis/implementations/auth/oidc/types.ts | 28 +++ packages/core/src/api-wrappers/defaultApis.ts | 11 + plugins/auth-backend/package.json | 4 + .../auth-backend/src/providers/factories.ts | 2 + .../auth-backend/src/providers/oidc/index.ts | 17 ++ .../src/providers/oidc/provider.ts | 216 ++++++++++++++++++ plugins/auth-backend/src/service/router.ts | 5 +- yarn.lock | 58 ++++- 15 files changed, 727 insertions(+), 3 deletions(-) create mode 100644 packages/core-api/src/apis/implementations/auth/oidc/Oidc.test.ts create mode 100644 packages/core-api/src/apis/implementations/auth/oidc/Oidc.ts create mode 100644 packages/core-api/src/apis/implementations/auth/oidc/index.ts create mode 100644 packages/core-api/src/apis/implementations/auth/oidc/types.ts create mode 100644 plugins/auth-backend/src/providers/oidc/index.ts create mode 100644 plugins/auth-backend/src/providers/oidc/provider.ts diff --git a/app-config.yaml b/app-config.yaml index 32d3470f1b..cfa47618e8 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -235,6 +235,20 @@ auth: $env: AUTH_OAUTH2_AUTH_URL tokenUrl: $env: AUTH_OAUTH2_TOKEN_URL + oidc: + development: + adUrl: + $env: AUTH_OIDC_AD_URL + clientId: + $env: AUTH_OIDC_CLIENT_ID + clientSecret: + $env: AUTH_OIDC_CLIENT_SECRET + authorizationUrl: + $env: AUTH_OIDC_AUTH_URL + tokenUrl: + $env: AUTH_OIDC_TOKEN_URL + tokenSignedResponseAlg: + $env: AUTH_OIDC_TOKEN_SIGNED_RESPONSE_ALG auth0: development: clientId: diff --git a/packages/app/src/identityProviders.ts b/packages/app/src/identityProviders.ts index 06ff6b07d3..ba657328f2 100644 --- a/packages/app/src/identityProviders.ts +++ b/packages/app/src/identityProviders.ts @@ -22,9 +22,16 @@ import { samlAuthApiRef, microsoftAuthApiRef, oneloginAuthApiRef, + oidcApiRef, } from '@backstage/core'; export const providers = [ + { + id: 'oidc-auth-provider', + title: 'Oidc', + message: 'Sign In using OpenId Connect', + apiRef: oidcApiRef, + }, { id: 'google-auth-provider', title: 'Google', diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index c538065f3a..5dae5532ad 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -311,6 +311,20 @@ export const oauth2ApiRef: ApiRef< description: 'Example of how to use oauth2 custom provider', }); +/** + * Provides authentication for custom OpenID Connect identity providers. + */ +export const oidcApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +> = createApiRef({ + id: 'core.auth.oidc', + description: 'Example of how to use oidc custom provider', +}); + /** * Provides authentication for saml based identity providers */ diff --git a/packages/core-api/src/apis/implementations/auth/index.ts b/packages/core-api/src/apis/implementations/auth/index.ts index 7ef78d19cf..5adfc6f621 100644 --- a/packages/core-api/src/apis/implementations/auth/index.ts +++ b/packages/core-api/src/apis/implementations/auth/index.ts @@ -23,3 +23,4 @@ export * from './saml'; export * from './auth0'; export * from './microsoft'; export * from './onelogin'; +export * from './oidc'; diff --git a/packages/core-api/src/apis/implementations/auth/oidc/Oidc.test.ts b/packages/core-api/src/apis/implementations/auth/oidc/Oidc.test.ts new file mode 100644 index 0000000000..4286fe7e5d --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/oidc/Oidc.test.ts @@ -0,0 +1,152 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import OpenIdConnect from './Oidc'; + +const theFuture = new Date(Date.now() + 3600000); +const thePast = new Date(Date.now() - 10); + +const PREFIX = 'https://www.googleapis.com/auth/'; + +const scopeTransform = (x: string[]) => x; + +describe('OpenIdConnect', () => { + it('should get refreshed access token', async () => { + const getSession = jest.fn().mockResolvedValue({ + providerInfo: { accessToken: 'access-token', expiresAt: theFuture }, + }); + const openIdConnect = new OpenIdConnect({ + sessionManager: { getSession } as any, + scopeTransform, + }); + + expect(await openIdConnect.getAccessToken('my-scope my-scope2')).toBe( + 'access-token', + ); + expect(getSession).toBeCalledTimes(1); + expect(getSession.mock.calls[0][0].scopes).toEqual( + new Set(['my-scope', 'my-scope2']), + ); + }); + + it('should transform scopes', async () => { + const getSession = jest.fn().mockResolvedValue({ + providerInfo: { accessToken: 'access-token', expiresAt: theFuture }, + }); + const openIdConnect = new OpenIdConnect({ + sessionManager: { getSession } as any, + scopeTransform: scopes => scopes.map(scope => `my-prefix/${scope}`), + }); + + expect(await openIdConnect.getAccessToken('my-scope')).toBe('access-token'); + expect(getSession).toBeCalledTimes(1); + expect(getSession.mock.calls[0][0].scopes).toEqual( + new Set(['my-prefix/my-scope']), + ); + }); + + it('should get refreshed id token', async () => { + const getSession = jest.fn().mockResolvedValue({ + providerInfo: { idToken: 'id-token', expiresAt: theFuture }, + }); + const openIdConnect = new OpenIdConnect({ + sessionManager: { getSession } as any, + scopeTransform, + }); + + expect(await openIdConnect.getIdToken()).toBe('id-token'); + expect(getSession).toBeCalledTimes(1); + }); + + it('should get optional id token', async () => { + const getSession = jest.fn().mockResolvedValue({ + providerInfo: { idToken: 'id-token', expiresAt: theFuture }, + }); + const openIdConnect = new OpenIdConnect({ + sessionManager: { getSession } as any, + scopeTransform, + }); + + expect(await openIdConnect.getIdToken({ optional: true })).toBe('id-token'); + expect(getSession).toBeCalledTimes(1); + }); + + it('should share popup closed errors', async () => { + const error = new Error('NOPE'); + error.name = 'RejectedError'; + const getSession = jest + .fn() + .mockResolvedValueOnce({ + providerInfo: { + accessToken: 'access-token', + expiresAt: theFuture, + scopes: new Set([`${PREFIX}not-enough`]), + }, + }) + .mockRejectedValue(error); + const openIdConnect = new OpenIdConnect({ + sessionManager: { getSession } as any, + scopeTransform, + }); + + // Make sure we have a session before we do the double request, so that we get past the !this.currentSession check + await expect(openIdConnect.getAccessToken()).resolves.toBe('access-token'); + + const promise1 = openIdConnect.getAccessToken('more'); + const promise2 = openIdConnect.getAccessToken('more'); + await expect(promise1).rejects.toBe(error); + await expect(promise2).rejects.toBe(error); + expect(getSession).toBeCalledTimes(3); + }); + + it('should wait for all session refreshes', async () => { + const initialSession = { + providerInfo: { + idToken: 'token1', + expiresAt: theFuture, + scopes: new Set(), + }, + }; + const getSession = jest + .fn() + .mockResolvedValueOnce(initialSession) + .mockResolvedValue({ + providerInfo: { + idToken: 'token2', + expiresAt: theFuture, + scopes: new Set(), + }, + }); + const openIdConnect = new OpenIdConnect({ + sessionManager: { getSession } as any, + scopeTransform, + }); + + // Grab the expired session first + await expect(openIdConnect.getIdToken()).resolves.toBe('token1'); + expect(getSession).toBeCalledTimes(1); + + initialSession.providerInfo.expiresAt = thePast; + + const promise1 = openIdConnect.getIdToken(); + const promise2 = openIdConnect.getIdToken(); + const promise3 = openIdConnect.getIdToken(); + await expect(promise1).resolves.toBe('token2'); + await expect(promise2).resolves.toBe('token2'); + await expect(promise3).resolves.toBe('token2'); + expect(getSession).toBeCalledTimes(4); // De-duping of session requests happens in client + }); +}); diff --git a/packages/core-api/src/apis/implementations/auth/oidc/Oidc.ts b/packages/core-api/src/apis/implementations/auth/oidc/Oidc.ts new file mode 100644 index 0000000000..deb8331432 --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/oidc/Oidc.ts @@ -0,0 +1,183 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import OpenIdConnectIcon from '@material-ui/icons/AcUnit'; +import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; +import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; +import { SessionManager } from '../../../../lib/AuthSessionManager/types'; +import { Observable } from '../../../../types'; +import { + AuthRequestOptions, + BackstageIdentity, + OAuthApi, + OpenIdConnectApi, + ProfileInfo, + ProfileInfoApi, + SessionState, + SessionApi, + BackstageIdentityApi, +} from '../../../definitions/auth'; +import { OpenIdConnectSession } from './types'; +import { OAuthApiCreateOptions } from '../types'; + +type Options = { + sessionManager: SessionManager; + scopeTransform: (scopes: string[]) => string[]; +}; + +type CreateOptions = OAuthApiCreateOptions & { + scopeTransform?: (scopes: string[]) => string[]; +}; + +export type OpenIdConnectResponse = { + providerInfo: { + accessToken: string; + idToken: string; + scope: string; + expiresInSeconds: number; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; +}; + +const DEFAULT_PROVIDER = { + id: 'oidc', + title: 'Open ID Connect Identity Provider', + icon: OpenIdConnectIcon, +}; + +class OpenIdConnect + implements + OAuthApi, + OpenIdConnectApi, + ProfileInfoApi, + BackstageIdentityApi, + SessionApi { + static create({ + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + defaultScopes = [], + scopeTransform = x => x, + }: CreateOptions) { + const connector = new DefaultAuthConnector({ + discoveryApi, + environment, + provider, + oauthRequestApi: oauthRequestApi, + sessionTransform(res: OpenIdConnectResponse): OpenIdConnectSession { + return { + ...res, + providerInfo: { + idToken: res.providerInfo.idToken, + accessToken: res.providerInfo.accessToken, + scopes: OpenIdConnect.normalizeScopes( + scopeTransform, + res.providerInfo.scope, + ), + expiresAt: new Date( + Date.now() + res.providerInfo.expiresInSeconds * 1000, + ), + }, + }; + }, + }); + + const sessionManager = new RefreshingAuthSessionManager({ + connector, + defaultScopes: new Set(defaultScopes), + sessionScopes: (session: OpenIdConnectSession) => + session.providerInfo.scopes, + sessionShouldRefresh: (session: OpenIdConnectSession) => { + const expiresInSec = + (session.providerInfo.expiresAt.getTime() - Date.now()) / 1000; + return expiresInSec < 60 * 5; + }, + }); + + return new OpenIdConnect({ sessionManager, scopeTransform }); + } + + private readonly sessionManager: SessionManager; + private readonly scopeTransform: (scopes: string[]) => string[]; + + constructor(options: Options) { + this.sessionManager = options.sessionManager; + this.scopeTransform = options.scopeTransform; + } + + async signIn() { + await this.getAccessToken(); + } + + async signOut() { + await this.sessionManager.removeSession(); + } + + sessionState$(): Observable { + return this.sessionManager.sessionState$(); + } + + async getAccessToken( + scope?: string | string[], + options?: AuthRequestOptions, + ) { + const normalizedScopes = OpenIdConnect.normalizeScopes( + this.scopeTransform, + scope, + ); + const session = await this.sessionManager.getSession({ + ...options, + scopes: normalizedScopes, + }); + return session?.providerInfo.accessToken ?? ''; + } + + async getIdToken(options: AuthRequestOptions = {}) { + const session = await this.sessionManager.getSession(options); + return session?.providerInfo.idToken ?? ''; + } + + async getBackstageIdentity( + options: AuthRequestOptions = {}, + ): Promise { + const session = await this.sessionManager.getSession(options); + return session?.backstageIdentity; + } + + async getProfile(options: AuthRequestOptions = {}) { + const session = await this.sessionManager.getSession(options); + return session?.profile; + } + + private static normalizeScopes( + scopeTransform: (scopes: string[]) => string[], + scopes?: string | string[], + ): Set { + if (!scopes) { + return new Set(); + } + + const scopeList = Array.isArray(scopes) + ? scopes + : scopes.split(/[\s|,]/).filter(Boolean); + + return new Set(scopeTransform(scopeList)); + } +} + +export default OpenIdConnect; diff --git a/packages/core-api/src/apis/implementations/auth/oidc/index.ts b/packages/core-api/src/apis/implementations/auth/oidc/index.ts new file mode 100644 index 0000000000..cf9add0757 --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/oidc/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { default as OpenIdConnect } from './Oidc'; +export * from './types'; diff --git a/packages/core-api/src/apis/implementations/auth/oidc/types.ts b/packages/core-api/src/apis/implementations/auth/oidc/types.ts new file mode 100644 index 0000000000..ae9696c229 --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/oidc/types.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ProfileInfo, BackstageIdentity } from '../../../definitions'; + +export type OpenIdConnectSession = { + providerInfo: { + idToken: string; + accessToken: string; + scopes: Set; + expiresAt: Date; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; +}; diff --git a/packages/core/src/api-wrappers/defaultApis.ts b/packages/core/src/api-wrappers/defaultApis.ts index 61d4f3a7e1..adc5fce30a 100644 --- a/packages/core/src/api-wrappers/defaultApis.ts +++ b/packages/core/src/api-wrappers/defaultApis.ts @@ -46,6 +46,8 @@ import { SamlAuth, oneloginAuthApiRef, OneLoginAuth, + oidcApiRef, + OpenIdConnect, } from '@backstage/core-api'; export const defaultApis = [ @@ -153,4 +155,13 @@ export const defaultApis = [ factory: ({ discoveryApi, oauthRequestApi }) => OneLoginAuth.create({ discoveryApi, oauthRequestApi }), }), + createApiFactory({ + api: oidcApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi }) => + OpenIdConnect.create({ discoveryApi, oauthRequestApi }), + }), ]; diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 8ce955350a..d28251badf 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -25,12 +25,15 @@ "@backstage/catalog-model": "^0.2.0", "@backstage/config": "^0.1.1", "@types/express": "^4.17.6", + "@types/express-session": "^1.17.2", + "@types/openid-client": "^3.7.0", "compression": "^1.7.4", "cookie-parser": "^1.4.5", "cors": "^2.8.5", "cross-fetch": "^3.0.6", "express": "^4.17.1", "express-promise-router": "^3.0.3", + "express-session": "^1.17.1", "fs-extra": "^9.0.0", "got": "^11.5.2", "helmet": "^4.0.0", @@ -39,6 +42,7 @@ "knex": "^0.21.6", "moment": "^2.26.0", "morgan": "^1.10.0", + "openid-client": "^4.2.1", "passport": "^0.4.1", "passport-github2": "^0.1.12", "passport-gitlab2": "^5.0.0", diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index a764d0b50e..d4a29c0959 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -18,6 +18,7 @@ import { createGithubProvider } from './github'; import { createGitlabProvider } from './gitlab'; import { createGoogleProvider } from './google'; import { createOAuth2Provider } from './oauth2'; +import { createOidcProvider } from './oidc'; import { createOktaProvider } from './okta'; import { createSamlProvider } from './saml'; import { createAuth0Provider } from './auth0'; @@ -34,6 +35,7 @@ const factories: { [providerId: string]: AuthProviderFactory } = { auth0: createAuth0Provider, microsoft: createMicrosoftProvider, oauth2: createOAuth2Provider, + oidc: createOidcProvider, onelogin: createOneLoginProvider, }; diff --git a/plugins/auth-backend/src/providers/oidc/index.ts b/plugins/auth-backend/src/providers/oidc/index.ts new file mode 100644 index 0000000000..acacbde73d --- /dev/null +++ b/plugins/auth-backend/src/providers/oidc/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { createOidcProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts new file mode 100644 index 0000000000..3217c2a537 --- /dev/null +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -0,0 +1,216 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import { + Issuer, + Client, + Strategy as OidcStrategy, + TokenSet, + UserinfoResponse, +} from 'openid-client'; +import { + OAuthAdapter, + OAuthProviderOptions, + OAuthHandlers, + OAuthResponse, + OAuthEnvironmentHandler, + OAuthStartRequest, + encodeState, + OAuthRefreshRequest, +} from '../../lib/oauth'; +import { + executeFetchUserProfileStrategy, + executeFrameHandlerStrategy, + executeRedirectStrategy, + executeRefreshTokenStrategy, + PassportDoneCallback, +} from '../../lib/passport'; +import { RedirectInfo, AuthProviderFactory, ProfileInfo } from '../types'; + +type PrivateInfo = { + refreshToken: string; +}; + +export type OidcAuthProviderOptions = OAuthProviderOptions & { + authorizationUrl: string; + tokenUrl: string; + adUrl: string; + tokenSignedResponseAlg?: string; +}; + +export class OidcAuthProvider implements OAuthHandlers { + private readonly _strategy: Promise>; + + constructor(options: OidcAuthProviderOptions) { + this._strategy = this.setupStrategy(options); + } + + async start(req: OAuthStartRequest): Promise { + const strategy = await this._strategy; + return await executeRedirectStrategy(req, strategy, { + accessType: 'offline', + prompt: 'consent', + scope: `${req.scope} default`, + state: encodeState(req.state), + }); + } + + async handler( + req: express.Request, + ): Promise<{ response: OAuthResponse; refreshToken: string }> { + const strategy = await this._strategy; + const { response, privateInfo } = await executeFrameHandlerStrategy< + OAuthResponse, + PrivateInfo + >(req, strategy); + + return { + response: await this.populateIdentity(response), + refreshToken: privateInfo.refreshToken, + }; + } + + async refresh(req: OAuthRefreshRequest): Promise { + const strategy = await this._strategy; + const refreshTokenResponse = await executeRefreshTokenStrategy( + strategy, + req.refreshToken, + req.scope, + ); + const { + accessToken, + params, + refreshToken: updatedRefreshToken, + } = refreshTokenResponse; + + const profile = await executeFetchUserProfileStrategy( + strategy, + accessToken, + params.id_token, + ); + + return this.populateIdentity({ + providerInfo: { + accessToken, + refreshToken: updatedRefreshToken, + idToken: params.id_token, + expiresInSeconds: params.expires_in, + scope: params.scope, + }, + profile, + }); + } + + private async setupStrategy( + options: OidcAuthProviderOptions, + ): Promise> { + const issuer = await Issuer.discover(options.adUrl); + // console.log('Discovered issuer %s %O', issuer.issuer, issuer.metadata); + const client = new issuer.Client({ + client_id: options.clientId, + client_secret: options.clientSecret, + redirect_uris: [options.callbackUrl], + response_types: ['code'], + id_token_signed_response_alg: options.tokenSignedResponseAlg || 'RS256', + }); + + const strategy = new OidcStrategy( + { + client, + passReqToCallback: false as true, + }, + ( + tokenset: TokenSet, + userinfo: UserinfoResponse, + done: PassportDoneCallback, + ) => { + const profile: ProfileInfo = { + displayName: userinfo.name, + email: userinfo.email, + picture: userinfo.picture, + }; + + done( + undefined, + { + providerInfo: { + idToken: tokenset.id_token || '', + accessToken: tokenset.access_token || '', + scope: tokenset.scope || '', + expiresInSeconds: tokenset.expires_in, + }, + profile, + }, + { + refreshToken: tokenset.refresh_token || '', + }, + ); + }, + ); + strategy.error = console.error; + return strategy; + } + + // Use this function to grab the user profile info from the token + // Then populate the profile with it + private async populateIdentity( + response: OAuthResponse, + ): Promise { + const { profile } = response; + + if (!profile.email) { + throw new Error('Profile does not contain an email'); + } + const id = profile.email.split('@')[0]; + + return { ...response, backstageIdentity: { id } }; + } +} + +export const createOidcProvider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'oidc'; + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const authorizationUrl = envConfig.getString('authorizationUrl'); + const adUrl = envConfig.getString('adUrl'); + const tokenUrl = envConfig.getString('tokenUrl'); + const tokenSignedResponseAlg = envConfig.getString( + 'tokenSignedResponseAlg', + ); + + const provider = new OidcAuthProvider({ + clientId, + clientSecret, + callbackUrl, + authorizationUrl, + tokenUrl, + tokenSignedResponseAlg, + adUrl, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); + }); diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 0d19e67fc4..b74f7d8d38 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -27,6 +27,7 @@ import Router from 'express-promise-router'; import { Logger } from 'winston'; import { createOidcRouter, DatabaseKeyStore, TokenFactory } from '../identity'; import { createAuthProvider } from '../providers'; +import session from 'express-session'; export interface RouterOptions { logger: Logger; @@ -59,7 +60,9 @@ export async function createRouter({ }); const catalogApi = new CatalogClient({ discoveryApi: discovery }); - router.use(cookieParser()); + const secret = 'backstage secret'; // TODO: Allow an override here + router.use(cookieParser(secret)); + router.use(session({ secret })); router.use(express.urlencoded({ extended: false })); router.use(express.json()); diff --git a/yarn.lock b/yarn.lock index bd1757cea0..2c4d23e8d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5138,6 +5138,13 @@ "@types/qs" "*" "@types/range-parser" "*" +"@types/express-session@^1.17.2": + version "1.17.2" + resolved "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.2.tgz#ed6a36dd9f267c7fef86004f653bfb9b5cea3c21" + integrity sha512-QRm/fUuvr/BAosL9CvK351SDQP7wpD8+h3S8ZEE/8IvHJ/ZqHrjZbjx/flYfazyPw7yNi9O5fbjFZbh0vZ1ccg== + dependencies: + "@types/express" "*" + "@types/express@*", "@types/express@4.17.7", "@types/express@^4.17.6", "@types/express@^4.17.7": version "4.17.7" resolved "https://registry.npmjs.org/@types/express/-/express-4.17.7.tgz#42045be6475636d9801369cd4418ef65cdb0dd59" @@ -5566,6 +5573,13 @@ dependencies: "@types/node" "*" +"@types/openid-client@^3.7.0": + version "3.7.0" + resolved "https://registry.npmjs.org/@types/openid-client/-/openid-client-3.7.0.tgz#8406e12798d16083df09cc3625973f5a00dd57fa" + integrity sha512-hW+QbAeUyfUHABwUjUECOcv56Mf5tN29xK3GPN7wy3R3XfIQdkm37XELA4wbe2RNG9GYUpN8l2ytfoW05XmpgQ== + dependencies: + openid-client "*" + "@types/ora@^3.2.0": version "3.2.0" resolved "https://registry.npmjs.org/@types/ora/-/ora-3.2.0.tgz#b2f65d1283a8f36d8b0f9ee767e0732a2f429362" @@ -11564,6 +11578,20 @@ express-promise-router@^3.0.3: lodash.flattendeep "^4.0.0" methods "^1.0.0" +express-session@^1.17.1: + version "1.17.1" + resolved "https://registry.npmjs.org/express-session/-/express-session-1.17.1.tgz#36ecbc7034566d38c8509885c044d461c11bf357" + integrity sha512-UbHwgqjxQZJiWRTMyhvWGvjBQduGCSBDhhZXYenziMFjxst5rMV+aJZ6hKPHZnPyHGsrqRICxtX8jtEbm/z36Q== + dependencies: + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~2.0.0" + on-headers "~1.0.2" + parseurl "~1.3.3" + safe-buffer "5.2.0" + uid-safe "~2.1.5" + express@^4.0.0, express@^4.17.0, express@^4.17.1: version "4.17.1" resolved "https://registry.npmjs.org/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" @@ -12792,7 +12820,7 @@ got@^11.6.2: p-cancelable "^2.0.0" responselike "^2.0.0" -got@^11.7.0: +got@^11.7.0, got@^11.8.0: version "11.8.0" resolved "https://registry.npmjs.org/got/-/got-11.8.0.tgz#be0920c3586b07fd94add3b5b27cb28f49e6545f" integrity sha512-k9noyoIIY9EejuhaBNLyZ31D5328LeqnyPNXJQb2XlJZcKakLqN5m6O/ikhq/0lw56kUYS54fVm+D1x57YC9oQ== @@ -17740,6 +17768,20 @@ opencollective-postinstall@^2.0.2: resolved "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89" integrity sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw== +openid-client@*, openid-client@^4.2.1: + version "4.2.1" + resolved "https://registry.npmjs.org/openid-client/-/openid-client-4.2.1.tgz#8200c0ab6a3b8e954727dfa790847dc5cb8999c2" + integrity sha512-07eOcJeMH3ZHNvx5DVMZQmy3vZSTQqKSSunbtM1pXb+k5LBPi5hMum1vJCFReXlo4wuLEqZ/OgbsZvXPhbGRtA== + dependencies: + base64url "^3.0.1" + got "^11.8.0" + jose "^2.0.2" + lru-cache "^6.0.0" + make-error "^1.3.6" + object-hash "^2.0.1" + oidc-token-hash "^5.0.0" + p-any "^3.0.0" + openid-client@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/openid-client/-/openid-client-4.1.1.tgz#3e8a25584c4292e9b9b03e60358f5549fb85197a" @@ -19573,6 +19615,11 @@ ramda@^0.26, ramda@~0.26.1: resolved "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== +random-bytes@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" + integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs= + randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -21086,7 +21133,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: +safe-buffer@5.2.0, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== @@ -23357,6 +23404,13 @@ uid-number@0.0.6: resolved "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE= +uid-safe@~2.1.5: + version "2.1.5" + resolved "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" + integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA== + dependencies: + random-bytes "~1.0.0" + uid2@0.0.3, uid2@0.0.x: version "0.0.3" resolved "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82" From 563533dc6c13b35c3e46f3ec6ee5f284e18f4ce5 Mon Sep 17 00:00:00 2001 From: Brian Leathem Date: Mon, 16 Nov 2020 16:13:01 -0800 Subject: [PATCH 07/94] Moved @types deps to devDependencies --- plugins/auth-backend/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index d28251badf..f1ad45d9e5 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -25,8 +25,6 @@ "@backstage/catalog-model": "^0.2.0", "@backstage/config": "^0.1.1", "@types/express": "^4.17.6", - "@types/express-session": "^1.17.2", - "@types/openid-client": "^3.7.0", "compression": "^1.7.4", "cookie-parser": "^1.4.5", "cors": "^2.8.5", @@ -60,7 +58,9 @@ "@backstage/cli": "^0.3.0", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", + "@types/express-session": "^1.17.2", "@types/jwt-decode": "2.2.1", + "@types/openid-client": "^3.7.0", "@types/passport": "^1.0.3", "@types/passport-github2": "^1.2.4", "@types/passport-google-oauth20": "^2.0.3", From 5d27349000cb702b81258a5a551533798b99daca Mon Sep 17 00:00:00 2001 From: Brian Leathem Date: Mon, 16 Nov 2020 16:13:33 -0800 Subject: [PATCH 08/94] Initialized saveUninitialized in express-session --- plugins/auth-backend/src/service/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index b74f7d8d38..3ae3b6cb95 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -62,7 +62,7 @@ export async function createRouter({ const secret = 'backstage secret'; // TODO: Allow an override here router.use(cookieParser(secret)); - router.use(session({ secret })); + router.use(session({ secret, saveUninitialized: false })); router.use(express.urlencoded({ extended: false })); router.use(express.json()); From b5bb6935223a93bac93158d09a034304d065bca2 Mon Sep 17 00:00:00 2001 From: Brian Leathem Date: Mon, 16 Nov 2020 17:13:19 -0800 Subject: [PATCH 09/94] Initialized resave in express-session --- plugins/auth-backend/src/service/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 3ae3b6cb95..e8624d36b3 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -62,7 +62,7 @@ export async function createRouter({ const secret = 'backstage secret'; // TODO: Allow an override here router.use(cookieParser(secret)); - router.use(session({ secret, saveUninitialized: false })); + router.use(session({ secret, saveUninitialized: false, resave: false })); router.use(express.urlencoded({ extended: false })); router.use(express.json()); From 14faf3ad37caa68115f6101f69c6d86b9107bcf6 Mon Sep 17 00:00:00 2001 From: Brian Leathem Date: Wed, 18 Nov 2020 12:55:11 -0800 Subject: [PATCH 10/94] Changed the oidc prompt parameter value to "none" --- plugins/auth-backend/src/providers/oidc/provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 3217c2a537..ed1488851e 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -63,7 +63,7 @@ export class OidcAuthProvider implements OAuthHandlers { const strategy = await this._strategy; return await executeRedirectStrategy(req, strategy, { accessType: 'offline', - prompt: 'consent', + prompt: 'none', scope: `${req.scope} default`, state: encodeState(req.state), }); From 52e79a003238ffd0a7950b89a765787687ffbc53 Mon Sep 17 00:00:00 2001 From: Brian Leathem Date: Wed, 18 Nov 2020 18:23:59 -0800 Subject: [PATCH 11/94] Addressed PR feedback from @Rugvip --- .../src/apis/implementations/auth/index.ts | 1 - .../implementations/auth/oidc/Oidc.test.ts | 152 --------------- .../apis/implementations/auth/oidc/Oidc.ts | 183 ------------------ .../apis/implementations/auth/oidc/index.ts | 18 -- .../apis/implementations/auth/oidc/types.ts | 28 --- packages/core/src/api-wrappers/defaultApis.ts | 3 +- .../src/providers/oidc/provider.ts | 15 +- 7 files changed, 5 insertions(+), 395 deletions(-) delete mode 100644 packages/core-api/src/apis/implementations/auth/oidc/Oidc.test.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/oidc/Oidc.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/oidc/index.ts delete mode 100644 packages/core-api/src/apis/implementations/auth/oidc/types.ts diff --git a/packages/core-api/src/apis/implementations/auth/index.ts b/packages/core-api/src/apis/implementations/auth/index.ts index 5adfc6f621..7ef78d19cf 100644 --- a/packages/core-api/src/apis/implementations/auth/index.ts +++ b/packages/core-api/src/apis/implementations/auth/index.ts @@ -23,4 +23,3 @@ export * from './saml'; export * from './auth0'; export * from './microsoft'; export * from './onelogin'; -export * from './oidc'; diff --git a/packages/core-api/src/apis/implementations/auth/oidc/Oidc.test.ts b/packages/core-api/src/apis/implementations/auth/oidc/Oidc.test.ts deleted file mode 100644 index 4286fe7e5d..0000000000 --- a/packages/core-api/src/apis/implementations/auth/oidc/Oidc.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import OpenIdConnect from './Oidc'; - -const theFuture = new Date(Date.now() + 3600000); -const thePast = new Date(Date.now() - 10); - -const PREFIX = 'https://www.googleapis.com/auth/'; - -const scopeTransform = (x: string[]) => x; - -describe('OpenIdConnect', () => { - it('should get refreshed access token', async () => { - const getSession = jest.fn().mockResolvedValue({ - providerInfo: { accessToken: 'access-token', expiresAt: theFuture }, - }); - const openIdConnect = new OpenIdConnect({ - sessionManager: { getSession } as any, - scopeTransform, - }); - - expect(await openIdConnect.getAccessToken('my-scope my-scope2')).toBe( - 'access-token', - ); - expect(getSession).toBeCalledTimes(1); - expect(getSession.mock.calls[0][0].scopes).toEqual( - new Set(['my-scope', 'my-scope2']), - ); - }); - - it('should transform scopes', async () => { - const getSession = jest.fn().mockResolvedValue({ - providerInfo: { accessToken: 'access-token', expiresAt: theFuture }, - }); - const openIdConnect = new OpenIdConnect({ - sessionManager: { getSession } as any, - scopeTransform: scopes => scopes.map(scope => `my-prefix/${scope}`), - }); - - expect(await openIdConnect.getAccessToken('my-scope')).toBe('access-token'); - expect(getSession).toBeCalledTimes(1); - expect(getSession.mock.calls[0][0].scopes).toEqual( - new Set(['my-prefix/my-scope']), - ); - }); - - it('should get refreshed id token', async () => { - const getSession = jest.fn().mockResolvedValue({ - providerInfo: { idToken: 'id-token', expiresAt: theFuture }, - }); - const openIdConnect = new OpenIdConnect({ - sessionManager: { getSession } as any, - scopeTransform, - }); - - expect(await openIdConnect.getIdToken()).toBe('id-token'); - expect(getSession).toBeCalledTimes(1); - }); - - it('should get optional id token', async () => { - const getSession = jest.fn().mockResolvedValue({ - providerInfo: { idToken: 'id-token', expiresAt: theFuture }, - }); - const openIdConnect = new OpenIdConnect({ - sessionManager: { getSession } as any, - scopeTransform, - }); - - expect(await openIdConnect.getIdToken({ optional: true })).toBe('id-token'); - expect(getSession).toBeCalledTimes(1); - }); - - it('should share popup closed errors', async () => { - const error = new Error('NOPE'); - error.name = 'RejectedError'; - const getSession = jest - .fn() - .mockResolvedValueOnce({ - providerInfo: { - accessToken: 'access-token', - expiresAt: theFuture, - scopes: new Set([`${PREFIX}not-enough`]), - }, - }) - .mockRejectedValue(error); - const openIdConnect = new OpenIdConnect({ - sessionManager: { getSession } as any, - scopeTransform, - }); - - // Make sure we have a session before we do the double request, so that we get past the !this.currentSession check - await expect(openIdConnect.getAccessToken()).resolves.toBe('access-token'); - - const promise1 = openIdConnect.getAccessToken('more'); - const promise2 = openIdConnect.getAccessToken('more'); - await expect(promise1).rejects.toBe(error); - await expect(promise2).rejects.toBe(error); - expect(getSession).toBeCalledTimes(3); - }); - - it('should wait for all session refreshes', async () => { - const initialSession = { - providerInfo: { - idToken: 'token1', - expiresAt: theFuture, - scopes: new Set(), - }, - }; - const getSession = jest - .fn() - .mockResolvedValueOnce(initialSession) - .mockResolvedValue({ - providerInfo: { - idToken: 'token2', - expiresAt: theFuture, - scopes: new Set(), - }, - }); - const openIdConnect = new OpenIdConnect({ - sessionManager: { getSession } as any, - scopeTransform, - }); - - // Grab the expired session first - await expect(openIdConnect.getIdToken()).resolves.toBe('token1'); - expect(getSession).toBeCalledTimes(1); - - initialSession.providerInfo.expiresAt = thePast; - - const promise1 = openIdConnect.getIdToken(); - const promise2 = openIdConnect.getIdToken(); - const promise3 = openIdConnect.getIdToken(); - await expect(promise1).resolves.toBe('token2'); - await expect(promise2).resolves.toBe('token2'); - await expect(promise3).resolves.toBe('token2'); - expect(getSession).toBeCalledTimes(4); // De-duping of session requests happens in client - }); -}); diff --git a/packages/core-api/src/apis/implementations/auth/oidc/Oidc.ts b/packages/core-api/src/apis/implementations/auth/oidc/Oidc.ts deleted file mode 100644 index deb8331432..0000000000 --- a/packages/core-api/src/apis/implementations/auth/oidc/Oidc.ts +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import OpenIdConnectIcon from '@material-ui/icons/AcUnit'; -import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; -import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; -import { SessionManager } from '../../../../lib/AuthSessionManager/types'; -import { Observable } from '../../../../types'; -import { - AuthRequestOptions, - BackstageIdentity, - OAuthApi, - OpenIdConnectApi, - ProfileInfo, - ProfileInfoApi, - SessionState, - SessionApi, - BackstageIdentityApi, -} from '../../../definitions/auth'; -import { OpenIdConnectSession } from './types'; -import { OAuthApiCreateOptions } from '../types'; - -type Options = { - sessionManager: SessionManager; - scopeTransform: (scopes: string[]) => string[]; -}; - -type CreateOptions = OAuthApiCreateOptions & { - scopeTransform?: (scopes: string[]) => string[]; -}; - -export type OpenIdConnectResponse = { - providerInfo: { - accessToken: string; - idToken: string; - scope: string; - expiresInSeconds: number; - }; - profile: ProfileInfo; - backstageIdentity: BackstageIdentity; -}; - -const DEFAULT_PROVIDER = { - id: 'oidc', - title: 'Open ID Connect Identity Provider', - icon: OpenIdConnectIcon, -}; - -class OpenIdConnect - implements - OAuthApi, - OpenIdConnectApi, - ProfileInfoApi, - BackstageIdentityApi, - SessionApi { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - defaultScopes = [], - scopeTransform = x => x, - }: CreateOptions) { - const connector = new DefaultAuthConnector({ - discoveryApi, - environment, - provider, - oauthRequestApi: oauthRequestApi, - sessionTransform(res: OpenIdConnectResponse): OpenIdConnectSession { - return { - ...res, - providerInfo: { - idToken: res.providerInfo.idToken, - accessToken: res.providerInfo.accessToken, - scopes: OpenIdConnect.normalizeScopes( - scopeTransform, - res.providerInfo.scope, - ), - expiresAt: new Date( - Date.now() + res.providerInfo.expiresInSeconds * 1000, - ), - }, - }; - }, - }); - - const sessionManager = new RefreshingAuthSessionManager({ - connector, - defaultScopes: new Set(defaultScopes), - sessionScopes: (session: OpenIdConnectSession) => - session.providerInfo.scopes, - sessionShouldRefresh: (session: OpenIdConnectSession) => { - const expiresInSec = - (session.providerInfo.expiresAt.getTime() - Date.now()) / 1000; - return expiresInSec < 60 * 5; - }, - }); - - return new OpenIdConnect({ sessionManager, scopeTransform }); - } - - private readonly sessionManager: SessionManager; - private readonly scopeTransform: (scopes: string[]) => string[]; - - constructor(options: Options) { - this.sessionManager = options.sessionManager; - this.scopeTransform = options.scopeTransform; - } - - async signIn() { - await this.getAccessToken(); - } - - async signOut() { - await this.sessionManager.removeSession(); - } - - sessionState$(): Observable { - return this.sessionManager.sessionState$(); - } - - async getAccessToken( - scope?: string | string[], - options?: AuthRequestOptions, - ) { - const normalizedScopes = OpenIdConnect.normalizeScopes( - this.scopeTransform, - scope, - ); - const session = await this.sessionManager.getSession({ - ...options, - scopes: normalizedScopes, - }); - return session?.providerInfo.accessToken ?? ''; - } - - async getIdToken(options: AuthRequestOptions = {}) { - const session = await this.sessionManager.getSession(options); - return session?.providerInfo.idToken ?? ''; - } - - async getBackstageIdentity( - options: AuthRequestOptions = {}, - ): Promise { - const session = await this.sessionManager.getSession(options); - return session?.backstageIdentity; - } - - async getProfile(options: AuthRequestOptions = {}) { - const session = await this.sessionManager.getSession(options); - return session?.profile; - } - - private static normalizeScopes( - scopeTransform: (scopes: string[]) => string[], - scopes?: string | string[], - ): Set { - if (!scopes) { - return new Set(); - } - - const scopeList = Array.isArray(scopes) - ? scopes - : scopes.split(/[\s|,]/).filter(Boolean); - - return new Set(scopeTransform(scopeList)); - } -} - -export default OpenIdConnect; diff --git a/packages/core-api/src/apis/implementations/auth/oidc/index.ts b/packages/core-api/src/apis/implementations/auth/oidc/index.ts deleted file mode 100644 index cf9add0757..0000000000 --- a/packages/core-api/src/apis/implementations/auth/oidc/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { default as OpenIdConnect } from './Oidc'; -export * from './types'; diff --git a/packages/core-api/src/apis/implementations/auth/oidc/types.ts b/packages/core-api/src/apis/implementations/auth/oidc/types.ts deleted file mode 100644 index ae9696c229..0000000000 --- a/packages/core-api/src/apis/implementations/auth/oidc/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ProfileInfo, BackstageIdentity } from '../../../definitions'; - -export type OpenIdConnectSession = { - providerInfo: { - idToken: string; - accessToken: string; - scopes: Set; - expiresAt: Date; - }; - profile: ProfileInfo; - backstageIdentity: BackstageIdentity; -}; diff --git a/packages/core/src/api-wrappers/defaultApis.ts b/packages/core/src/api-wrappers/defaultApis.ts index adc5fce30a..9b3d26fb4d 100644 --- a/packages/core/src/api-wrappers/defaultApis.ts +++ b/packages/core/src/api-wrappers/defaultApis.ts @@ -47,7 +47,6 @@ import { oneloginAuthApiRef, OneLoginAuth, oidcApiRef, - OpenIdConnect, } from '@backstage/core-api'; export const defaultApis = [ @@ -162,6 +161,6 @@ export const defaultApis = [ oauthRequestApi: oauthRequestApiRef, }, factory: ({ discoveryApi, oauthRequestApi }) => - OpenIdConnect.create({ discoveryApi, oauthRequestApi }), + OAuth2.create({ discoveryApi, oauthRequestApi }), }), ]; diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index ed1488851e..71ea2c4103 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -46,9 +46,7 @@ type PrivateInfo = { }; export type OidcAuthProviderOptions = OAuthProviderOptions & { - authorizationUrl: string; - tokenUrl: string; - adUrl: string; + metadataUrl: string; tokenSignedResponseAlg?: string; }; @@ -118,8 +116,7 @@ export class OidcAuthProvider implements OAuthHandlers { private async setupStrategy( options: OidcAuthProviderOptions, ): Promise> { - const issuer = await Issuer.discover(options.adUrl); - // console.log('Discovered issuer %s %O', issuer.issuer, issuer.metadata); + const issuer = await Issuer.discover(options.metadataUrl); const client = new issuer.Client({ client_id: options.clientId, client_secret: options.clientSecret, @@ -191,9 +188,7 @@ export const createOidcProvider: AuthProviderFactory = ({ const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const authorizationUrl = envConfig.getString('authorizationUrl'); - const adUrl = envConfig.getString('adUrl'); - const tokenUrl = envConfig.getString('tokenUrl'); + const metadataUrl = envConfig.getString('metadataUrl'); const tokenSignedResponseAlg = envConfig.getString( 'tokenSignedResponseAlg', ); @@ -202,10 +197,8 @@ export const createOidcProvider: AuthProviderFactory = ({ clientId, clientSecret, callbackUrl, - authorizationUrl, - tokenUrl, tokenSignedResponseAlg, - adUrl, + metadataUrl, }); return OAuthAdapter.fromConfig(globalConfig, provider, { From 5c485ff60ff2c381e7083449c57cea58586b4acc Mon Sep 17 00:00:00 2001 From: Brian Leathem Date: Thu, 19 Nov 2020 09:11:13 -0800 Subject: [PATCH 12/94] Added a auth.session.secret config value for setting the express session secret --- app-config.yaml | 2 ++ plugins/auth-backend/package.json | 23 +++++++++++++++++++++- plugins/auth-backend/src/service/router.ts | 4 +++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index cfa47618e8..34a0b09520 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -191,6 +191,8 @@ scaffolder: $env: AZURE_TOKEN auth: + session: + secret: yaml session secret providers: google: development: diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index f1ad45d9e5..94502775e4 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -71,5 +71,26 @@ "files": [ "dist", "migrations" - ] + ], + "configSchema": { + "$schema": "https://backstage.io/schema/config-v1", + "title": "@backstage/auth-backend", + "type": "object", + "properties": { + "auth": { + "type": "object", + "properties": { + "session": { + "type": "object", + "properties": { + "secret": { + "type": "string", + "visibility": "secret" + } + } + } + } + } + } + } } diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index e8624d36b3..1baa1d4892 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -60,7 +60,9 @@ export async function createRouter({ }); const catalogApi = new CatalogClient({ discoveryApi: discovery }); - const secret = 'backstage secret'; // TODO: Allow an override here + const secret = + config.getOptionalString('auth.session.secret') ?? 'backstage secret'; + console.log('using secret', secret); router.use(cookieParser(secret)); router.use(session({ secret, saveUninitialized: false, resave: false })); router.use(express.urlencoded({ extended: false })); From 78b390483df0dbc4f4369d4887a157c4df2692d5 Mon Sep 17 00:00:00 2001 From: Brian Leathem Date: Thu, 19 Nov 2020 11:13:04 -0800 Subject: [PATCH 13/94] Added a oidc provider test --- plugins/auth-backend/package.json | 4 +- .../src/providers/oidc/provider.test.ts | 60 +++++++++++++++++++ .../src/providers/oidc/provider.ts | 2 +- yarn.lock | 22 +++++++ 4 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 plugins/auth-backend/src/providers/oidc/provider.test.ts diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 94502775e4..4d1e52aa21 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -60,13 +60,15 @@ "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", "@types/jwt-decode": "2.2.1", + "@types/nock": "^11.1.0", "@types/openid-client": "^3.7.0", "@types/passport": "^1.0.3", "@types/passport-github2": "^1.2.4", "@types/passport-google-oauth20": "^2.0.3", "@types/passport-microsoft": "^0.0.0", "@types/passport-saml": "^1.1.2", - "msw": "^0.21.2" + "msw": "^0.21.2", + "nock": "^13.0.5" }, "files": [ "dist", diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts new file mode 100644 index 0000000000..e8b414edf2 --- /dev/null +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import nock from 'nock'; +import { ClientMetadata, IssuerMetadata } from 'openid-client'; +import { OidcAuthProvider } from './provider'; + +const issuerMetadata = { + issuer: 'https://oidc.test', + authorization_endpoint: 'https://oidc.test/as/authorization.oauth2', + token_endpoint: 'https://oidc.test/as/token.oauth2', + revocation_endpoint: 'https://oidc.test/as/revoke_token.oauth2', + userinfo_endpoint: 'https://oidc.test/idp/userinfo.openid', + introspection_endpoint: 'https://oidc.test/as/introspect.oauth2', + jwks_uri: 'https://oidc.test/pf/JWKS', + scopes_supported: ['openid'], + claims_supported: ['email'], + response_types_supported: ['code'], + id_token_signing_alg_values_supported: ['RS256', 'RS512'], + token_endpoint_auth_signing_alg_values_supported: ['RS256', 'RS512'], + request_object_signing_alg_values_supported: ['RS256', 'RS512'], +}; + +const clientMetadata = { + callbackUrl: 'https://oidc.test/callback', + clientId: 'testclientid', + clientSecret: 'testclientsecret', + metadataUrl: 'https://oidc.test/.well-known/openid-configuration', +}; + +describe('OidcAuthProvider', () => { + it('hit the metadataurl', async () => { + const scope = nock('https://oidc.test') + .get('/.well-known/openid-configuration') + .reply(200, issuerMetadata); + const provider = new OidcAuthProvider(clientMetadata); + const strategy = ((await provider._strategy) as any) as { + _client: ClientMetadata; + _issuer: IssuerMetadata; + }; + // Assert that the expected request to the metadaurl was made. + expect(scope.isDone()).toBeTruthy(); + const { _client, _issuer } = strategy; + expect(_client.client_id).toBe(clientMetadata.clientId); + expect(_issuer.token_endpoint).toBe(issuerMetadata.token_endpoint); + }); +}); diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 71ea2c4103..5ebb859f2f 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -51,7 +51,7 @@ export type OidcAuthProviderOptions = OAuthProviderOptions & { }; export class OidcAuthProvider implements OAuthHandlers { - private readonly _strategy: Promise>; + readonly _strategy: Promise>; constructor(options: OidcAuthProviderOptions) { this._strategy = this.setupStrategy(options); diff --git a/yarn.lock b/yarn.lock index 2c4d23e8d7..ba25713fdc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5521,6 +5521,13 @@ dependencies: "@types/node" "*" +"@types/nock@^11.1.0": + version "11.1.0" + resolved "https://registry.npmjs.org/@types/nock/-/nock-11.1.0.tgz#0a8c1056a31ba32a959843abccf99626dd90a538" + integrity sha512-jI/ewavBQ7X5178262JQR0ewicPAcJhXS/iFaNJl0VHLfyosZ/kwSrsa6VNQNSO8i9d8SqdRgOtZSOKJ/+iNMw== + dependencies: + nock "*" + "@types/node-fetch@2.5.7", "@types/node-fetch@^2.5.4": version "2.5.7" resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c" @@ -17139,6 +17146,16 @@ no-case@^3.0.3: lower-case "^2.0.1" tslib "^1.10.0" +nock@*, nock@^13.0.5: + version "13.0.5" + resolved "https://registry.npmjs.org/nock/-/nock-13.0.5.tgz#a618c6f86372cb79fac04ca9a2d1e4baccdb2414" + integrity sha512-1ILZl0zfFm2G4TIeJFW0iHknxr2NyA+aGCMTjDVUsBY4CkMRispF1pfIYkTRdAR/3Bg+UzdEuK0B6HczMQZcCg== + dependencies: + debug "^4.1.0" + json-stringify-safe "^5.0.1" + lodash.set "^4.3.2" + propagate "^2.0.0" + node-addon-api@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz#f9afb8d777a91525244b01775ea0ddbe1125483b" @@ -19385,6 +19402,11 @@ prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, object-assign "^4.1.1" react-is "^16.8.1" +propagate@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" + integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== + property-expr@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/property-expr/-/property-expr-2.0.2.tgz#fff2a43919135553a3bc2fdd94bdb841965b2330" From 3712c47974e67a6a4d4d3e29569ecf470d27a355 Mon Sep 17 00:00:00 2001 From: Brian Leathem Date: Thu, 19 Nov 2020 17:35:03 -0800 Subject: [PATCH 14/94] Added a test for the OidcAuthProvider handler method --- .../src/providers/oidc/provider.test.ts | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts index e8b414edf2..0ad464f607 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.test.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -14,9 +14,12 @@ * limitations under the License. */ +import express from 'express'; +import { Session } from 'express-session'; import nock from 'nock'; import { ClientMetadata, IssuerMetadata } from 'openid-client'; import { OidcAuthProvider } from './provider'; +import { JWT, JWK } from 'jose'; const issuerMetadata = { issuer: 'https://oidc.test', @@ -29,9 +32,9 @@ const issuerMetadata = { scopes_supported: ['openid'], claims_supported: ['email'], response_types_supported: ['code'], - id_token_signing_alg_values_supported: ['RS256', 'RS512'], - token_endpoint_auth_signing_alg_values_supported: ['RS256', 'RS512'], - request_object_signing_alg_values_supported: ['RS256', 'RS512'], + id_token_signing_alg_values_supported: ['RS256', 'RS512', 'HS256'], + token_endpoint_auth_signing_alg_values_supported: ['RS256', 'RS512', 'HS256'], + request_object_signing_alg_values_supported: ['RS256', 'RS512', 'HS256'], }; const clientMetadata = { @@ -39,10 +42,11 @@ const clientMetadata = { clientId: 'testclientid', clientSecret: 'testclientsecret', metadataUrl: 'https://oidc.test/.well-known/openid-configuration', + tokenSignedResponseAlg: 'none', }; describe('OidcAuthProvider', () => { - it('hit the metadataurl', async () => { + it('hit the metadata url', async () => { const scope = nock('https://oidc.test') .get('/.well-known/openid-configuration') .reply(200, issuerMetadata); @@ -57,4 +61,35 @@ describe('OidcAuthProvider', () => { expect(_client.client_id).toBe(clientMetadata.clientId); expect(_issuer.token_endpoint).toBe(issuerMetadata.token_endpoint); }); + + it('OidcAuthProvider#handler successfully invokes the oidc endpoints', async () => { + const jwt = { + sub: 'alice', + iss: 'https://oidc.test', + aud: clientMetadata.clientId, + exp: Date.now() + 10000, + }; + const scope = nock('https://oidc.test') + .get('/.well-known/openid-configuration') + .reply(200, issuerMetadata) + .post('/as/token.oauth2') + .reply(200, { + id_token: JWT.sign(jwt, JWK.None), + access_token: 'test', + authorization_signed_response_alg: 'HS256', + }) + .get('/idp/userinfo.openid') + .reply(200, { + sub: 'alice', + email: 'alice@oidc.test', + }); + const provider = new OidcAuthProvider(clientMetadata); + const req = { + method: 'GET', + url: '/?code=test2', + session: ({ 'oidc:oidc.test': 'test' } as any) as Session, + } as express.Request; + await provider.handler(req); + expect(scope.isDone()).toBeTruthy(); + }); }); From afaaf96eccccd91ea914f3e52a9ea14e1e3ae6e6 Mon Sep 17 00:00:00 2001 From: Brian Leathem Date: Thu, 19 Nov 2020 23:49:12 -0800 Subject: [PATCH 15/94] Cleanup and fixes after refactoring --- app-config.yaml | 4 ++-- packages/app/src/identityProviders.ts | 4 ++-- packages/core-api/src/apis/definitions/auth.ts | 2 +- packages/core/src/api-wrappers/defaultApis.ts | 16 +++++++++++++--- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 34a0b09520..8dc870a7cd 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -239,8 +239,8 @@ auth: $env: AUTH_OAUTH2_TOKEN_URL oidc: development: - adUrl: - $env: AUTH_OIDC_AD_URL + metadataUrl: + $env: AUTH_OIDC_METADATA_URL clientId: $env: AUTH_OIDC_CLIENT_ID clientSecret: diff --git a/packages/app/src/identityProviders.ts b/packages/app/src/identityProviders.ts index ba657328f2..01828b1405 100644 --- a/packages/app/src/identityProviders.ts +++ b/packages/app/src/identityProviders.ts @@ -22,7 +22,7 @@ import { samlAuthApiRef, microsoftAuthApiRef, oneloginAuthApiRef, - oidcApiRef, + oidcAuthApiRef, } from '@backstage/core'; export const providers = [ @@ -30,7 +30,7 @@ export const providers = [ id: 'oidc-auth-provider', title: 'Oidc', message: 'Sign In using OpenId Connect', - apiRef: oidcApiRef, + apiRef: oidcAuthApiRef, }, { id: 'google-auth-provider', diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index 5dae5532ad..30b07887ad 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -314,7 +314,7 @@ export const oauth2ApiRef: ApiRef< /** * Provides authentication for custom OpenID Connect identity providers. */ -export const oidcApiRef: ApiRef< +export const oidcAuthApiRef: ApiRef< OAuthApi & OpenIdConnectApi & ProfileInfoApi & diff --git a/packages/core/src/api-wrappers/defaultApis.ts b/packages/core/src/api-wrappers/defaultApis.ts index 9b3d26fb4d..1f18f54d2a 100644 --- a/packages/core/src/api-wrappers/defaultApis.ts +++ b/packages/core/src/api-wrappers/defaultApis.ts @@ -46,9 +46,11 @@ import { SamlAuth, oneloginAuthApiRef, OneLoginAuth, - oidcApiRef, + oidcAuthApiRef, } from '@backstage/core-api'; +import OAuth2Icon from '@material-ui/icons/AcUnit'; + export const defaultApis = [ createApiFactory({ api: discoveryApiRef, @@ -155,12 +157,20 @@ export const defaultApis = [ OneLoginAuth.create({ discoveryApi, oauthRequestApi }), }), createApiFactory({ - api: oidcApiRef, + api: oidcAuthApiRef, deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef, }, factory: ({ discoveryApi, oauthRequestApi }) => - OAuth2.create({ discoveryApi, oauthRequestApi }), + OAuth2.create({ + discoveryApi, + oauthRequestApi, + provider: { + id: 'oidc', + title: 'Your Identity Provider', + icon: OAuth2Icon, + }, + }), }), ]; From 68be35912b8e0c52c701e5a9e172a3b1cd9afb6f Mon Sep 17 00:00:00 2001 From: Brian Leathem Date: Fri, 20 Nov 2020 10:07:59 -0800 Subject: [PATCH 16/94] Conditionally enable the auth-backend session support --- app-config.yaml | 6 +++--- plugins/auth-backend/src/service/router.ts | 16 +++++++++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 8dc870a7cd..fcc54a1a35 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -189,10 +189,10 @@ scaffolder: api: token: $env: AZURE_TOKEN - auth: - session: - secret: yaml session secret + ### Providing an auth.session.secret will enable session support in the auth-backend + # session: + # secret: custom session secret providers: google: development: diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 1baa1d4892..47e0d14caa 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -28,6 +28,7 @@ import { Logger } from 'winston'; import { createOidcRouter, DatabaseKeyStore, TokenFactory } from '../identity'; import { createAuthProvider } from '../providers'; import session from 'express-session'; +import passport from 'passport'; export interface RouterOptions { logger: Logger; @@ -60,11 +61,16 @@ export async function createRouter({ }); const catalogApi = new CatalogClient({ discoveryApi: discovery }); - const secret = - config.getOptionalString('auth.session.secret') ?? 'backstage secret'; - console.log('using secret', secret); - router.use(cookieParser(secret)); - router.use(session({ secret, saveUninitialized: false, resave: false })); + const secret = config.getOptionalString('auth.session.secret'); + if (secret) { + router.use(cookieParser(secret)); + // TODO: Configure the server-side session storage. The default MemoryStore is not designed for production + router.use(session({ secret, saveUninitialized: false, resave: false })); + router.use(passport.initialize()); + router.use(passport.session()); + } else { + router.use(cookieParser()); + } router.use(express.urlencoded({ extended: false })); router.use(express.json()); From 40b3036fac715bb7ee81b673ec397b7951876a35 Mon Sep 17 00:00:00 2001 From: Brian Leathem Date: Fri, 20 Nov 2020 11:57:00 -0800 Subject: [PATCH 17/94] Added a test for the createOidcProvider method --- .../src/providers/oidc/provider.test.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts index 0ad464f607..ade69255b7 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.test.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -18,8 +18,11 @@ import express from 'express'; import { Session } from 'express-session'; import nock from 'nock'; import { ClientMetadata, IssuerMetadata } from 'openid-client'; -import { OidcAuthProvider } from './provider'; +import { createOidcProvider, OidcAuthProvider } from './provider'; import { JWT, JWK } from 'jose'; +import { AuthProviderFactoryOptions } from '../types'; +import { Config } from '@backstage/config'; +import { OAuthAdapter } from '../../lib/oauth'; const issuerMetadata = { issuer: 'https://oidc.test', @@ -92,4 +95,21 @@ describe('OidcAuthProvider', () => { await provider.handler(req); expect(scope.isDone()).toBeTruthy(); }); + + const options = { + globalConfig: { + appUrl: 'https://oidc.test', + baseUrl: 'https://oidc.test', + }, + config: ({ + keys: jest.fn(() => ['test']), + getConfig: jest.fn(() => ({ getString: () => '' })), + } as any) as Config, + } as AuthProviderFactoryOptions; + + it('createOidcProvider', () => { + const provider = createOidcProvider(options) as OAuthAdapter; + console.log(provider); + expect(provider.start).toBeDefined(); + }); }); From c220f7e35a9159b6540150a6a0f1710275c74aff Mon Sep 17 00:00:00 2001 From: blam Date: Sun, 22 Nov 2020 02:50:35 +0100 Subject: [PATCH 18/94] chore(scaffolder/template): fixing catalog-info.yaml reference for templates with new version --- .../{component-info.yaml => catalog-info.yaml} | 0 .../{component-info.yaml => catalog-info.yaml} | 0 .../{component-info.yaml => catalog-info.yaml} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/{component-info.yaml => catalog-info.yaml} (100%) rename plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/{component-info.yaml => catalog-info.yaml} (100%) rename plugins/scaffolder-backend/sample-templates/springboot-grpc-template/{{cookiecutter.component_id}}/{component-info.yaml => catalog-info.yaml} (100%) diff --git a/plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/component-info.yaml b/plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/catalog-info.yaml similarity index 100% rename from plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/component-info.yaml rename to plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/catalog-info.yaml diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/component-info.yaml b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/catalog-info.yaml similarity index 100% rename from plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/component-info.yaml rename to plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/catalog-info.yaml diff --git a/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/{{cookiecutter.component_id}}/component-info.yaml b/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/{{cookiecutter.component_id}}/catalog-info.yaml similarity index 100% rename from plugins/scaffolder-backend/sample-templates/springboot-grpc-template/{{cookiecutter.component_id}}/component-info.yaml rename to plugins/scaffolder-backend/sample-templates/springboot-grpc-template/{{cookiecutter.component_id}}/catalog-info.yaml From 9075c4816b73716988bdad7daa2fd7a1eb94bd50 Mon Sep 17 00:00:00 2001 From: blam Date: Sun, 22 Nov 2020 02:51:10 +0100 Subject: [PATCH 19/94] docs: added a note about templates should have catalog-info --- docs/features/software-templates/adding-templates.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/features/software-templates/adding-templates.md b/docs/features/software-templates/adding-templates.md index 75a75003f7..e2b69d6767 100644 --- a/docs/features/software-templates/adding-templates.md +++ b/docs/features/software-templates/adding-templates.md @@ -55,6 +55,10 @@ contains more information about the required fields. Once we have a `template.yaml` ready, we can then add it to the service catalog for use by the scaffolder. +_NOTE_: When the `publish` step is completed, it is currently assumed by the +scaffolder that the final repository should contain a `catalog-info.yaml` in +order to register this with the Catalog in Backstage. + Currently the catalog supports loading definitions from GitHub + Local Files. To load from other places, not only will there need to be another preparer, but the support to load the location will also need to be added to the Catalog. From ab4794c70272da8deb01df99367a234242cdf3eb Mon Sep 17 00:00:00 2001 From: blam Date: Sun, 22 Nov 2020 02:51:40 +0100 Subject: [PATCH 20/94] chore(scaffolder/cra): needs to create catalog-info yaml --- .../scaffolder-backend/src/scaffolder/stages/templater/cra.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cra.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cra.ts index da33149c24..045feabc62 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cra.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cra.ts @@ -69,7 +69,7 @@ export class CreateReactAppTemplater implements TemplaterBase { ); await fs.promises.writeFile( - `${finalDir}/component-info.yaml`, + `${finalDir}/catalog-info.yaml`, yaml.stringify(componentInfo), ); From f12a7cd906fe000263d82d4b37e8803ae9c49bac Mon Sep 17 00:00:00 2001 From: blam Date: Sun, 22 Nov 2020 02:52:00 +0100 Subject: [PATCH 21/94] chore(scaffolder): a little housekeeping and better error handling for the scaffolder frontend. --- .../JobStatusModal/JobStatusModal.tsx | 61 ++++++++-------- .../components/TemplatePage/TemplatePage.tsx | 73 ++++++++++--------- .../useJobPolling.ts | 51 +++++-------- plugins/scaffolder/src/types.ts | 6 +- 4 files changed, 94 insertions(+), 97 deletions(-) rename plugins/scaffolder/src/components/{JobStatusModal => hooks}/useJobPolling.ts (55%) diff --git a/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx b/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx index d8b194eb41..f0aa5ccd81 100644 --- a/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx +++ b/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx @@ -13,43 +13,48 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { Button } from '@backstage/core'; -import { entityRoute, entityRouteParams } from '@backstage/plugin-catalog'; import { + Button as Action, Dialog, DialogActions, DialogContent, DialogTitle, LinearProgress, } from '@material-ui/core'; -import React, { useEffect, useState } from 'react'; -import { generatePath } from 'react-router-dom'; + +import React, { useCallback, useState } from 'react'; import { Job } from '../../types'; import { JobStage } from '../JobStage/JobStage'; -import { useJobPolling } from './useJobPolling'; type Props = { - onComplete: (job: Job) => void; - jobId: string; - entity: TemplateEntityV1alpha1 | null; + job: Job; + toCatalogLink?: string; }; -export const JobStatusModal = ({ jobId, onComplete, entity }: Props) => { - const job = useJobPolling(jobId); - const [dialogTitle, setDialogTitle] = useState('Creating component...'); +export const JobStatusModal = ({ job, toCatalogLink }: Props) => { + const [isOpen, setOpen] = useState(true); - useEffect(() => { - if (job?.status === 'COMPLETED') { - setDialogTitle('Successfully created component'); - onComplete(job); - } else if (job?.status === 'FAILED') - setDialogTitle('Failed to create component'); - }, [job, onComplete, setDialogTitle]); + const renderTitle = () => { + switch (job?.status) { + case 'COMPLETED': + return 'Successfully created component'; + case 'FAILED': + return 'Failed to create component'; + default: + return 'Create component'; + } + }; + + const onClose = useCallback(() => { + if (job.status === 'COMPLETED' || job.status === 'FAILED') { + setOpen(false); + } + }, [job]); return ( - - {dialogTitle} + + {renderTitle()} {!job ? ( @@ -66,16 +71,14 @@ export const JobStatusModal = ({ jobId, onComplete, entity }: Props) => { )) )} - {entity && ( + {job?.status && toCatalogLink && ( - + + + )} + {job?.status === 'FAILED' && ( + + setOpen(false)}>Close )} diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index d8b803f893..0754b64d7f 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -23,18 +23,22 @@ import { Page, useApi, } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog'; +import { + catalogApiRef, + entityRoute, + entityRouteParams, +} from '@backstage/plugin-catalog'; import { LinearProgress } from '@material-ui/core'; import { IChangeEvent } from '@rjsf/core'; import React, { useState, useCallback } from 'react'; -import { Navigate } from 'react-router'; +import { generatePath, Navigate } from 'react-router'; import { useParams } from 'react-router-dom'; import { useAsync } from 'react-use'; import { scaffolderApiRef } from '../../api'; import { rootRoute } from '../../routes'; -import { Job } from '../../types'; import { JobStatusModal } from '../JobStatusModal'; import { MultistepJsonForm } from '../MultistepJsonForm'; +import { useJobPolling } from '../hooks/useJobPolling'; const useTemplate = ( templateName: string, @@ -81,10 +85,10 @@ export const TemplatePage = () => { const catalogApi = useApi(catalogApiRef); const scaffolderApi = useApi(scaffolderApiRef); const { templateName } = useParams(); + const [catalogLink, setCatalogLink] = useState(); const { template, loading } = useTemplate(templateName, catalogApi); - const [formState, setFormState] = useState({}); - + const [modalOpen, setModalOpen] = useState(false); const handleFormReset = () => setFormState({}); const handleChange = useCallback( (e: IChangeEvent) => setFormState({ ...formState, ...e.formData }), @@ -92,35 +96,42 @@ export const TemplatePage = () => { ); const [jobId, setJobId] = useState(null); - - const handleCreate = async () => { - try { - const job = await scaffolderApi.scaffold(templateName, formState); - setJobId(job); - } catch (e) { - errorApi.post(e); - } - }; - - const [entity, setEntity] = React.useState( - null, - ); - - const handleCreateComplete = async (job: Job) => { + const job = useJobPolling(jobId, async job => { if (!job.metadata.catalogInfoUrl) { errorApi.post( - new Error( - `Failed to find catalog-info.yaml file in ${job.metadata.remoteUrl}.`, - ), + new Error(`No catalogInfoUrl returned from the scaffolder`), ); return; } - const { - entities: [createdEntity], - } = await catalogApi.addLocation({ target: job.metadata.catalogInfoUrl }); + try { + const { + entities: [createdEntity], + } = await catalogApi.addLocation({ target: job.metadata.catalogInfoUrl }); - setEntity((createdEntity as any) as TemplateEntityV1alpha1); + const resolvedPath = generatePath( + `/catalog/${entityRoute.path}`, + entityRouteParams(createdEntity), + ); + + setCatalogLink(resolvedPath); + } catch (ex) { + errorApi.post( + new Error( + `Something went wrong trying to add the new 'catalog-info.yaml' to the catalog`, + ), + ); + } + }); + + const handleCreate = async () => { + try { + const jobId = await scaffolderApi.scaffold(templateName, formState); + setJobId(jobId); + setModalOpen(true); + } catch (e) { + errorApi.post(e); + } }; if (!loading && !template) { @@ -150,13 +161,7 @@ export const TemplatePage = () => { /> {loading && } - {jobId && ( - - )} + {modalOpen && } {template && ( Promise, ms: number) => { - let shouldStop = false; - (async () => { - while (!shouldStop) { - await thunk(); - await new Promise(res => setTimeout(res, ms)); - } - })(); - - return () => { - shouldStop = true; - }; -}; export const useJobPolling = ( jobId: string | null, + onFinish?: (j: Job) => void, pollingInterval = DEFAULT_POLLING_INTERVAL, ) => { const scaffolderApi = useApi(scaffolderApiRef); - const [job, setJob] = useState(null); - - useEffect(() => { - if (!jobId) return () => {}; - - const stopPolling = poll(async () => { - const nextJobState = await scaffolderApi.getJob(jobId); - if ( - nextJobState.status === 'FAILED' || - nextJobState.status === 'COMPLETED' - ) { - stopPolling(); + const [currentJob, setCurrentJob] = useState(null); + const shouldBeRunningInterval = + jobId && + currentJob?.status !== 'COMPLETED' && + currentJob?.status !== 'FAILED'; + useInterval( + async () => { + if (jobId) { + const job = await scaffolderApi.getJob(jobId); + if (job?.status === 'COMPLETED' || job?.status === 'FAILED') { + onFinish?.(job); + } + setCurrentJob(job); } - setJob(nextJobState); - }, pollingInterval); - return () => { - stopPolling(); - }; - }, [jobId, setJob, scaffolderApi, pollingInterval]); + }, + shouldBeRunningInterval ? pollingInterval : null, + ); - return job; + return currentJob; }; diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder/src/types.ts index e07581ad28..45672c603d 100644 --- a/plugins/scaffolder/src/types.ts +++ b/plugins/scaffolder/src/types.ts @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +export type JobStatus = 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED'; export type Job = { id: string; metadata: { @@ -21,7 +23,7 @@ export type Job = { remoteUrl?: string; catalogInfoUrl?: string; }; - status: 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED'; + status: JobStatus; stages: Stage[]; error?: Error; }; @@ -29,7 +31,7 @@ export type Job = { export type Stage = { name: string; log: string[]; - status: 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED'; + status: JobStatus; startedAt: string; endedAt?: string; }; From b877f46fde512dd7b7168c5977b65bdd01963982 Mon Sep 17 00:00:00 2001 From: blam Date: Sun, 22 Nov 2020 03:07:23 +0100 Subject: [PATCH 22/94] chore(lint): fixing linting for hooks --- plugins/scaffolder/src/components/hooks/useJobPolling.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder/src/components/hooks/useJobPolling.ts b/plugins/scaffolder/src/components/hooks/useJobPolling.ts index 91705c108f..1bfdfc53b8 100644 --- a/plugins/scaffolder/src/components/hooks/useJobPolling.ts +++ b/plugins/scaffolder/src/components/hooks/useJobPolling.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import { Job } from '../../types'; import { useApi } from '@backstage/core'; import { scaffolderApiRef } from '../../api'; From 9cdf4413db1a45c313eaa7a15d0689ff3914a8bd Mon Sep 17 00:00:00 2001 From: blam Date: Sun, 22 Nov 2020 03:20:42 +0100 Subject: [PATCH 23/94] chore(scaffolder): trying to fix the linting and tsc issues --- .../src/components/JobStatusModal/JobStatusModal.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx b/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx index f0aa5ccd81..01a6346d24 100644 --- a/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx +++ b/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx @@ -28,7 +28,7 @@ import { Job } from '../../types'; import { JobStage } from '../JobStage/JobStage'; type Props = { - job: Job; + job: Job | null; toCatalogLink?: string; }; @@ -47,6 +47,9 @@ export const JobStatusModal = ({ job, toCatalogLink }: Props) => { }; const onClose = useCallback(() => { + if (!job) { + return; + } if (job.status === 'COMPLETED' || job.status === 'FAILED') { setOpen(false); } From 031ead5f5a81a8004017327434ac44f4b3bc42bd Mon Sep 17 00:00:00 2001 From: Kevin Kelly Date: Sat, 21 Nov 2020 23:33:32 -0500 Subject: [PATCH 24/94] docs: update local install example for node v14 --- docs/getting-started/running-backstage-locally.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/getting-started/running-backstage-locally.md b/docs/getting-started/running-backstage-locally.md index a30a918fd0..6de9d234e3 100644 --- a/docs/getting-started/running-backstage-locally.md +++ b/docs/getting-started/running-backstage-locally.md @@ -8,19 +8,19 @@ description: Documentation on How to run Backstage Locally - Node.js -First make sure you are using Node.js with an Active LTS Release, currently v12. +First make sure you are using Node.js with an Active LTS Release, currently v14. This is made easy with a version manager such as [nvm](https://github.com/nvm-sh/nvm) which allows for version switching. ```bash # Installing a new version -nvm install 12 -> Downloading and installing node v12.18.3... -> Now using node v12.18.3 (npm v6.14.6) +nvm install 14 +> Downloading and installing node v14.15.1... +> Now using node v14.15.1 (npm v6.14.8) # Checking your version node --version -> v12.18.3 +> v14.15.1 ``` - Yarn From ed08863e17cb02b32242f5474c4d8295d1a94375 Mon Sep 17 00:00:00 2001 From: Kevin Kelly Date: Sat, 21 Nov 2020 23:48:36 -0500 Subject: [PATCH 25/94] docs: create an app, node version update --- docs/getting-started/create-an-app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/create-an-app.md b/docs/getting-started/create-an-app.md index 552b6c3ec2..6aef4c48c1 100644 --- a/docs/getting-started/create-an-app.md +++ b/docs/getting-started/create-an-app.md @@ -14,7 +14,7 @@ need to run Backstage in your own environment. To create a Backstage app, you will need to have [Node.js](https://nodejs.org/en/download/) Active LTS Release installed -(currently v12). +(currently v14). Backstage provides a utility for creating new apps. It guides you through the initial setup of selecting the name of the app and a database for the backend. From 0145faa6ee2faf8d4938c7d518f053a634009811 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 22 Nov 2020 14:20:13 +0100 Subject: [PATCH 26/94] cli: add Lockfile utility --- packages/cli/package.json | 3 + .../cli/src/lib/versioning/Lockfile.test.ts | 150 +++++++++++ packages/cli/src/lib/versioning/Lockfile.ts | 242 ++++++++++++++++++ packages/cli/src/lib/versioning/index.ts | 17 ++ yarn.lock | 10 + 5 files changed, 422 insertions(+) create mode 100644 packages/cli/src/lib/versioning/Lockfile.test.ts create mode 100644 packages/cli/src/lib/versioning/Lockfile.ts create mode 100644 packages/cli/src/lib/versioning/index.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index ec3aaacdee..eb56b19971 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -51,6 +51,7 @@ "@types/webpack-node-externals": "^2.5.0", "@typescript-eslint/eslint-plugin": "^v3.10.1", "@typescript-eslint/parser": "^v3.10.1", + "@yarnpkg/lockfile": "^1.1.0", "bfj": "^7.0.2", "chalk": "^4.0.0", "chokidar": "^3.3.1", @@ -92,6 +93,7 @@ "rollup-plugin-postcss": "^3.1.1", "rollup-plugin-typescript2": "^0.27.3", "rollup-pluginutils": "^2.8.2", + "semver": "^7.3.2", "start-server-webpack-plugin": "^2.2.5", "style-loader": "^1.2.1", "sucrase": "^3.16.0", @@ -131,6 +133,7 @@ "@types/tar": "^4.0.3", "@types/webpack": "^4.41.7", "@types/webpack-dev-server": "^3.11.0", + "@types/yarnpkg__lockfile": "^1.1.4", "del": "^5.1.0", "mock-fs": "^4.13.0", "nodemon": "^2.0.2", diff --git a/packages/cli/src/lib/versioning/Lockfile.test.ts b/packages/cli/src/lib/versioning/Lockfile.test.ts new file mode 100644 index 0000000000..5459ed53fb --- /dev/null +++ b/packages/cli/src/lib/versioning/Lockfile.test.ts @@ -0,0 +1,150 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import mockFs from 'mock-fs'; +import { Lockfile } from './Lockfile'; + +const HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + +`; + +const mockA = `${HEADER} +a@^1: + version "1.0.1" + resolved "https://my-registry/a-1.0.01.tgz#abc123" + integrity sha512-xyz + dependencies: + b "^2" + +b@2.0.x: + version "2.0.1" + +b@^2: + version "2.0.0" +`; + +const mockADedup = `${HEADER} +a@^1: + version "1.0.1" + resolved "https://my-registry/a-1.0.01.tgz#abc123" + integrity sha512-xyz + dependencies: + b "^2" + +b@2.0.x, b@^2: + version "2.0.1" +`; + +const mockB = `${HEADER} +"@s/a@*", "@s/a@1 || 2", "@s/a@^1": + version "1.0.1" + +"@s/a@^2.0.x": + version "2.0.0" +`; + +const mockBDedup = `${HEADER} +"@s/a@*", "@s/a@1 || 2", "@s/a@^2.0.x": + version "2.0.0" + +"@s/a@^1": + version "1.0.1" +`; + +describe('Lockfile', () => { + afterEach(() => { + mockFs.restore(); + }); + + it('should load and serialize mockA', async () => { + mockFs({ + '/yarn.lock': mockA, + }); + + const lockfile = await Lockfile.load('/yarn.lock'); + expect(lockfile.get('a')).toEqual([{ range: '^1', version: '1.0.1' }]); + expect(lockfile.get('b')).toEqual([ + { range: '2.0.x', version: '2.0.1' }, + { range: '^2', version: '2.0.0' }, + ]); + expect(lockfile.toString()).toBe(mockA); + }); + + it('should deduplicate mockA', async () => { + mockFs({ + '/yarn.lock': mockA, + }); + + const lockfile = await Lockfile.load('/yarn.lock'); + const result = lockfile.analyze(); + expect(result).toEqual({ + invalidRanges: [], + newRanges: [], + newVersions: [ + { + name: 'b', + range: '^2', + oldVersion: '2.0.0', + newVersion: '2.0.1', + }, + ], + }); + + expect(lockfile.toString()).toBe(mockA); + lockfile.replaceVersions(result.newVersions); + expect(lockfile.toString()).toBe(mockADedup); + }); + + it('should deduplicate mockB', async () => { + mockFs({ + '/yarn.lock': mockB, + }); + + const lockfile = await Lockfile.load('/yarn.lock'); + const result = lockfile.analyze(); + expect(result).toEqual({ + invalidRanges: [], + newRanges: [ + { + name: '@s/a', + oldRange: '^1', + newRange: '*', + oldVersion: '1.0.1', + newVersion: '2.0.0', + }, + ], + newVersions: [ + { + name: '@s/a', + range: '*', + oldVersion: '1.0.1', + newVersion: '2.0.0', + }, + { + name: '@s/a', + range: '1 || 2', + oldVersion: '1.0.1', + newVersion: '2.0.0', + }, + ], + }); + + expect(lockfile.toString()).toBe(mockB); + lockfile.replaceVersions(result.newVersions); + expect(lockfile.toString()).toBe(mockBDedup); + }); +}); diff --git a/packages/cli/src/lib/versioning/Lockfile.ts b/packages/cli/src/lib/versioning/Lockfile.ts new file mode 100644 index 0000000000..e03fa8ec3f --- /dev/null +++ b/packages/cli/src/lib/versioning/Lockfile.ts @@ -0,0 +1,242 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import semver from 'semver'; +import { + parse as parseLockfile, + stringify as stringifyLockfile, +} from '@yarnpkg/lockfile'; + +const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/; + +type LockfileData = { + [entry: string]: { + version: string; + resolved?: string; + integrity?: string; + dependencies?: { [name: string]: string }; + }; +}; + +type LockfileQueryEntry = { + range: string; + version: string; +}; + +/** Entries that have an invalid version range, for example an NPM tag */ +type AnalyzeResultInvalidRange = { + name: string; + range: string; +}; + +/** Entries that can be deduplicated by bumping to an existing higher version */ +type AnalyzeResultNewVersion = { + name: string; + range: string; + oldVersion: string; + newVersion: string; +}; + +/** Entries that {would need a dependency update in package.json to be deduplicated */ +type AnalyzeResultNewRange = { + name: string; + oldRange: string; + newRange: string; + oldVersion: string; + newVersion: string; +}; + +type AnalyzeResult = { + invalidRanges: AnalyzeResultInvalidRange[]; + newVersions: AnalyzeResultNewVersion[]; + newRanges: AnalyzeResultNewRange[]; +}; + +export class Lockfile { + static async load(path: string) { + const lockfileContents = await fs.readFile(path, 'utf8'); + const lockfile = parseLockfile(lockfileContents); + if (lockfile.type !== 'success') { + throw new Error(`Failed yarn.lock parse with ${lockfile.type}`); + } + + const data = lockfile.object as LockfileData; + const packages = new Map(); + + for (const [key, value] of Object.entries(data)) { + const [, name, range] = ENTRY_PATTERN.exec(key) ?? []; + if (!name) { + throw new Error(`Failed to parse yarn.lock entry '${key}'`); + } + + let queries = packages.get(name); + if (!queries) { + queries = []; + packages.set(name, queries); + } + queries.push({ range, version: value.version }); + } + + return new Lockfile(packages, data); + } + + constructor( + private readonly packages: Map, + private readonly data: LockfileData, + ) {} + + get(name: string): LockfileQueryEntry[] | undefined { + return this.packages.get(name); + } + + /** Analyzes the lockfile to identify possible actions and warnings for the entries */ + analyze(options?: { filter?: (name: string) => boolean }): AnalyzeResult { + const { filter } = options ?? {}; + const result: AnalyzeResult = { + invalidRanges: [], + newVersions: [], + newRanges: [], + }; + + for (const [name, allEntries] of this.packages) { + if (filter && !filter(name)) { + continue; + } + + // Get rid of an signal any invalid ranges upfront + const invalid = allEntries.filter(e => !semver.validRange(e.range)); + result.invalidRanges.push( + ...invalid.map(({ range }) => ({ name, range })), + ); + + // Grab all valid entries, if there isn't at least 2 different valid ones we're done + const entries = allEntries.filter(e => semver.validRange(e.range)); + if (entries.length < 2) { + continue; + } + + // Find all versions currently in use + const versions = Array.from( + new Set(entries.map(e => e.version)), + ).sort((v1, v2) => semver.rcompare(v1, v2)); + + // If we're not using at least 2 different versions we're done + if (versions.length < 2) { + continue; + } + + const acceptedVersions = new Set(); + for (const { version, range } of entries) { + // Finds the highest matching version from the the known versions + // TODO(Rugvip): We may want to select the version that satisfies the most ranges rather than the highest one + const acceptedVersion = versions.find(v => semver.satisfies(v, range)); + if (!acceptedVersion) { + throw new Error( + `No existing version was accepted for range ${range}, searching through ${versions}`, + ); + } + + if (acceptedVersion !== version) { + result.newVersions.push({ + name, + range, + newVersion: acceptedVersion, + oldVersion: version, + }); + } + + acceptedVersions.add(acceptedVersion); + } + + // If all ranges where able to accept the same version, we're done + if (acceptedVersions.size === 1) { + continue; + } + + // Find the max version and range that we may want bump older packages to + const maxVersion = Array.from(acceptedVersions).sort(semver.rcompare)[0]; + const maxEntry = entries.find(e => semver.satisfies(maxVersion, e.range)); + if (!maxEntry) { + throw new Error( + `No entry found that satisfies max version '${maxVersion}'`, + ); + } + + // Find all entries that don't satisfy the max version + for (const { version, range } of entries) { + if (semver.satisfies(maxVersion, range)) { + continue; + } + + result.newRanges.push({ + name, + oldRange: range, + newRange: maxEntry.range, + oldVersion: version, + newVersion: maxVersion, + }); + } + } + + return result; + } + + /** Modifies the lockfile by bumping packages to the suggested versions */ + replaceVersions(results: AnalyzeResultNewVersion[]) { + for (const { name, range, oldVersion, newVersion } of results) { + const query = `${name}@${range}`; + + // Update the backing data + const entryData = this.data[query]; + if (!entryData) { + throw new Error(`No entry data for ${query}`); + } + if (entryData.version !== oldVersion) { + throw new Error( + `Expected existing version data for ${query} to be ${oldVersion}, was ${entryData.version}`, + ); + } + + // Modifying the data in the entry is not enough, we need to reference an existing version object + const matchingEntry = Object.entries(this.data).find( + ([q, e]) => q.startsWith(`${name}@`) && e.version === newVersion, + ); + if (!matchingEntry) { + throw new Error( + `No matching entry found for ${name} at version ${newVersion}`, + ); + } + this.data[query] = matchingEntry[1]; + + // Update out internal data structure + const entry = this.packages.get(name)?.find(e => e.range === range); + if (!entry) { + throw new Error(`No entry data for ${query}`); + } + if (entry.version !== oldVersion) { + throw new Error( + `Expected existing version data for ${query} to be ${oldVersion}, was ${entryData.version}`, + ); + } + entry.version = newVersion; + } + } + + toString() { + return stringifyLockfile(this.data); + } +} diff --git a/packages/cli/src/lib/versioning/index.ts b/packages/cli/src/lib/versioning/index.ts new file mode 100644 index 0000000000..6f0a649ee4 --- /dev/null +++ b/packages/cli/src/lib/versioning/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { Lockfile } from './Lockfile'; diff --git a/yarn.lock b/yarn.lock index a90d885927..92aecbba36 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6088,6 +6088,11 @@ dependencies: "@types/yargs-parser" "*" +"@types/yarnpkg__lockfile@^1.1.4": + version "1.1.4" + resolved "https://registry.npmjs.org/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.4.tgz#445251eb00bd9c1e751f82c7c6bf4f714edfd464" + integrity sha512-/emrKCfQMQmFCqRqqBJ0JueHBT06jBRM3e8OgnvDUcvuExONujIk2hFA5dNsN9Nt41ljGVDdChvCydATZ+KOZw== + "@types/yup@^0.29.8": version "0.29.8" resolved "https://registry.npmjs.org/@types/yup/-/yup-0.29.8.tgz#83db15735987db9fe5a38772a0fb9500e3c5bf39" @@ -6384,6 +6389,11 @@ resolved "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== +"@yarnpkg/lockfile@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" + integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== + "@zkochan/cmd-shim@^3.1.0": version "3.1.0" resolved "https://registry.npmjs.org/@zkochan/cmd-shim/-/cmd-shim-3.1.0.tgz#2ab8ed81f5bb5452a85f25758eb9b8681982fd2e" From 4c642721376961ecc06c9abde42ba70534d9b63c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 22 Nov 2020 14:48:12 +0100 Subject: [PATCH 27/94] cli: make Lockfile analyze pick the most specific new version range --- packages/cli/src/lib/versioning/Lockfile.test.ts | 2 +- packages/cli/src/lib/versioning/Lockfile.ts | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/lib/versioning/Lockfile.test.ts b/packages/cli/src/lib/versioning/Lockfile.test.ts index 5459ed53fb..20847242d3 100644 --- a/packages/cli/src/lib/versioning/Lockfile.test.ts +++ b/packages/cli/src/lib/versioning/Lockfile.test.ts @@ -122,7 +122,7 @@ describe('Lockfile', () => { { name: '@s/a', oldRange: '^1', - newRange: '*', + newRange: '^2.0.x', oldVersion: '1.0.1', newVersion: '2.0.0', }, diff --git a/packages/cli/src/lib/versioning/Lockfile.ts b/packages/cli/src/lib/versioning/Lockfile.ts index e03fa8ec3f..8acb387704 100644 --- a/packages/cli/src/lib/versioning/Lockfile.ts +++ b/packages/cli/src/lib/versioning/Lockfile.ts @@ -167,9 +167,15 @@ export class Lockfile { continue; } - // Find the max version and range that we may want bump older packages to + // Find the max version that we may want bump older packages to const maxVersion = Array.from(acceptedVersions).sort(semver.rcompare)[0]; - const maxEntry = entries.find(e => semver.satisfies(maxVersion, e.range)); + // Find all existing ranges that satisfy the new max version, and pick the one that + // results in the highest minimum allowed version, usually being the more specific one + const maxEntry = entries + .filter(e => semver.satisfies(maxVersion, e.range)) + .map(e => ({ e, min: semver.minVersion(e.range) })) + .filter(p => p.min) + .sort((a, b) => semver.rcompare(a.min!, b.min!))[0]?.e; if (!maxEntry) { throw new Error( `No entry found that satisfies max version '${maxVersion}'`, From 46a20ad22fbfd2f5cf3965e8eedbf3a2118b6f35 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 22 Nov 2020 15:58:28 +0100 Subject: [PATCH 28/94] cli: add versions:lint command --- packages/cli/src/commands/index.ts | 6 ++ packages/cli/src/commands/versions/lint.ts | 114 +++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 packages/cli/src/commands/versions/lint.ts diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 1ebe518cac..3f3e8bfe83 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -154,6 +154,12 @@ export function registerCommands(program: CommanderStatic) { ) .action(lazy(() => import('./config/validate').then(m => m.default))); + program + .command('versions:lint') + .option('--fix', 'Fix any auto-fixable versioning problems') + .description('Lint Backstage package versioning') + .action(lazy(() => import('./versions/lint').then(m => m.default))); + program .command('prepack') .description('Prepares a package for packaging before publishing') diff --git a/packages/cli/src/commands/versions/lint.ts b/packages/cli/src/commands/versions/lint.ts new file mode 100644 index 0000000000..2f200a75a9 --- /dev/null +++ b/packages/cli/src/commands/versions/lint.ts @@ -0,0 +1,114 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import { Command } from 'commander'; +import { Lockfile } from '../../lib/versioning'; +import { paths } from '../../lib/paths'; +import partition from 'lodash/partition'; + +// Packages that we try to avoid duplicates for +const INCLUDED = [/^@backstage\//]; + +// Packages that are not allowed to have any duplicates +const FORBID_DUPLICATES = [ + /^@backstage\/core$/, + /^@backstage\/core-api$/, + /^@backstage\/plugin-/, +]; + +export default async (cmd: Command) => { + const fix = Boolean(cmd.fix); + + let success = true; + + const lockfilePath = paths.resolveTargetRoot('yarn.lock'); + const lockfile = await Lockfile.load(lockfilePath); + const result = lockfile.analyze({ + filter: name => INCLUDED.some(pattern => pattern.test(name)), + }); + + logArray( + result.invalidRanges, + "The following packages versions are invalid and can't be analyzed:", + e => ` ${e.name} @ ${e.range}`, + ); + + if (fix) { + lockfile.replaceVersions(result.newVersions); + + await fs.writeFile(lockfilePath, lockfile.toString(), 'utf8'); + } else { + const [ + newVersionsForbidden, + newVersionsAllowed, + ] = partition(result.newVersions, ({ name }) => + FORBID_DUPLICATES.some(pattern => pattern.test(name)), + ); + if (newVersionsForbidden.length && !fix) { + success = false; + } + + logArray( + newVersionsForbidden, + 'The following packages must be deduplicated, this can be done automatically with --fix', + e => + ` ${e.name} @ ${e.range} bumped from ${e.oldVersion} to ${e.newVersion}`, + ); + logArray( + newVersionsAllowed, + 'The following packages can be deduplicated, this can be done automatically with --fix', + e => + ` ${e.name} @ ${e.range} bumped from ${e.oldVersion} to ${e.newVersion}`, + ); + } + + const [newRangesForbidden, newRangesAllowed] = partition( + result.newRanges, + ({ name }) => FORBID_DUPLICATES.some(pattern => pattern.test(name)), + ); + if (newRangesForbidden.length) { + success = false; + } + + logArray( + newRangesForbidden, + 'The following packages must be deduplicated by updating dependencies in package.json', + e => ` ${e.name} @ ${e.oldRange} should be changed to ${e.newRange}`, + ); + logArray( + newRangesAllowed, + 'The following packages can be deduplicated by updating dependencies in package.json', + e => ` ${e.name} @ ${e.oldRange} should be changed to ${e.newRange}`, + ); + + if (!success) { + throw new Error('Failed versioning check'); + } +}; + +function logArray(arr: T[], header: string, each: (item: T) => void) { + if (arr.length === 0) { + return; + } + + console.log(header); + console.log(); + for (const e of arr) { + console.log(each(e)); + } + console.log(); +} From 06432a0e9973be53a73bd833e1b42eddb1901b5f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 22 Nov 2020 17:17:44 +0100 Subject: [PATCH 29/94] cli: add versions:bump command --- packages/cli/src/commands/index.ts | 5 + packages/cli/src/commands/versions/bump.ts | 199 +++++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 packages/cli/src/commands/versions/bump.ts diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 3f3e8bfe83..9f967a2727 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -154,6 +154,11 @@ export function registerCommands(program: CommanderStatic) { ) .action(lazy(() => import('./config/validate').then(m => m.default))); + program + .command('versions:bump') + .description('Bump Backstage packages to the latest versions') + .action(lazy(() => import('./versions/bump').then(m => m.default))); + program .command('versions:lint') .option('--fix', 'Fix any auto-fixable versioning problems') diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts new file mode 100644 index 0000000000..87f6d6c12b --- /dev/null +++ b/packages/cli/src/commands/versions/bump.ts @@ -0,0 +1,199 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import semver from 'semver'; +import { resolve as resolvePath } from 'path'; +import { run, runPlain } from '../../lib/run'; +import { paths } from '../../lib/paths'; +import { Lockfile } from '../../lib/versioning'; + +const PREFIX = '@backstage'; + +const DEP_TYPES = [ + 'dependencies', + 'devDependencies', + 'peerDependencies', + 'optionalDependencies', +]; + +// Package data as returned by `yarn info` +type YarnInfoInspectData = { + name: string; + 'dist-tags': { latest: string }; + versions: string[]; + time: { [version: string]: string }; +}; + +// Possible `yarn info` output +type YarnInfo = { + type: 'inspect'; + data: YarnInfoInspectData | { type: string; data: unknown }; +}; + +type PkgVersionInfo = { + range: string; + name: string; + location: string; +}; + +export default async () => { + const LernaProject = require('@lerna/project'); + const project = new LernaProject(paths.targetDir); + const packages = await project.getPackages(); + + // First we discover all Backstage dependencies within our own repo + const dependencyMap = new Map(); + for (const pkg of packages) { + const deps = DEP_TYPES.flatMap( + t => Object.entries(pkg.get(t) ?? {}) as [string, string][], + ); + + for (const [name, range] of deps) { + if (name.startsWith(PREFIX)) { + dependencyMap.set( + name, + (dependencyMap.get(name) ?? []).concat({ + range, + name: pkg.name, + location: pkg.location, + }), + ); + } + } + } + + // Next check with the package registry what the latest version of all of those dependencies are + const targetVersions = new Map(); + await workerThreads(16, dependencyMap.keys(), async name => { + console.log(`Checking for updates of ${name}`); + const output = await runPlain('yarn', 'info', '--json', name); + const info = JSON.parse(output) as YarnInfo; + if (info.type !== 'inspect') { + throw new Error(`Received unknown yarn info for ${name}, ${output}`); + } + + const data = info.data as YarnInfoInspectData; + const latest = data['dist-tags'].latest; + if (!latest) { + throw new Error(`No latest version found for ${name}`); + } + + targetVersions.set(name, latest); + }); + + // Then figure out which local packages need to have their dependencies bumped + const versionBumps = new Map(); + for (const [name, pkgs] of dependencyMap) { + const targetVersion = targetVersions.get(name)!; + for (const pkg of pkgs) { + if (semver.satisfies(targetVersion, pkg.range)) { + continue; + } + + versionBumps.set( + pkg.name, + (versionBumps.get(pkg.name) ?? []).concat({ + name, + location: pkg.location, + range: `^${targetVersion}`, // TODO(Rugvip): Option to use something else than ^? + }), + ); + } + } + + console.log(); + + // Write all discovered version bumps to package.json in this repo + if (versionBumps.size === 0) { + console.log('All Backstage packages are up to date!'); + } else { + console.log('Some packages are outdated, updating'); + console.log(); + + await workerThreads(16, versionBumps.entries(), async ([name, deps]) => { + const pkgPath = resolvePath(deps[0].location, 'package.json'); + const pkgJson = await fs.readJson(pkgPath); + + for (const dep of deps) { + console.log(`Bumping ${dep.name} in ${name} to ${dep.range}`); + + for (const depType of DEP_TYPES) { + if (depType in pkgJson && dep.name in pkgJson[depType]) { + pkgJson[depType][dep.name] = dep.range; + } + } + } + + await fs.writeJson(pkgPath, pkgJson, { spaces: 2 }); + }); + + console.log(); + console.log("Running 'yarn install' to install new versions"); + console.log(); + await run('yarn', ['install']); + } + + console.log(); + + // Finally we make sure the new lockfile doesn't have any duplicates + const lockfilePath = paths.resolveTargetRoot('yarn.lock'); + const lockfile = await Lockfile.load(lockfilePath); + const result = lockfile.analyze({ + filter: name => dependencyMap.has(name), + }); + + if (result.newVersions.length > 0) { + console.log(); + console.log('Removing duplicate dependencies from yarn.lock'); + lockfile.replaceVersions(result.newVersions); + await fs.writeFile(lockfilePath, lockfile.toString(), 'utf8'); + + console.log( + "Running 'yarn install' to remove duplicates from node_modules", + ); + console.log(); + await run('yarn', ['install']); + + console.log(); + } + + if (result.newRanges.length > 0) { + throw new Error( + `Version bump failed for ${result.newRanges.map(i => i.name).join(', ')}`, + ); + } +}; + +async function workerThreads( + count: number, + items: IterableIterator, + fn: (item: T) => Promise, +) { + const queue = Array.from(items); + + async function pop() { + const item = queue.pop()!; + if (!item) { + return; + } + + await fn(item); + await pop(); + } + + return Promise.all(Array(count).fill(0).map(pop)); +} From b90ad40d1c6c4e3e235fca9c1b5a652eedc625b8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 22 Nov 2020 17:42:52 +0100 Subject: [PATCH 30/94] cli: add Lockfile.save --- packages/cli/src/commands/versions/bump.ts | 11 +++++++---- packages/cli/src/commands/versions/lint.ts | 7 ++----- packages/cli/src/lib/versioning/Lockfile.test.ts | 7 ++++++- packages/cli/src/lib/versioning/Lockfile.ts | 9 +++++++-- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index 87f6d6c12b..c2db4eff6e 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -150,8 +150,7 @@ export default async () => { console.log(); // Finally we make sure the new lockfile doesn't have any duplicates - const lockfilePath = paths.resolveTargetRoot('yarn.lock'); - const lockfile = await Lockfile.load(lockfilePath); + const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); const result = lockfile.analyze({ filter: name => dependencyMap.has(name), }); @@ -160,7 +159,7 @@ export default async () => { console.log(); console.log('Removing duplicate dependencies from yarn.lock'); lockfile.replaceVersions(result.newVersions); - await fs.writeFile(lockfilePath, lockfile.toString(), 'utf8'); + await lockfile.save(); console.log( "Running 'yarn install' to remove duplicates from node_modules", @@ -195,5 +194,9 @@ async function workerThreads( await pop(); } - return Promise.all(Array(count).fill(0).map(pop)); + return Promise.all( + Array(count) + .fill(0) + .map(pop), + ); } diff --git a/packages/cli/src/commands/versions/lint.ts b/packages/cli/src/commands/versions/lint.ts index 2f200a75a9..f91d2b74eb 100644 --- a/packages/cli/src/commands/versions/lint.ts +++ b/packages/cli/src/commands/versions/lint.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import fs from 'fs-extra'; import { Command } from 'commander'; import { Lockfile } from '../../lib/versioning'; import { paths } from '../../lib/paths'; @@ -35,8 +34,7 @@ export default async (cmd: Command) => { let success = true; - const lockfilePath = paths.resolveTargetRoot('yarn.lock'); - const lockfile = await Lockfile.load(lockfilePath); + const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); const result = lockfile.analyze({ filter: name => INCLUDED.some(pattern => pattern.test(name)), }); @@ -49,8 +47,7 @@ export default async (cmd: Command) => { if (fix) { lockfile.replaceVersions(result.newVersions); - - await fs.writeFile(lockfilePath, lockfile.toString(), 'utf8'); + await lockfile.save(); } else { const [ newVersionsForbidden, diff --git a/packages/cli/src/lib/versioning/Lockfile.test.ts b/packages/cli/src/lib/versioning/Lockfile.test.ts index 20847242d3..89126c7ce1 100644 --- a/packages/cli/src/lib/versioning/Lockfile.test.ts +++ b/packages/cli/src/lib/versioning/Lockfile.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { Lockfile } from './Lockfile'; @@ -84,7 +85,7 @@ describe('Lockfile', () => { expect(lockfile.toString()).toBe(mockA); }); - it('should deduplicate mockA', async () => { + it('should deduplicate and save mockA', async () => { mockFs({ '/yarn.lock': mockA, }); @@ -107,6 +108,10 @@ describe('Lockfile', () => { expect(lockfile.toString()).toBe(mockA); lockfile.replaceVersions(result.newVersions); expect(lockfile.toString()).toBe(mockADedup); + + await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockA); + await expect(lockfile.save()).resolves.toBeUndefined(); + await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockADedup); }); it('should deduplicate mockB', async () => { diff --git a/packages/cli/src/lib/versioning/Lockfile.ts b/packages/cli/src/lib/versioning/Lockfile.ts index 8acb387704..59a734c314 100644 --- a/packages/cli/src/lib/versioning/Lockfile.ts +++ b/packages/cli/src/lib/versioning/Lockfile.ts @@ -91,10 +91,11 @@ export class Lockfile { queries.push({ range, version: value.version }); } - return new Lockfile(packages, data); + return new Lockfile(path, packages, data); } - constructor( + private constructor( + private readonly path: string, private readonly packages: Map, private readonly data: LockfileData, ) {} @@ -242,6 +243,10 @@ export class Lockfile { } } + async save() { + await fs.writeFile(this.path, this.toString(), 'utf8'); + } + toString() { return stringifyLockfile(this.data); } From 99134596986b1e5c7b1594a2c50b60d8a13d3995 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 22 Nov 2020 17:44:25 +0100 Subject: [PATCH 31/94] changesets: add cli versions command change --- .changeset/poor-mails-marry.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/poor-mails-marry.md diff --git a/.changeset/poor-mails-marry.md b/.changeset/poor-mails-marry.md new file mode 100644 index 0000000000..586bf91c47 --- /dev/null +++ b/.changeset/poor-mails-marry.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Add new `versions:lint` and `versions:bump` commands to simplify version management and avoid conflicts From c0f7591dd7ab805ef16e1be727636acbcd7110fd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 22 Nov 2020 18:17:55 +0100 Subject: [PATCH 32/94] cli: refactor out parts of versioning:bump + add tests --- packages/cli/src/commands/versions/bump.ts | 58 ++-------- packages/cli/src/lib/versioning/index.ts | 1 + .../cli/src/lib/versioning/packages.test.ts | 109 ++++++++++++++++++ packages/cli/src/lib/versioning/packages.ts | 90 +++++++++++++++ 4 files changed, 209 insertions(+), 49 deletions(-) create mode 100644 packages/cli/src/lib/versioning/packages.test.ts create mode 100644 packages/cli/src/lib/versioning/packages.ts diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index c2db4eff6e..1d8b9cecf3 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -17,11 +17,13 @@ import fs from 'fs-extra'; import semver from 'semver'; import { resolve as resolvePath } from 'path'; -import { run, runPlain } from '../../lib/run'; +import { run } from '../../lib/run'; import { paths } from '../../lib/paths'; -import { Lockfile } from '../../lib/versioning'; - -const PREFIX = '@backstage'; +import { + mapDependencies, + fetchPackageInfo, + Lockfile, +} from '../../lib/versioning'; const DEP_TYPES = [ 'dependencies', @@ -30,20 +32,6 @@ const DEP_TYPES = [ 'optionalDependencies', ]; -// Package data as returned by `yarn info` -type YarnInfoInspectData = { - name: string; - 'dist-tags': { latest: string }; - versions: string[]; - time: { [version: string]: string }; -}; - -// Possible `yarn info` output -type YarnInfo = { - type: 'inspect'; - data: YarnInfoInspectData | { type: string; data: unknown }; -}; - type PkgVersionInfo = { range: string; name: string; @@ -51,43 +39,15 @@ type PkgVersionInfo = { }; export default async () => { - const LernaProject = require('@lerna/project'); - const project = new LernaProject(paths.targetDir); - const packages = await project.getPackages(); - // First we discover all Backstage dependencies within our own repo - const dependencyMap = new Map(); - for (const pkg of packages) { - const deps = DEP_TYPES.flatMap( - t => Object.entries(pkg.get(t) ?? {}) as [string, string][], - ); - - for (const [name, range] of deps) { - if (name.startsWith(PREFIX)) { - dependencyMap.set( - name, - (dependencyMap.get(name) ?? []).concat({ - range, - name: pkg.name, - location: pkg.location, - }), - ); - } - } - } + const dependencyMap = await mapDependencies(); // Next check with the package registry what the latest version of all of those dependencies are const targetVersions = new Map(); await workerThreads(16, dependencyMap.keys(), async name => { console.log(`Checking for updates of ${name}`); - const output = await runPlain('yarn', 'info', '--json', name); - const info = JSON.parse(output) as YarnInfo; - if (info.type !== 'inspect') { - throw new Error(`Received unknown yarn info for ${name}, ${output}`); - } - - const data = info.data as YarnInfoInspectData; - const latest = data['dist-tags'].latest; + const info = await fetchPackageInfo(name); + const latest = info['dist-tags'].latest; if (!latest) { throw new Error(`No latest version found for ${name}`); } diff --git a/packages/cli/src/lib/versioning/index.ts b/packages/cli/src/lib/versioning/index.ts index 6f0a649ee4..71fb7647ce 100644 --- a/packages/cli/src/lib/versioning/index.ts +++ b/packages/cli/src/lib/versioning/index.ts @@ -15,3 +15,4 @@ */ export { Lockfile } from './Lockfile'; +export { fetchPackageInfo, mapDependencies } from './packages'; diff --git a/packages/cli/src/lib/versioning/packages.test.ts b/packages/cli/src/lib/versioning/packages.test.ts new file mode 100644 index 0000000000..ba328fa356 --- /dev/null +++ b/packages/cli/src/lib/versioning/packages.test.ts @@ -0,0 +1,109 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import mockFs from 'mock-fs'; +import * as runObj from '../run'; +import { paths } from '../paths'; +import { fetchPackageInfo, mapDependencies } from './packages'; + +describe('fetchPackageInfo', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should forward info', async () => { + jest + .spyOn(runObj, 'runPlain') + .mockResolvedValue(`{"type":"inspect","data":{"the":"data"}}`); + + await expect(fetchPackageInfo('my-package')).resolves.toEqual({ + the: 'data', + }); + expect(runObj.runPlain).toHaveBeenCalledWith( + 'yarn', + 'info', + '--json', + 'my-package', + ); + }); +}); + +describe('mapDependencies', () => { + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + + it('should derp', async () => { + // Make sure all modules involved in package discovery are in the module cache before we mock fs + const LernaProject = require('@lerna/project'); + const project = new LernaProject(paths.targetDir); + await project.getPackages(); + + mockFs({ + '/lerna.json': JSON.stringify({ + packages: ['pkgs/*'], + }), + '/pkgs/a/package.json': JSON.stringify({ + name: 'a', + dependencies: { + '@backstage/core': '1 || 2', + }, + }), + '/pkgs/b/package.json': JSON.stringify({ + name: 'b', + dependencies: { + '@backstage/core': '3', + '@backstage/cli': '^0', + }, + }), + }); + + const oldDir = paths.targetDir; + paths.targetDir = '/'; + + const dependencyMap = await mapDependencies(); + expect(Array.from(dependencyMap)).toEqual([ + [ + '@backstage/core', + [ + { + name: 'a', + range: '1 || 2', + location: '/pkgs/a', + }, + { + name: 'b', + range: '3', + location: '/pkgs/b', + }, + ], + ], + [ + '@backstage/cli', + [ + { + name: 'b', + range: '^0', + location: '/pkgs/b', + }, + ], + ], + ]); + + paths.targetDir = oldDir; + }); +}); diff --git a/packages/cli/src/lib/versioning/packages.ts b/packages/cli/src/lib/versioning/packages.ts new file mode 100644 index 0000000000..239fe575d6 --- /dev/null +++ b/packages/cli/src/lib/versioning/packages.ts @@ -0,0 +1,90 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { runPlain } from '../../lib/run'; +import { paths } from '../../lib/paths'; + +const PREFIX = '@backstage'; + +const DEP_TYPES = [ + 'dependencies', + 'devDependencies', + 'peerDependencies', + 'optionalDependencies', +]; + +// Package data as returned by `yarn info` +type YarnInfoInspectData = { + name: string; + 'dist-tags': { latest: string }; + versions: string[]; + time: { [version: string]: string }; +}; + +// Possible `yarn info` output +type YarnInfo = { + type: 'inspect'; + data: YarnInfoInspectData | { type: string; data: unknown }; +}; + +type PkgVersionInfo = { + range: string; + name: string; + location: string; +}; + +export async function fetchPackageInfo( + name: string, +): Promise { + const output = await runPlain('yarn', 'info', '--json', name); + const info = JSON.parse(output) as YarnInfo; + if (info.type !== 'inspect') { + throw new Error(`Received unknown yarn info for ${name}, ${output}`); + } + + return info.data as YarnInfoInspectData; +} + +/** Map all dependencies in the repo as dependency => dependents */ +export async function mapDependencies(): Promise< + Map +> { + const LernaProject = require('@lerna/project'); + const project = new LernaProject(paths.targetDir); + const packages = await project.getPackages(); + + const dependencyMap = new Map(); + for (const pkg of packages) { + const deps = DEP_TYPES.flatMap( + t => Object.entries(pkg.get(t) ?? {}) as [string, string][], + ); + + for (const [name, range] of deps) { + if (name.startsWith(PREFIX)) { + dependencyMap.set( + name, + (dependencyMap.get(name) ?? []).concat({ + range, + name: pkg.name, + location: pkg.location, + }), + ); + } + } + } + + return dependencyMap; +} From 1af14032a090d599e6e9fc438350eb18528a21f1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 22 Nov 2020 18:32:32 +0100 Subject: [PATCH 33/94] cli: rename versions:lint to versions:check --- .changeset/poor-mails-marry.md | 2 +- packages/cli/src/commands/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/poor-mails-marry.md b/.changeset/poor-mails-marry.md index 586bf91c47..fdc7e6f1b8 100644 --- a/.changeset/poor-mails-marry.md +++ b/.changeset/poor-mails-marry.md @@ -2,4 +2,4 @@ '@backstage/cli': patch --- -Add new `versions:lint` and `versions:bump` commands to simplify version management and avoid conflicts +Add new `versions:check` and `versions:bump` commands to simplify version management and avoid conflicts diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 9f967a2727..60c1b45cfc 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -160,9 +160,9 @@ export function registerCommands(program: CommanderStatic) { .action(lazy(() => import('./versions/bump').then(m => m.default))); program - .command('versions:lint') + .command('versions:check') .option('--fix', 'Fix any auto-fixable versioning problems') - .description('Lint Backstage package versioning') + .description('Check Backstage package versioning') .action(lazy(() => import('./versions/lint').then(m => m.default))); program From 351539680bcf0eb1406591a0e81590cd89eb8114 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 22 Nov 2020 20:37:06 +0100 Subject: [PATCH 34/94] cli: review feedback on versions:* commands --- packages/cli/src/commands/versions/bump.ts | 2 +- packages/cli/src/commands/versions/lint.ts | 2 +- packages/cli/src/lib/versioning/Lockfile.ts | 10 +++++----- packages/cli/src/lib/versioning/packages.test.ts | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index 1d8b9cecf3..526ab9d4cf 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -145,7 +145,7 @@ async function workerThreads( const queue = Array.from(items); async function pop() { - const item = queue.pop()!; + const item = queue.pop(); if (!item) { return; } diff --git a/packages/cli/src/commands/versions/lint.ts b/packages/cli/src/commands/versions/lint.ts index f91d2b74eb..e2c36ba094 100644 --- a/packages/cli/src/commands/versions/lint.ts +++ b/packages/cli/src/commands/versions/lint.ts @@ -97,7 +97,7 @@ export default async (cmd: Command) => { } }; -function logArray(arr: T[], header: string, each: (item: T) => void) { +function logArray(arr: T[], header: string, each: (item: T) => string) { if (arr.length === 0) { return; } diff --git a/packages/cli/src/lib/versioning/Lockfile.ts b/packages/cli/src/lib/versioning/Lockfile.ts index 59a734c314..893beca6e2 100644 --- a/packages/cli/src/lib/versioning/Lockfile.ts +++ b/packages/cli/src/lib/versioning/Lockfile.ts @@ -51,7 +51,7 @@ type AnalyzeResultNewVersion = { newVersion: string; }; -/** Entries that {would need a dependency update in package.json to be deduplicated */ +/** Entries that would need a dependency update in package.json to be deduplicated */ type AnalyzeResultNewRange = { name: string; oldRange: string; @@ -118,13 +118,13 @@ export class Lockfile { continue; } - // Get rid of an signal any invalid ranges upfront + // Get rid of and signal any invalid ranges upfront const invalid = allEntries.filter(e => !semver.validRange(e.range)); result.invalidRanges.push( ...invalid.map(({ range }) => ({ name, range })), ); - // Grab all valid entries, if there isn't at least 2 different valid ones we're done + // Grab all valid entries, if there aren't at least 2 different valid ones we're done const entries = allEntries.filter(e => semver.validRange(e.range)); if (entries.length < 2) { continue; @@ -163,7 +163,7 @@ export class Lockfile { acceptedVersions.add(acceptedVersion); } - // If all ranges where able to accept the same version, we're done + // If all ranges were able to accept the same version, we're done if (acceptedVersions.size === 1) { continue; } @@ -229,7 +229,7 @@ export class Lockfile { } this.data[query] = matchingEntry[1]; - // Update out internal data structure + // Update our internal data structure const entry = this.packages.get(name)?.find(e => e.range === range); if (!entry) { throw new Error(`No entry data for ${query}`); diff --git a/packages/cli/src/lib/versioning/packages.test.ts b/packages/cli/src/lib/versioning/packages.test.ts index ba328fa356..fd37d7af95 100644 --- a/packages/cli/src/lib/versioning/packages.test.ts +++ b/packages/cli/src/lib/versioning/packages.test.ts @@ -47,7 +47,7 @@ describe('mapDependencies', () => { jest.resetAllMocks(); }); - it('should derp', async () => { + it('should read dependencies', async () => { // Make sure all modules involved in package discovery are in the module cache before we mock fs const LernaProject = require('@lerna/project'); const project = new LernaProject(paths.targetDir); From 0c597a8d62b448a152c951bdcee988a84657cba1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 22 Nov 2020 20:18:19 +0100 Subject: [PATCH 35/94] cli: work around flaky formatting in bump.ts --- packages/cli/src/commands/versions/bump.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index 526ab9d4cf..eec8abed9b 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -157,6 +157,6 @@ async function workerThreads( return Promise.all( Array(count) .fill(0) - .map(pop), + .map(() => pop()), ); } From 5c8e99ce191b6e5fb9b14771fe5eb64cdbc6ad62 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Sun, 22 Nov 2020 20:38:57 -0500 Subject: [PATCH 36/94] Fix entity YAML name --- docs/features/software-catalog/descriptor-format.md | 2 +- plugins/circleci/README.md | 2 +- plugins/jenkins/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 5d7a5b9af0..c3adaabc45 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -43,7 +43,7 @@ software catalog API. "kind": "Component", "metadata": { "annotations": { - "backstage.io/managed-by-location": "file:/tmp/component-info.yaml", + "backstage.io/managed-by-location": "file:/tmp/catalog-info.yaml", "example.com/service-discovery": "artistweb", "circleci.com/project-slug": "github/example-org/artist-website" }, diff --git a/plugins/circleci/README.md b/plugins/circleci/README.md index 00a39ce6c8..c96de56617 100644 --- a/plugins/circleci/README.md +++ b/plugins/circleci/README.md @@ -61,7 +61,7 @@ proxy: ``` 5. Get and provide `CIRCLECI_AUTH_TOKEN` as env variable (https://circleci.com/docs/api/#add-an-api-token) -6. Add `circleci.com/project-slug` annotation to your component-info.yaml file in format // (https://backstage.io/docs/architecture-decisions/adrs-adr002#format) +6. Add `circleci.com/project-slug` annotation to your catalog-info.yaml file in format // (https://backstage.io/docs/architecture-decisions/adrs-adr002#format) ```yaml apiVersion: backstage.io/v1alpha1 diff --git a/plugins/jenkins/README.md b/plugins/jenkins/README.md index 1d3e7f60e5..5e0a701caf 100644 --- a/plugins/jenkins/README.md +++ b/plugins/jenkins/README.md @@ -41,7 +41,7 @@ export JENKINS_BASIC_AUTH_HEADER="Basic $HEADER" ``` 5. Run app with `yarn start` -6. Add the Jenkins folder annotation to your `component-info.yaml`, (note: currently this plugin only supports folders and Git SCM) +6. Add the Jenkins folder annotation to your `catalog-info.yaml`, (note: currently this plugin only supports folders and Git SCM) ```yaml apiVersion: backstage.io/v1alpha1 From da8ca6cd1c13e13390b36da8f9ae84b7d0396317 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Sun, 22 Nov 2020 20:51:05 -0500 Subject: [PATCH 37/94] Fix typo --- plugins/circleci/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/circleci/README.md b/plugins/circleci/README.md index c96de56617..4802e8bee2 100644 --- a/plugins/circleci/README.md +++ b/plugins/circleci/README.md @@ -88,4 +88,4 @@ spec: ## Limitations - CircleCI has pretty strict rate limits per token, be careful with opened tabs -- CircelCI doesn't provide a way to auth by 3rd party (e.g. GitHub) token, nor by calling their OAuth endpoints, which currently stands in the way of better auth integration with Backstage (https://discuss.circleci.com/t/circleci-api-authorization-with-github-token/5356) +- CircleCI doesn't provide a way to auth by 3rd party (e.g. GitHub) token, nor by calling their OAuth endpoints, which currently stands in the way of better auth integration with Backstage (https://discuss.circleci.com/t/circleci-api-authorization-with-github-token/5356) From fe3619782bbbbe2cc5d4b2a2b0fca731315d88da Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Sun, 22 Nov 2020 20:57:40 -0500 Subject: [PATCH 38/94] Change entity YAML name --- .../scaffolder-backend/src/scaffolder/stages/templater/cra.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cra.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cra.ts index da33149c24..0fcd8a4f15 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cra.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cra.ts @@ -48,7 +48,7 @@ export class CreateReactAppTemplater implements TemplaterBase { }, }); - // Need to also make a component-info.yaml to store the data about the service. + // Need to also make a catalog-info.yaml to store the data about the service. const componentInfo = { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -69,7 +69,7 @@ export class CreateReactAppTemplater implements TemplaterBase { ); await fs.promises.writeFile( - `${finalDir}/component-info.yaml`, + `${finalDir}/catalog-info.yaml`, yaml.stringify(componentInfo), ); From 5a1d8dca38ffa26303e8a9ab2bf02b214bb1eda7 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Sun, 22 Nov 2020 21:01:08 -0500 Subject: [PATCH 39/94] Add changeset --- .changeset/tidy-brooms-look.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tidy-brooms-look.md diff --git a/.changeset/tidy-brooms-look.md b/.changeset/tidy-brooms-look.md new file mode 100644 index 0000000000..f2c2a9064e --- /dev/null +++ b/.changeset/tidy-brooms-look.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Fix React entity YAML filename to new standard From af45dc5320457f660b408dfe4b91f0a909de7c2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Nov 2020 06:01:33 +0000 Subject: [PATCH 40/94] build(deps-dev): bump eslint-plugin-cypress from 2.11.1 to 2.11.2 Bumps [eslint-plugin-cypress](https://github.com/cypress-io/eslint-plugin-cypress) from 2.11.1 to 2.11.2. - [Release notes](https://github.com/cypress-io/eslint-plugin-cypress/releases) - [Commits](https://github.com/cypress-io/eslint-plugin-cypress/compare/v2.11.1...v2.11.2) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index bd1757cea0..bdf52a4bd5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11122,9 +11122,9 @@ eslint-module-utils@^2.1.1, eslint-module-utils@^2.6.0: pkg-dir "^2.0.0" eslint-plugin-cypress@^2.10.3: - version "2.11.1" - resolved "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.11.1.tgz#a945e2774b88211e2c706a059d431e262b5c2862" - integrity sha512-MxMYoReSO5+IZMGgpBZHHSx64zYPSPTpXDwsgW7ChlJTF/sA+obqRbHplxD6sBStE+g4Mi0LCLkG4t9liu//mQ== + version "2.11.2" + resolved "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.11.2.tgz#a8f3fe7ec840f55e4cea37671f93293e6c3e76a0" + integrity sha512-1SergF1sGbVhsf7MYfOLiBhdOg6wqyeV9pXUAIDIffYTGMN3dTBQS9nFAzhLsHhO+Bn0GaVM1Ecm71XUidQ7VA== dependencies: globals "^11.12.0" From 0e352e981ddcb3619c24360449b0488bae8fae22 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 23 Nov 2020 09:33:09 +0100 Subject: [PATCH 41/94] Make clicking on the item to execute search work --- packages/core/src/layout/Sidebar/Items.tsx | 35 ++++++++++++++-------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/packages/core/src/layout/Sidebar/Items.tsx b/packages/core/src/layout/Sidebar/Items.tsx index b8786886cc..929828cee4 100644 --- a/packages/core/src/layout/Sidebar/Items.tsx +++ b/packages/core/src/layout/Sidebar/Items.tsx @@ -14,23 +14,23 @@ * limitations under the License. */ +import { IconComponent } from '@backstage/core-api'; +import { BackstageTheme } from '@backstage/theme'; import { + Badge, makeStyles, styled, TextField, Typography, - Badge, } from '@material-ui/core'; -import { BackstageTheme } from '@backstage/theme'; -import { IconComponent } from '@backstage/core-api'; import SearchIcon from '@material-ui/icons/Search'; import clsx from 'clsx'; import React, { + forwardRef, + KeyboardEventHandler, + ReactNode, useContext, useState, - KeyboardEventHandler, - forwardRef, - ReactNode, } from 'react'; import { NavLink } from 'react-router-dom'; import { sidebarConfig, SidebarContext } from './config'; @@ -120,7 +120,7 @@ type SidebarItemProps = { // If 'to' is set the item will act as a nav link with highlight, otherwise it's just a button to?: string; hasNotifications?: boolean; - onClick?: () => void; + onClick?: (ev: React.MouseEvent) => void; children?: ReactNode; }; @@ -218,10 +218,14 @@ export const SidebarSearchField = (props: SidebarSearchFieldProps) => { const [input, setInput] = useState(''); const classes = useStyles(); + const search = () => { + props.onSearch(input); + setInput(''); + }; + const handleEnter: KeyboardEventHandler = ev => { if (ev.key === 'Enter') { - props.onSearch(input); - setInput(''); + search(); } }; @@ -229,18 +233,25 @@ export const SidebarSearchField = (props: SidebarSearchFieldProps) => { setInput(ev.target.value); }; - const handleClick = (ev: React.MouseEvent) => { + const handleInputClick = (ev: React.MouseEvent) => { // Clicking into the search fields shouldn't navigate to the search page ev.preventDefault(); + ev.stopPropagation(); + }; + + const handleItemClick = (ev: React.MouseEvent) => { + // Clicking on the search icon while should execute a query with the current field content + search(); + ev.preventDefault(); }; return (
- + Date: Mon, 23 Nov 2020 11:25:45 +0100 Subject: [PATCH 42/94] backend-common: make test-utils a dev dep --- packages/backend-common/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index d4f0052d28..b22e4638f5 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -33,7 +33,6 @@ "@backstage/config": "^0.1.1", "@backstage/config-loader": "^0.3.0", "@backstage/integration": "^0.1.1", - "@backstage/test-utils": "^0.1.3", "@types/cors": "^2.8.6", "@types/express": "^4.17.6", "archiver": "^5.0.2", @@ -69,6 +68,7 @@ }, "devDependencies": { "@backstage/cli": "^0.3.0", + "@backstage/test-utils": "^0.1.3", "@types/archiver": "^3.1.1", "@types/compression": "^1.7.0", "@types/concat-stream": "^1.6.0", From 31d8b6979d99f627d678a2cdedde06f32ea1e0c3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 23 Nov 2020 11:29:26 +0100 Subject: [PATCH 43/94] cli: add experimental backend:bundle command --- .changeset/orange-cherries-run.md | 5 +++ .gitignore | 1 + packages/cli/src/commands/backend/bundle.ts | 39 +++++++++++++++++++++ packages/cli/src/commands/index.ts | 5 +++ 4 files changed, 50 insertions(+) create mode 100644 .changeset/orange-cherries-run.md create mode 100644 packages/cli/src/commands/backend/bundle.ts diff --git a/.changeset/orange-cherries-run.md b/.changeset/orange-cherries-run.md new file mode 100644 index 0000000000..3c06c27b47 --- /dev/null +++ b/.changeset/orange-cherries-run.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Add experimental backend:bundle command diff --git a/.gitignore b/.gitignore index 5c27601791..3334bf956d 100644 --- a/.gitignore +++ b/.gitignore @@ -96,6 +96,7 @@ typings/ .nuxt dist dist-types +dist-workspace # Gatsby files .cache/ diff --git a/packages/cli/src/commands/backend/bundle.ts b/packages/cli/src/commands/backend/bundle.ts new file mode 100644 index 0000000000..e604ae9733 --- /dev/null +++ b/packages/cli/src/commands/backend/bundle.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Command } from 'commander'; +import fs from 'fs-extra'; +import { createDistWorkspace } from '../../lib/packager'; +import { paths } from '../../lib/paths'; +import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel'; + +const PKG_PATH = 'package.json'; +const TARGET_DIR = 'dist-workspace'; + +export default async (cmd: Command) => { + const targetDir = paths.resolveTarget(TARGET_DIR); + const pkgPath = paths.resolveTarget(PKG_PATH); + const pkg = await fs.readJson(pkgPath); + + await fs.remove(targetDir); + await fs.mkdir(targetDir); + await createDistWorkspace([pkg.name], { + targetDir: targetDir, + buildDependencies: Boolean(cmd.build), + parallel: parseParallel(process.env[PARALLEL_ENV_VAR]), + skeleton: 'skeleton.tar', + }); +}; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 1ebe518cac..bbd577d069 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -44,6 +44,11 @@ export function registerCommands(program: CommanderStatic) { .description('Build a backend plugin') .action(lazy(() => import('./backend/build').then(m => m.default))); + program + .command('backend:__experimental__bundle__', { hidden: true }) + .description('Bundle all backend packages into dist-workspace') + .action(lazy(() => import('./backend/bundle').then(m => m.default))); + program .command('backend:build-image') .allowUnknownOption(true) From 1185919f399ea9df8340daed9a679d35101a2550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 23 Nov 2020 11:29:37 +0100 Subject: [PATCH 44/94] catalog: mark ancestors and descendants for deprecation (#3392) --- .changeset/forty-buses-watch.md | 14 ++++++ .../software-catalog/descriptor-format.md | 10 ++++ .../src/kinds/GroupEntityV1alpha1.ts | 14 ++++++ .../BuiltinKindsEntityProcessor.test.ts | 47 +++++++++++++++++++ .../processors/BuiltinKindsEntityProcessor.ts | 19 ++++++++ 5 files changed, 104 insertions(+) create mode 100644 .changeset/forty-buses-watch.md create mode 100644 plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts diff --git a/.changeset/forty-buses-watch.md b/.changeset/forty-buses-watch.md new file mode 100644 index 0000000000..4074566fe7 --- /dev/null +++ b/.changeset/forty-buses-watch.md @@ -0,0 +1,14 @@ +--- +'@backstage/catalog-model': patch +'@backstage/plugin-catalog-backend': patch +--- + +Marked the `Group` entity fields `ancestors` and `descendants` for deprecation on Dec 6th, 2020. See https://github.com/backstage/backstage/issues/3049 for details. + +Code that consumes these fields should remove those usages as soon as possible. There is no current or planned replacement for these fields. + +The BuiltinKindsEntityProcessor has been updated to inject these fields as empty arrays if they are missing. Therefore, if you are on a catalog instance that uses the updated version of this code, you can start removing the fields from your source catalog-info.yaml data as well, without breaking validation. + +After Dec 6th, the fields will be removed from types and classes of the Backstage repository. At the first release after that, they will not be present in released packages either. + +If your catalog-info.yaml files still contain these fields after the deletion, they will still be valid and your ingestion will not break, but they won't be visible in the types for consuming code. diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index c3adaabc45..f5cb361b81 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -700,6 +700,11 @@ sufficient to enter only the `metadata.name` field of that group. ### `spec.ancestors` [required] +**NOTE**: This field was marked for deprecation on Nov 22nd, 2020. It will be +removed entirely from the model on Dec 6th, 2020 in the repository and will not +be present in released packages following the next release after that. Please +update your code to not consume this field before the removal date. + The recursive list of parents up the hierarchy, by stepping through parents one by one. The list must be present, but may be empty if `parent` is not present. The first entry in the list is equal to `parent`, and then the following ones @@ -728,6 +733,11 @@ sufficient to enter only the `metadata.name` field of those groups. ### `spec.descendants` [required] +**NOTE**: This field was marked for deprecation on Nov 22nd, 2020. It will be +removed entirely from the model on Dec 6th, 2020 in the repository and will not +be present in released packages following the next release after that. Please +update your code to not consume this field before the removal date. + The immediate and recursive child groups of this group in the hierarchy (children, and children's children, etc.). The list must be present, but may be empty if there are no child groups. The items are not guaranteed to be ordered diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts index f6345741f7..7be432ad1f 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts @@ -57,8 +57,22 @@ export interface GroupEntityV1alpha1 extends Entity { spec: { type: string; parent?: string; + /** + * @deprecated This field will disappear on Dec 6th, 2020. Please remove + * any consuming code. Producers can stop producing this field + * before that date, as long as the catalog backend uses the + * BuiltinKindsEntityProcessor which inserts the fields in the + * mean time. + */ ancestors: string[]; children: string[]; + /** + * @deprecated This field will disappear on Dec 6th, 2020. Please remove + * any consuming code. Producers can stop producing this field + * before that date, as long as the catalog backend uses the + * BuiltinKindsEntityProcessor which inserts the fields in the + * mean time. + */ descendants: string[]; }; } diff --git a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts new file mode 100644 index 0000000000..e1328e827a --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BuiltinKindsEntityProcessor } from './BuiltinKindsEntityProcessor'; + +describe('BuiltinKindsEntityProcessor', () => { + it('fills in fields for #3049', async () => { + const p = new BuiltinKindsEntityProcessor(); + const result = await p.preProcessEntity({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'n', + }, + spec: { + type: 't', + children: [], + } as any, + }); + expect(result).toEqual({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'n', + }, + spec: { + type: 't', + children: [], + ancestors: [], + descendants: [], + }, + }); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts index 4d1eb5e583..472dffeb16 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts @@ -35,6 +35,25 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { userEntityV1alpha1Validator, ]; + async preProcessEntity(entity: Entity): Promise { + // NOTE(freben): Part of Group field deprecation on Nov 22nd, 2020. Fields + // scheduled for removal Dec 6th, 2020. This code can be deleted after that + // point. See https://github.com/backstage/backstage/issues/3049 + if ( + entity.apiVersion === 'backstage.io/v1alpha1' && + entity.kind === 'Group' && + entity.spec + ) { + if (!entity.spec.ancestors) { + entity.spec.ancestors = []; + } + if (!entity.spec.descendants) { + entity.spec.descendants = []; + } + } + return entity; + } + async validateEntityKind(entity: Entity): Promise { for (const validator of this.validators) { const result = await validator.check(entity); From da2f4cd41a3034aa26b8c598057ef308b40bde07 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 23 Nov 2020 12:08:46 +0100 Subject: [PATCH 45/94] Add graphql API type to descriptor format (#3395) --- docs/features/software-catalog/descriptor-format.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index f5cb361b81..60f1901807 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -592,6 +592,9 @@ The current set of well-known and common values for this field is: [OpenAPI](https://swagger.io/specification/) version 2 or version 3 spec. - `asyncapi` - An API definition based on the [AsyncAPI](https://www.asyncapi.com/docs/specifications/latest/) spec. +- `graphql` - An API definition based on + [GraphQL schemas](https://spec.graphql.org/) for consuming + [GraphQL](https://graphql.org/) based APIs. - `grpc` - An API definition based on [Protocol Buffers](https://developers.google.com/protocol-buffers) to use with [gRPC](https://grpc.io/). From e2085293a635a4c4e356f54a7bf213d0958b2447 Mon Sep 17 00:00:00 2001 From: Moustafa Baiou Date: Mon, 19 Oct 2020 23:22:08 -0400 Subject: [PATCH 46/94] feat(kubernetes): update k8s-backend plugin to use label selectors update plugin to use label selectors when querying backend --- .../src/service/KubernetesFetcher.test.ts | 6 +- .../src/service/KubernetesFetcher.ts | 97 +++++++++---------- ...ubernetesObjectsByServiceIdHandler.test.ts | 10 +- ... getKubernetesObjectsForServiceHandler.ts} | 8 +- .../src/service/router.test.ts | 2 +- .../kubernetes-backend/src/service/router.ts | 12 ++- plugins/kubernetes-backend/src/types/types.ts | 3 +- plugins/kubernetes/README.md | 25 ++++- .../src/api/KubernetesBackendClient.ts | 18 +++- plugins/kubernetes/src/api/types.ts | 4 +- .../KubernetesContent/KubernetesContent.tsx | 21 +++- 11 files changed, 132 insertions(+), 74 deletions(-) rename plugins/kubernetes-backend/src/service/{getKubernetesObjectsByServiceIdHandler.ts => getKubernetesObjectsForServiceHandler.ts} (90%) diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts index 6294b3a7ed..8c562b5200 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -57,7 +57,7 @@ describe('KubernetesClientProvider', () => { clientMock.listServiceForAllNamespaces.mockRejectedValue(errorResponse); - const result = await sut.fetchObjectsByServiceId( + const result = await sut.fetchObjectsForService( 'some-service', { name: 'cluster1', @@ -120,7 +120,7 @@ describe('KubernetesClientProvider', () => { }, }); - const result = await sut.fetchObjectsByServiceId( + const result = await sut.fetchObjectsForService( 'some-service', { name: 'cluster1', @@ -169,7 +169,7 @@ describe('KubernetesClientProvider', () => { }); it('should throw error on unknown type', () => { expect(() => - sut.fetchObjectsByServiceId( + sut.fetchObjectsForService( 'some-service', { name: 'cluster1', diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts index f66a45816a..3bfb651d52 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -105,15 +105,19 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { this.logger = logger; } - fetchObjectsByServiceId( + fetchObjectsForService( serviceId: string, clusterDetails: ClusterDetails, objectTypesToFetch: Set, + labelSelector: string, ): Promise { const fetchResults = Array.from(objectTypesToFetch).map(type => { - return this.fetchByObjectType(serviceId, clusterDetails, type).catch( - captureKubernetesErrorsRethrowOthers, - ); + return this.fetchByObjectType( + serviceId, + clusterDetails, + type, + labelSelector, + ).catch(captureKubernetesErrorsRethrowOthers); }); return Promise.all(fetchResults).then(fetchResultsToResponseWrapper); @@ -124,42 +128,53 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { serviceId: string, clusterDetails: ClusterDetails, type: KubernetesObjectTypes, + labelSelector: string, ): Promise { switch (type) { case 'pods': - return this.fetchPodsByServiceId(serviceId, clusterDetails).then(r => ({ + return this.fetchPodsForService( + serviceId, + clusterDetails, + labelSelector, + ).then(r => ({ type: type, resources: r, })); case 'configmaps': - return this.fetchConfigMapsByServiceId( + return this.fetchConfigMapsForService( serviceId, clusterDetails, + labelSelector, ).then(r => ({ type: type, resources: r })); case 'deployments': - return this.fetchDeploymentsByServiceId( + return this.fetchDeploymentsForService( serviceId, clusterDetails, + labelSelector, ).then(r => ({ type: type, resources: r })); case 'replicasets': - return this.fetchReplicaSetsByServiceId( + return this.fetchReplicaSetsForService( serviceId, clusterDetails, + labelSelector, ).then(r => ({ type: type, resources: r })); case 'services': - return this.fetchServicesByServiceId( + return this.fetchServicesForService( serviceId, clusterDetails, + labelSelector, ).then(r => ({ type: type, resources: r })); case 'horizontalpodautoscalers': - return this.fetchHorizontalPodAutoscalersByServiceId( + return this.fetchHorizontalPodAutoscalersForService( serviceId, clusterDetails, + labelSelector, ).then(r => ({ type: type, resources: r })); case 'ingresses': - return this.fetchIngressesByServiceId( + return this.fetchIngressesForService( serviceId, clusterDetails, + labelSelector, ).then(r => ({ type: type, resources: r })); default: // unrecognised type @@ -192,79 +207,60 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { }); } - private fetchServicesByServiceId( + private fetchServicesForService( serviceId: string, clusterDetails: ClusterDetails, + labelSelector: string, ): Promise> { return this.singleClusterFetch(clusterDetails, ({ core }) => - core.listServiceForAllNamespaces( - false, - '', - '', - `backstage.io/kubernetes-id=${serviceId}`, - ), + core.listServiceForAllNamespaces(false, '', '', labelSelector), ); } - private fetchPodsByServiceId( + private fetchPodsForService( serviceId: string, clusterDetails: ClusterDetails, + labelSelector: string, ): Promise> { return this.singleClusterFetch(clusterDetails, ({ core }) => - core.listPodForAllNamespaces( - false, - '', - '', - `backstage.io/kubernetes-id=${serviceId}`, - ), + core.listPodForAllNamespaces(false, '', '', labelSelector), ); } - private fetchConfigMapsByServiceId( + private fetchConfigMapsForService( serviceId: string, clusterDetails: ClusterDetails, + labelSelector: string, ): Promise> { return this.singleClusterFetch(clusterDetails, ({ core }) => - core.listConfigMapForAllNamespaces( - false, - '', - '', - `backstage.io/kubernetes-id=${serviceId}`, - ), + core.listConfigMapForAllNamespaces(false, '', '', labelSelector), ); } - private fetchDeploymentsByServiceId( + private fetchDeploymentsForService( serviceId: string, clusterDetails: ClusterDetails, + labelSelector: string, ): Promise> { return this.singleClusterFetch(clusterDetails, ({ apps }) => - apps.listDeploymentForAllNamespaces( - false, - '', - '', - `backstage.io/kubernetes-id=${serviceId}`, - ), + apps.listDeploymentForAllNamespaces(false, '', '', labelSelector), ); } - private fetchReplicaSetsByServiceId( + private fetchReplicaSetsForService( serviceId: string, clusterDetails: ClusterDetails, + labelSelector: string, ): Promise> { return this.singleClusterFetch(clusterDetails, ({ apps }) => - apps.listReplicaSetForAllNamespaces( - false, - '', - '', - `backstage.io/kubernetes-id=${serviceId}`, - ), + apps.listReplicaSetForAllNamespaces(false, '', '', labelSelector), ); } - private fetchHorizontalPodAutoscalersByServiceId( + private fetchHorizontalPodAutoscalersForService( serviceId: string, clusterDetails: ClusterDetails, + labelSelector: string, ): Promise> { return this.singleClusterFetch( clusterDetails, @@ -273,14 +269,15 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { false, '', '', - `backstage.io/kubernetes-id=${serviceId}`, + labelSelector, ), ); } - private fetchIngressesByServiceId( + private fetchIngressesForService( serviceId: string, clusterDetails: ClusterDetails, + labelSelector: string, ): Promise> { return this.singleClusterFetch( clusterDetails, @@ -289,7 +286,7 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { false, '', '', - `backstage.io/kubernetes-id=${serviceId}`, + labelSelector, ), ); } diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts index 8c26ded2d4..f413d080c6 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { handleGetKubernetesObjectsByServiceId } from './getKubernetesObjectsByServiceIdHandler'; +import { handleGetKubernetesObjectsForService } from './getKubernetesObjectsForServiceHandler'; import { getVoidLogger } from '@backstage/backend-common'; import { ClusterDetails } from '..'; @@ -81,10 +81,10 @@ describe('handleGetKubernetesObjectsByServiceId', () => { mockFetch(fetchObjectsByServiceId); - const result = await handleGetKubernetesObjectsByServiceId( + const result = await handleGetKubernetesObjectsForService( TEST_SERVICE_ID, { - fetchObjectsByServiceId, + fetchObjectsForService: fetchObjectsByServiceId, }, { getClustersByServiceId, @@ -155,10 +155,10 @@ describe('handleGetKubernetesObjectsByServiceId', () => { mockFetch(fetchObjectsByServiceId); - const result = await handleGetKubernetesObjectsByServiceId( + const result = await handleGetKubernetesObjectsForService( TEST_SERVICE_ID, { - fetchObjectsByServiceId, + fetchObjectsForService: fetchObjectsByServiceId, }, { getClustersByServiceId, diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts similarity index 90% rename from plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts rename to plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts index 6fc6092bd6..5f31248790 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts @@ -26,12 +26,13 @@ import { import { KubernetesAuthTranslator } from '../kubernetes-auth-translator/types'; import { KubernetesAuthTranslatorGenerator } from '../kubernetes-auth-translator/KubernetesAuthTranslatorGenerator'; -export type GetKubernetesObjectsByServiceIdHandler = ( +export type GetKubernetesObjectsForServiceHandler = ( serviceId: string, fetcher: KubernetesFetcher, serviceLocator: KubernetesServiceLocator, logger: Logger, requestBody: AuthRequestBody, + labelSelector: string, objectsToFetch?: Set, ) => Promise; @@ -46,12 +47,13 @@ const DEFAULT_OBJECTS = new Set([ ]); // Fans out the request to all clusters that the service lives in, aggregates their responses together -export const handleGetKubernetesObjectsByServiceId: GetKubernetesObjectsByServiceIdHandler = async ( +export const handleGetKubernetesObjectsForService: GetKubernetesObjectsForServiceHandler = async ( serviceId, fetcher, serviceLocator, logger, requestBody, + labelSelector: string, objectsToFetch = DEFAULT_OBJECTS, ) => { const clusterDetails: ClusterDetails[] = await serviceLocator.getClustersByServiceId( @@ -81,7 +83,7 @@ export const handleGetKubernetesObjectsByServiceId: GetKubernetesObjectsByServic return Promise.all( clusterDetailsDecoratedForAuth.map(cd => { return fetcher - .fetchObjectsByServiceId(serviceId, cd, objectsToFetch) + .fetchObjectsForService(serviceId, cd, objectsToFetch, labelSelector) .then(result => { return { cluster: { diff --git a/plugins/kubernetes-backend/src/service/router.test.ts b/plugins/kubernetes-backend/src/service/router.test.ts index 6f9fbbf70f..b63b63ead6 100644 --- a/plugins/kubernetes-backend/src/service/router.test.ts +++ b/plugins/kubernetes-backend/src/service/router.test.ts @@ -32,7 +32,7 @@ describe('router', () => { beforeAll(async () => { kubernetesFetcher = { - fetchObjectsByServiceId: jest.fn(), + fetchObjectsForService: jest.fn(), }; kubernetesServiceLocator = { diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts index b73115e1e4..5462b5c9fb 100644 --- a/plugins/kubernetes-backend/src/service/router.ts +++ b/plugins/kubernetes-backend/src/service/router.ts @@ -22,9 +22,9 @@ import { MultiTenantServiceLocator } from '../service-locator/MultiTenantService import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; import { KubernetesClientProvider } from './KubernetesClientProvider'; import { - GetKubernetesObjectsByServiceIdHandler, - handleGetKubernetesObjectsByServiceId, -} from './getKubernetesObjectsByServiceIdHandler'; + GetKubernetesObjectsForServiceHandler, + handleGetKubernetesObjectsForService, +} from './getKubernetesObjectsForServiceHandler'; import { AuthRequestBody, KubernetesServiceLocator, @@ -64,13 +64,14 @@ export const makeRouter = ( logger: Logger, fetcher: KubernetesFetcher, serviceLocator: KubernetesServiceLocator, - handleGetByServiceId: GetKubernetesObjectsByServiceIdHandler, + handleGetByServiceId: GetKubernetesObjectsForServiceHandler, ): express.Router => { const router = Router(); router.use(express.json()); router.post('/services/:serviceId', async (req, res) => { const serviceId = req.params.serviceId; + const labelSelector = req.query.labelSelector; const requestBody: AuthRequestBody = req.body; try { const response = await handleGetByServiceId( @@ -79,6 +80,7 @@ export const makeRouter = ( serviceLocator, logger, requestBody, + labelSelector, ); res.send(response); } catch (e) { @@ -119,6 +121,6 @@ export async function createRouter( logger, fetcher, serviceLocator, - handleGetKubernetesObjectsByServiceId, + handleGetKubernetesObjectsForService, ); } diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 1422511c21..0425687f48 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -110,10 +110,11 @@ export interface IngressesFetchResponse { // Fetches information from a kubernetes cluster using the cluster details object // to target a specific cluster export interface KubernetesFetcher { - fetchObjectsByServiceId( + fetchObjectsForService( serviceId: string, clusterDetails: ClusterDetails, objectTypesToFetch: Set, + labelSelector: string, ): Promise; } diff --git a/plugins/kubernetes/README.md b/plugins/kubernetes/README.md index 1ad478bd89..e6f1f2355a 100644 --- a/plugins/kubernetes/README.md +++ b/plugins/kubernetes/README.md @@ -14,7 +14,12 @@ It is only meant for local development, and the setup for it can be found inside ## Surfacing your Kubernetes components as part of an entity -### Adding the entity annotation +There are 2 ways to surface your kubernetes components as part of an entity. +The label selector takes precedence over the annotation/service id. + +### Common `backstage.io/kubernetes-id` label + +#### Adding the entity annotation In order for Backstage to detect that an entity has Kubernetes components, the following annotation should be added to the entity. @@ -24,7 +29,7 @@ annotations: 'backstage.io/kubernetes-id': dice-roller ``` -### Labeling Kubernetes components +#### Labeling Kubernetes components In order for Kubernetes components to show up in the service catalog as a part of an entity, Kubernetes components must be labeled with the following label: @@ -32,3 +37,19 @@ as a part of an entity, Kubernetes components must be labeled with the following ```yaml 'backstage.io/kubernetes-id': ``` + +### Full label selector + +#### Adding the entity label selector to the spec + +In order for Backstage to detect that an entity has kubernetes components the `kubernetes.selector` must have a valid selector. (Currently only matchLabels is supported) + +```yaml +spec: + kubernetes: + selector: + matchLabels: + someKey: someValue + other-key: other-value + app.kubernetes.io/name: dice-roller +``` diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.ts index 86bb830046..faf9aaf024 100644 --- a/plugins/kubernetes/src/api/KubernetesBackendClient.ts +++ b/plugins/kubernetes/src/api/KubernetesBackendClient.ts @@ -20,6 +20,7 @@ import { AuthRequestBody, ObjectsByServiceIdResponse, } from '@backstage/plugin-kubernetes-backend'; +import { V1LabelSelector } from '@kubernetes/client-node'; export class KubernetesBackendClient implements KubernetesApi { private readonly discoveryApi: DiscoveryApi; @@ -50,10 +51,23 @@ export class KubernetesBackendClient implements KubernetesApi { return await response.json(); } - async getObjectsByServiceId( + private parseLabelSelector(params: V1LabelSelector): string { + // TODO: figure out how to convert the selector to the full query param from the yaml + // (as shown here https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector.go) + return Object.keys(params.matchLabels) + .map(key => `${key}=${params[key]}`) + .join(','); + } + + async getObjectsByLabelSelector( serviceId: String, + labelSelector: V1LabelSelector, requestBody: AuthRequestBody, ): Promise { - return await this.getRequired(`/services/${serviceId}`, requestBody); + const labelSelectorQueryParams = this.parseLabelSelector(labelSelector); + return await this.getRequired( + `/services/${serviceId}?labelSelector=${labelSelectorQueryParams}`, + requestBody, + ); } } diff --git a/plugins/kubernetes/src/api/types.ts b/plugins/kubernetes/src/api/types.ts index 8e44d58e6a..2f13dcb02b 100644 --- a/plugins/kubernetes/src/api/types.ts +++ b/plugins/kubernetes/src/api/types.ts @@ -19,6 +19,7 @@ import { AuthRequestBody, ObjectsByServiceIdResponse, } from '@backstage/plugin-kubernetes-backend'; +import { V1LabelSelector } from '@kubernetes/client-node'; export const kubernetesApiRef = createApiRef({ id: 'plugin.kubernetes.service', @@ -27,8 +28,9 @@ export const kubernetesApiRef = createApiRef({ }); export interface KubernetesApi { - getObjectsByServiceId( + getObjectsByLabelSelector( serviceId: String, + labelSelector: V1LabelSelector, requestBody: AuthRequestBody, ): Promise; } diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx index a3e82e79a4..d46a888b39 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx @@ -41,6 +41,7 @@ import { ExtensionsV1beta1Ingress, V1ConfigMap, V1HorizontalPodAutoscaler, + V1LabelSelector, V1Service, } from '@kubernetes/client-node'; import { Services } from '../Services'; @@ -129,9 +130,27 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => { ); } + // decide label selector to search by defaulting to this label + let labelSelector: V1LabelSelector = { + matchLabels: { + 'backstage.io/kubernetes-id': entity.metadata.name, + }, + }; + + if ( + entity.spec.kubernetes && + (entity.spec.kubernetes.selector as V1LabelSelector) + ) { + labelSelector = entity.spec.kubernetes.selector; + } + // TODO: Add validation on contents/format of requestBody kubernetesApi - .getObjectsByServiceId(entity.metadata.name, requestBody) + .getObjectsByLabelSelector( + entity.metadata.name, + labelSelector, + requestBody, + ) .then(result => { setKubernetesObjects(result); }) From beaa0a82dbd267193fe7877cf806e977a8b1241e Mon Sep 17 00:00:00 2001 From: Moustafa Baiou Date: Tue, 20 Oct 2020 00:22:44 -0400 Subject: [PATCH 47/94] fix tests, types and cleanup --- .../src/service/KubernetesFetcher.test.ts | 45 +++++++++++++++++++ .../src/service/KubernetesFetcher.ts | 4 +- ...ubernetesObjectsByServiceIdHandler.test.ts | 18 ++++---- .../kubernetes-backend/src/service/router.ts | 2 +- .../src/api/KubernetesBackendClient.ts | 5 ++- 5 files changed, 62 insertions(+), 12 deletions(-) diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts index 8c562b5200..f37a81f9f8 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -66,6 +66,7 @@ describe('KubernetesClientProvider', () => { authProvider: 'serviceAccount', }, new Set(['pods', 'services']), + '', ); expect(result).toStrictEqual({ @@ -129,6 +130,7 @@ describe('KubernetesClientProvider', () => { authProvider: 'serviceAccount', }, new Set(['pods', 'services']), + '', ); expect(result).toStrictEqual({ @@ -178,6 +180,7 @@ describe('KubernetesClientProvider', () => { authProvider: 'serviceAccount', }, new Set(['foo']), + '', ), ).toThrow('unrecognised type=foo'); @@ -254,4 +257,46 @@ describe('KubernetesClientProvider', () => { }, ); }); + it('should always add a labelSelector query', async () => { + clientMock.listPodForAllNamespaces.mockResolvedValueOnce({ + body: { + items: [ + { + metadata: { + name: 'pod-name', + }, + }, + ], + }, + }); + + clientMock.listServiceForAllNamespaces.mockResolvedValueOnce({ + body: { + items: [ + { + metadata: { + name: 'service-name', + }, + }, + ], + }, + }); + + await sut.fetchObjectsForService( + 'some-service', + { + name: 'cluster1', + url: 'http://localhost:9999', + serviceAccountToken: 'token', + authProvider: 'serviceAccount', + }, + new Set(['pods', 'services']), + '', + ); + + const mockCall = clientMock.listPodForAllNamespaces.mock.calls[0]; + const actualSelector = mockCall[mockCall.length - 1]; + const expectedSelector = 'backstage.io/kubernetes-id=some-service'; + expect(actualSelector).toBe(expectedSelector); + }); }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts index 3bfb651d52..a1ed21552b 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -116,7 +116,9 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { serviceId, clusterDetails, type, - labelSelector, + labelSelector.length !== 0 + ? labelSelector + : `backstage.io/kubernetes-id=${serviceId}`, ).catch(captureKubernetesErrorsRethrowOthers); }); diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts index f413d080c6..2f779adfe8 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts @@ -20,7 +20,7 @@ import { ClusterDetails } from '..'; const TEST_SERVICE_ID = 'my-service'; -const fetchObjectsByServiceId = jest.fn(); +const fetchObjectsForService = jest.fn(); const getClustersByServiceId = jest.fn(); @@ -64,7 +64,7 @@ const mockFetch = (mock: jest.Mock) => { ); }; -describe('handleGetKubernetesObjectsByServiceId', () => { +describe('handleGetKubernetesObjectsForService', () => { beforeEach(() => { jest.resetAllMocks(); }); @@ -79,22 +79,23 @@ describe('handleGetKubernetesObjectsByServiceId', () => { ]), ); - mockFetch(fetchObjectsByServiceId); + mockFetch(fetchObjectsForService); const result = await handleGetKubernetesObjectsForService( TEST_SERVICE_ID, { - fetchObjectsForService: fetchObjectsByServiceId, + fetchObjectsForService: fetchObjectsForService, }, { getClustersByServiceId, }, getVoidLogger(), {}, + '', ); expect(getClustersByServiceId.mock.calls.length).toBe(1); - expect(fetchObjectsByServiceId.mock.calls.length).toBe(1); + expect(fetchObjectsForService.mock.calls.length).toBe(1); expect(result).toStrictEqual({ items: [ { @@ -153,12 +154,12 @@ describe('handleGetKubernetesObjectsByServiceId', () => { ]), ); - mockFetch(fetchObjectsByServiceId); + mockFetch(fetchObjectsForService); const result = await handleGetKubernetesObjectsForService( TEST_SERVICE_ID, { - fetchObjectsForService: fetchObjectsByServiceId, + fetchObjectsForService: fetchObjectsForService, }, { getClustersByServiceId, @@ -169,10 +170,11 @@ describe('handleGetKubernetesObjectsByServiceId', () => { google: 'google_token_123', }, }, + '', ); expect(getClustersByServiceId.mock.calls.length).toBe(1); - expect(fetchObjectsByServiceId.mock.calls.length).toBe(2); + expect(fetchObjectsForService.mock.calls.length).toBe(2); expect(result).toStrictEqual({ items: [ { diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts index 5462b5c9fb..fada4c7888 100644 --- a/plugins/kubernetes-backend/src/service/router.ts +++ b/plugins/kubernetes-backend/src/service/router.ts @@ -80,7 +80,7 @@ export const makeRouter = ( serviceLocator, logger, requestBody, - labelSelector, + labelSelector ? labelSelector.toString() : '', ); res.send(response); } catch (e) { diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.ts index faf9aaf024..c6d0153538 100644 --- a/plugins/kubernetes/src/api/KubernetesBackendClient.ts +++ b/plugins/kubernetes/src/api/KubernetesBackendClient.ts @@ -54,8 +54,9 @@ export class KubernetesBackendClient implements KubernetesApi { private parseLabelSelector(params: V1LabelSelector): string { // TODO: figure out how to convert the selector to the full query param from the yaml // (as shown here https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector.go) - return Object.keys(params.matchLabels) - .map(key => `${key}=${params[key]}`) + const { matchLabels } = params; + return Object.keys(matchLabels) + .map(key => `${key}=${matchLabels[key]}`) .join(','); } From 61a552fac6133af81be6ee1d59aeec29240ef53e Mon Sep 17 00:00:00 2001 From: Moustafa Baiou Date: Tue, 20 Oct 2020 00:49:23 -0400 Subject: [PATCH 48/94] fix type checks --- .../src/service/KubernetesFetcher.ts | 29 ++++--------------- .../src/api/KubernetesBackendClient.ts | 5 +++- 2 files changed, 10 insertions(+), 24 deletions(-) diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts index a1ed21552b..11917efc52 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -113,7 +113,6 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { ): Promise { const fetchResults = Array.from(objectTypesToFetch).map(type => { return this.fetchByObjectType( - serviceId, clusterDetails, type, labelSelector.length !== 0 @@ -127,54 +126,45 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { // TODO could probably do with a tidy up private fetchByObjectType( - serviceId: string, clusterDetails: ClusterDetails, type: KubernetesObjectTypes, labelSelector: string, ): Promise { switch (type) { case 'pods': - return this.fetchPodsForService( - serviceId, - clusterDetails, - labelSelector, - ).then(r => ({ - type: type, - resources: r, - })); + return this.fetchPodsForService(clusterDetails, labelSelector).then( + r => ({ + type: type, + resources: r, + }), + ); case 'configmaps': return this.fetchConfigMapsForService( - serviceId, clusterDetails, labelSelector, ).then(r => ({ type: type, resources: r })); case 'deployments': return this.fetchDeploymentsForService( - serviceId, clusterDetails, labelSelector, ).then(r => ({ type: type, resources: r })); case 'replicasets': return this.fetchReplicaSetsForService( - serviceId, clusterDetails, labelSelector, ).then(r => ({ type: type, resources: r })); case 'services': return this.fetchServicesForService( - serviceId, clusterDetails, labelSelector, ).then(r => ({ type: type, resources: r })); case 'horizontalpodautoscalers': return this.fetchHorizontalPodAutoscalersForService( - serviceId, clusterDetails, labelSelector, ).then(r => ({ type: type, resources: r })); case 'ingresses': return this.fetchIngressesForService( - serviceId, clusterDetails, labelSelector, ).then(r => ({ type: type, resources: r })); @@ -210,7 +200,6 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { } private fetchServicesForService( - serviceId: string, clusterDetails: ClusterDetails, labelSelector: string, ): Promise> { @@ -220,7 +209,6 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { } private fetchPodsForService( - serviceId: string, clusterDetails: ClusterDetails, labelSelector: string, ): Promise> { @@ -230,7 +218,6 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { } private fetchConfigMapsForService( - serviceId: string, clusterDetails: ClusterDetails, labelSelector: string, ): Promise> { @@ -240,7 +227,6 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { } private fetchDeploymentsForService( - serviceId: string, clusterDetails: ClusterDetails, labelSelector: string, ): Promise> { @@ -250,7 +236,6 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { } private fetchReplicaSetsForService( - serviceId: string, clusterDetails: ClusterDetails, labelSelector: string, ): Promise> { @@ -260,7 +245,6 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { } private fetchHorizontalPodAutoscalersForService( - serviceId: string, clusterDetails: ClusterDetails, labelSelector: string, ): Promise> { @@ -277,7 +261,6 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { } private fetchIngressesForService( - serviceId: string, clusterDetails: ClusterDetails, labelSelector: string, ): Promise> { diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.ts index c6d0153538..088196b0c8 100644 --- a/plugins/kubernetes/src/api/KubernetesBackendClient.ts +++ b/plugins/kubernetes/src/api/KubernetesBackendClient.ts @@ -55,8 +55,11 @@ export class KubernetesBackendClient implements KubernetesApi { // TODO: figure out how to convert the selector to the full query param from the yaml // (as shown here https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector.go) const { matchLabels } = params; + if (!matchLabels) { + return ''; + } return Object.keys(matchLabels) - .map(key => `${key}=${matchLabels[key]}`) + .map(key => `${key}=${matchLabels[key.toString()]}`) .join(','); } From 99fffed216b5f2f0030627b45cea7c20e9c72fbd Mon Sep 17 00:00:00 2001 From: Moustafa Baiou Date: Sat, 7 Nov 2020 19:44:19 -0500 Subject: [PATCH 49/94] fix types --- .../src/kinds/ComponentEntityV1alpha1.ts | 16 ++++++++++++++++ .../KubernetesContent/KubernetesContent.tsx | 10 ++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts index 9d46dea03f..0e1e369e44 100644 --- a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts @@ -30,6 +30,15 @@ const schema = yup.object>({ lifecycle: yup.string().required().min(1), owner: yup.string().required().min(1), implementsApis: yup.array(yup.string().required()).notRequired(), + kubernetes: yup + .object({ + selector: yup + .object({ + matchLabels: yup.object().required(), + }) + .required(), + }) + .notRequired(), }) .required(), }); @@ -42,6 +51,13 @@ export interface ComponentEntityV1alpha1 extends Entity { lifecycle: string; owner: string; implementsApis?: string[]; + kubernetes?: { + selector: { + matchLabels: { + [key: string]: string; + }; + }; + }; }; } diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx index d46a888b39..3f9c1a866f 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx @@ -26,7 +26,7 @@ import { TabbedCard, useApi, } from '@backstage/core'; -import { Entity } from '@backstage/catalog-model'; +import { ComponentEntityV1alpha1, Entity } from '@backstage/catalog-model'; import { kubernetesApiRef } from '../../api/types'; import { AuthRequestBody, @@ -137,11 +137,13 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => { }, }; + const componentEntity = entity as ComponentEntityV1alpha1; if ( - entity.spec.kubernetes && - (entity.spec.kubernetes.selector as V1LabelSelector) + componentEntity.spec && + componentEntity.spec.kubernetes && + (componentEntity.spec.kubernetes.selector as V1LabelSelector) ) { - labelSelector = entity.spec.kubernetes.selector; + labelSelector = componentEntity.spec.kubernetes.selector; } // TODO: Add validation on contents/format of requestBody From 4639d20720b0ba3c4fa48d0f1fbc2b417b90e6bf Mon Sep 17 00:00:00 2001 From: Moustafa Baiou Date: Mon, 9 Nov 2020 01:42:08 -0500 Subject: [PATCH 50/94] modify approach to post entire entity to k8s backend --- .../GoogleKubernetesAuthTranslator.ts | 4 +-- .../ServiceAccountKubernetesAuthTranslator.ts | 4 +-- .../src/kubernetes-auth-translator/types.ts | 4 +-- ...ubernetesObjectsByServiceIdHandler.test.ts | 1 - .../getKubernetesObjectsForServiceHandler.ts | 33 +++++++++++++++---- .../src/service/router.test.ts | 4 +-- .../kubernetes-backend/src/service/router.ts | 10 +++--- plugins/kubernetes-backend/src/types/types.ts | 6 ++-- .../src/api/KubernetesBackendClient.ts | 28 ++++------------ plugins/kubernetes/src/api/types.ts | 10 +++--- .../KubernetesContent/KubernetesContent.tsx | 32 ++++-------------- .../GoogleKubernetesAuthProvider.ts | 6 ++-- .../KubernetesAuthProviders.ts | 6 ++-- .../ServiceAccountKubernetesAuthProvider.ts | 6 ++-- .../src/kubernetes-auth-provider/types.ts | 10 +++--- 15 files changed, 74 insertions(+), 90 deletions(-) diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts index 7f9b2f0bae..0dd2a4bcc2 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts @@ -15,13 +15,13 @@ */ import { KubernetesAuthTranslator } from './types'; -import { AuthRequestBody, ClusterDetails } from '../types/types'; +import { KubernetesRequestBody, ClusterDetails } from '../types/types'; export class GoogleKubernetesAuthTranslator implements KubernetesAuthTranslator { async decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, - requestBody: AuthRequestBody, + requestBody: KubernetesRequestBody, ): Promise { const clusterDetailsWithAuthToken: ClusterDetails = Object.assign( {}, diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/ServiceAccountKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/ServiceAccountKubernetesAuthTranslator.ts index ecf2f12b72..6433e41546 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/ServiceAccountKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/ServiceAccountKubernetesAuthTranslator.ts @@ -15,7 +15,7 @@ */ import { KubernetesAuthTranslator } from './types'; -import { AuthRequestBody, ClusterDetails } from '../types/types'; +import { KubernetesRequestBody, ClusterDetails } from '../types/types'; export class ServiceAccountKubernetesAuthTranslator implements KubernetesAuthTranslator { @@ -23,7 +23,7 @@ export class ServiceAccountKubernetesAuthTranslator clusterDetails: ClusterDetails, // To ignore TS6133 linting error where it detects 'requestBody' is declared but its value is never read. // @ts-ignore-start - requestBody: AuthRequestBody, // eslint-disable-line @typescript-eslint/no-unused-vars + requestBody: KubernetesRequestBody, // eslint-disable-line @typescript-eslint/no-unused-vars // @ts-ignore-end ): Promise { return clusterDetails; diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/types.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/types.ts index f89e04456f..c01a57889c 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/types.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/types.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { AuthRequestBody, ClusterDetails } from '../types/types'; +import { KubernetesRequestBody, ClusterDetails } from '../types/types'; export interface KubernetesAuthTranslator { decorateClusterDetailsWithAuth( clusterDetails: ClusterDetails, - requestBody: AuthRequestBody, + requestBody: KubernetesRequestBody, ): Promise; } diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts index 2f779adfe8..f95b9a7242 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts @@ -91,7 +91,6 @@ describe('handleGetKubernetesObjectsForService', () => { }, getVoidLogger(), {}, - '', ); expect(getClustersByServiceId.mock.calls.length).toBe(1); diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts index 5f31248790..46f3f27c73 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts @@ -16,25 +16,25 @@ import { Logger } from 'winston'; import { - AuthRequestBody, + KubernetesRequestBody, ClusterDetails, KubernetesServiceLocator, KubernetesFetcher, KubernetesObjectTypes, - ObjectsByServiceIdResponse, + ObjectsByEntityResponse, } from '../types/types'; import { KubernetesAuthTranslator } from '../kubernetes-auth-translator/types'; import { KubernetesAuthTranslatorGenerator } from '../kubernetes-auth-translator/KubernetesAuthTranslatorGenerator'; +import { ComponentEntityV1alpha1 } from '@backstage/catalog-model'; export type GetKubernetesObjectsForServiceHandler = ( serviceId: string, fetcher: KubernetesFetcher, serviceLocator: KubernetesServiceLocator, logger: Logger, - requestBody: AuthRequestBody, - labelSelector: string, + requestBody: KubernetesRequestBody, objectsToFetch?: Set, -) => Promise; +) => Promise; const DEFAULT_OBJECTS = new Set([ 'pods', @@ -46,6 +46,26 @@ const DEFAULT_OBJECTS = new Set([ 'ingresses', ]); +function parseLabelSelector(entity: ComponentEntityV1alpha1): string { + if ( + entity && + entity.spec && + entity.spec.kubernetes && + entity.spec.kubernetes.selector + ) { + // TODO: figure out how to convert the selector to the full query param from the yaml + // (as shown here https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector.go) + const { matchLabels } = entity.spec.kubernetes.selector; + if (!matchLabels) { + return ''; + } + return Object.keys(matchLabels) + .map(key => `${key}=${matchLabels[key.toString()]}`) + .join(','); + } + return ''; +} + // Fans out the request to all clusters that the service lives in, aggregates their responses together export const handleGetKubernetesObjectsForService: GetKubernetesObjectsForServiceHandler = async ( serviceId, @@ -53,7 +73,6 @@ export const handleGetKubernetesObjectsForService: GetKubernetesObjectsForServic serviceLocator, logger, requestBody, - labelSelector: string, objectsToFetch = DEFAULT_OBJECTS, ) => { const clusterDetails: ClusterDetails[] = await serviceLocator.getClustersByServiceId( @@ -80,6 +99,8 @@ export const handleGetKubernetesObjectsForService: GetKubernetesObjectsForServic .join(', ')}]`, ); + const labelSelector = parseLabelSelector(requestBody.entity); + return Promise.all( clusterDetailsDecoratedForAuth.map(cd => { return fetcher diff --git a/plugins/kubernetes-backend/src/service/router.test.ts b/plugins/kubernetes-backend/src/service/router.test.ts index b63b63ead6..a407ce9693 100644 --- a/plugins/kubernetes-backend/src/service/router.test.ts +++ b/plugins/kubernetes-backend/src/service/router.test.ts @@ -21,14 +21,14 @@ import { makeRouter } from './router'; import { KubernetesServiceLocator, KubernetesFetcher, - ObjectsByServiceIdResponse, + ObjectsByEntityResponse, } from '..'; describe('router', () => { let app: express.Express; let kubernetesFetcher: jest.Mocked; let kubernetesServiceLocator: jest.Mocked; - let handleGetByServiceId: jest.Mock>; + let handleGetByServiceId: jest.Mock>; beforeAll(async () => { kubernetesFetcher = { diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts index fada4c7888..38a02ad970 100644 --- a/plugins/kubernetes-backend/src/service/router.ts +++ b/plugins/kubernetes-backend/src/service/router.ts @@ -26,7 +26,7 @@ import { handleGetKubernetesObjectsForService, } from './getKubernetesObjectsForServiceHandler'; import { - AuthRequestBody, + KubernetesRequestBody, KubernetesServiceLocator, KubernetesFetcher, ServiceLocatorMethod, @@ -64,23 +64,21 @@ export const makeRouter = ( logger: Logger, fetcher: KubernetesFetcher, serviceLocator: KubernetesServiceLocator, - handleGetByServiceId: GetKubernetesObjectsForServiceHandler, + handleGetByEntity: GetKubernetesObjectsForServiceHandler, ): express.Router => { const router = Router(); router.use(express.json()); router.post('/services/:serviceId', async (req, res) => { const serviceId = req.params.serviceId; - const labelSelector = req.query.labelSelector; - const requestBody: AuthRequestBody = req.body; + const requestBody: KubernetesRequestBody = req.body; try { - const response = await handleGetByServiceId( + const response = await handleGetByEntity( serviceId, fetcher, serviceLocator, logger, requestBody, - labelSelector ? labelSelector.toString() : '', ); res.send(response); } catch (e) { diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 0425687f48..738bc89b21 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -23,6 +23,7 @@ import { V1ReplicaSet, V1Service, } from '@kubernetes/client-node'; +import { ComponentEntityV1alpha1 } from '@backstage/catalog-model'; export interface ClusterDetails { name: string; @@ -31,10 +32,11 @@ export interface ClusterDetails { serviceAccountToken?: string | undefined; } -export interface AuthRequestBody { +export interface KubernetesRequestBody { auth?: { google?: string; }; + entity: ComponentEntityV1alpha1; } export interface ClusterObjects { @@ -43,7 +45,7 @@ export interface ClusterObjects { errors: KubernetesFetchError[]; } -export interface ObjectsByServiceIdResponse { +export interface ObjectsByEntityResponse { items: ClusterObjects[]; } diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.ts index 088196b0c8..d8fa800ab6 100644 --- a/plugins/kubernetes/src/api/KubernetesBackendClient.ts +++ b/plugins/kubernetes/src/api/KubernetesBackendClient.ts @@ -17,10 +17,9 @@ import { DiscoveryApi } from '@backstage/core'; import { KubernetesApi } from './types'; import { - AuthRequestBody, - ObjectsByServiceIdResponse, + KubernetesRequestBody, + ObjectsByEntityResponse, } from '@backstage/plugin-kubernetes-backend'; -import { V1LabelSelector } from '@kubernetes/client-node'; export class KubernetesBackendClient implements KubernetesApi { private readonly discoveryApi: DiscoveryApi; @@ -31,7 +30,7 @@ export class KubernetesBackendClient implements KubernetesApi { private async getRequired( path: string, - requestBody: AuthRequestBody, + requestBody: KubernetesRequestBody, ): Promise { const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}${path}`; const response = await fetch(url, { @@ -51,26 +50,11 @@ export class KubernetesBackendClient implements KubernetesApi { return await response.json(); } - private parseLabelSelector(params: V1LabelSelector): string { - // TODO: figure out how to convert the selector to the full query param from the yaml - // (as shown here https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector.go) - const { matchLabels } = params; - if (!matchLabels) { - return ''; - } - return Object.keys(matchLabels) - .map(key => `${key}=${matchLabels[key.toString()]}`) - .join(','); - } - async getObjectsByLabelSelector( - serviceId: String, - labelSelector: V1LabelSelector, - requestBody: AuthRequestBody, - ): Promise { - const labelSelectorQueryParams = this.parseLabelSelector(labelSelector); + requestBody: KubernetesRequestBody, + ): Promise { return await this.getRequired( - `/services/${serviceId}?labelSelector=${labelSelectorQueryParams}`, + `/services/${requestBody.entity.metadata.name}`, requestBody, ); } diff --git a/plugins/kubernetes/src/api/types.ts b/plugins/kubernetes/src/api/types.ts index 2f13dcb02b..663841ddf9 100644 --- a/plugins/kubernetes/src/api/types.ts +++ b/plugins/kubernetes/src/api/types.ts @@ -16,8 +16,8 @@ import { createApiRef } from '@backstage/core'; import { - AuthRequestBody, - ObjectsByServiceIdResponse, + KubernetesRequestBody, + ObjectsByEntityResponse, } from '@backstage/plugin-kubernetes-backend'; import { V1LabelSelector } from '@kubernetes/client-node'; @@ -29,8 +29,6 @@ export const kubernetesApiRef = createApiRef({ export interface KubernetesApi { getObjectsByLabelSelector( - serviceId: String, - labelSelector: V1LabelSelector, - requestBody: AuthRequestBody, - ): Promise; + requestBody: KubernetesRequestBody, + ): Promise; } diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx index 3f9c1a866f..4e1560be70 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx @@ -29,10 +29,10 @@ import { import { ComponentEntityV1alpha1, Entity } from '@backstage/catalog-model'; import { kubernetesApiRef } from '../../api/types'; import { - AuthRequestBody, + KubernetesRequestBody, ClusterObjects, FetchResponse, - ObjectsByServiceIdResponse, + ObjectsByEntityResponse, } from '@backstage/plugin-kubernetes-backend'; import { kubernetesAuthProvidersApiRef } from '../../kubernetes-auth-provider/types'; import { DeploymentTables } from '../DeploymentTables'; @@ -105,7 +105,7 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => { const kubernetesApi = useApi(kubernetesApiRef); const [kubernetesObjects, setKubernetesObjects] = useState< - ObjectsByServiceIdResponse | undefined + ObjectsByEntityResponse | undefined >(undefined); const [error, setError] = useState(undefined); @@ -121,7 +121,9 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => { useEffect(() => { (async () => { // For each auth type, invoke decorateRequestBodyForAuth on corresponding KubernetesAuthProvider - let requestBody: AuthRequestBody = {}; + let requestBody: KubernetesRequestBody = { + entity: entity as ComponentEntityV1alpha1, + }; for (const authProviderStr of authProviders) { // Multiple asyncs done sequentially instead of all at once to prevent same requestBody from being modified simultaneously requestBody = await kubernetesAuthProvidersApi.decorateRequestBodyForAuth( @@ -130,29 +132,9 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => { ); } - // decide label selector to search by defaulting to this label - let labelSelector: V1LabelSelector = { - matchLabels: { - 'backstage.io/kubernetes-id': entity.metadata.name, - }, - }; - - const componentEntity = entity as ComponentEntityV1alpha1; - if ( - componentEntity.spec && - componentEntity.spec.kubernetes && - (componentEntity.spec.kubernetes.selector as V1LabelSelector) - ) { - labelSelector = componentEntity.spec.kubernetes.selector; - } - // TODO: Add validation on contents/format of requestBody kubernetesApi - .getObjectsByLabelSelector( - entity.metadata.name, - labelSelector, - requestBody, - ) + .getObjectsByLabelSelector(requestBody) .then(result => { setKubernetesObjects(result); }) diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts index 7de5c7c0a9..570806a1a4 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts @@ -16,7 +16,7 @@ import { OAuthApi } from '@backstage/core'; import { KubernetesAuthProvider } from './types'; -import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend'; +import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-backend'; export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider { authProvider: OAuthApi; @@ -26,8 +26,8 @@ export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider { } async decorateRequestBodyForAuth( - requestBody: AuthRequestBody, - ): Promise { + requestBody: KubernetesRequestBody, + ): Promise { const googleAuthToken: string = await this.authProvider.getAccessToken( 'https://www.googleapis.com/auth/cloud-platform', ); diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts index ac909d6695..9f64af9c2e 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts @@ -15,7 +15,7 @@ */ import { OAuthApi } from '@backstage/core'; -import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend'; +import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-backend'; import { KubernetesAuthProvider, KubernetesAuthProvidersApi } from './types'; import { GoogleKubernetesAuthProvider } from './GoogleKubernetesAuthProvider'; import { ServiceAccountKubernetesAuthProvider } from './ServiceAccountKubernetesAuthProvider'; @@ -40,8 +40,8 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { async decorateRequestBodyForAuth( authProvider: string, - requestBody: AuthRequestBody, - ): Promise { + requestBody: KubernetesRequestBody, + ): Promise { const kubernetesAuthProvider: | KubernetesAuthProvider | undefined = this.kubernetesAuthProviderMap.get(authProvider); diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/ServiceAccountKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/ServiceAccountKubernetesAuthProvider.ts index 3ac5a9494b..5d33521ae4 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/ServiceAccountKubernetesAuthProvider.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/ServiceAccountKubernetesAuthProvider.ts @@ -15,13 +15,13 @@ */ import { KubernetesAuthProvider } from './types'; -import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend'; +import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-backend'; export class ServiceAccountKubernetesAuthProvider implements KubernetesAuthProvider { async decorateRequestBodyForAuth( - requestBody: AuthRequestBody, - ): Promise { + requestBody: KubernetesRequestBody, + ): Promise { // No-op, with service account for auth, cluster config/details should already have serviceAccountToken return requestBody; } diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/types.ts b/plugins/kubernetes/src/kubernetes-auth-provider/types.ts index 24fee0f94c..5bd253d2c1 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/types.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/types.ts @@ -15,12 +15,12 @@ */ import { createApiRef } from '@backstage/core'; -import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend'; +import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-backend'; export interface KubernetesAuthProvider { decorateRequestBodyForAuth( - requestBody: AuthRequestBody, - ): Promise; + requestBody: KubernetesRequestBody, + ): Promise; } export const kubernetesAuthProvidersApiRef = createApiRef< @@ -33,6 +33,6 @@ export const kubernetesAuthProvidersApiRef = createApiRef< export interface KubernetesAuthProvidersApi { decorateRequestBodyForAuth( authProvider: string, - requestBody: AuthRequestBody, - ): Promise; + requestBody: KubernetesRequestBody, + ): Promise; } From 1166fcc36e22c7e4de184ca9e02479d84a14d72c Mon Sep 17 00:00:00 2001 From: Moustafa Baiou Date: Mon, 9 Nov 2020 01:44:15 -0500 Subject: [PATCH 51/94] add changeset --- .changeset/chatty-radios-beam.md | 7 +++++++ plugins/kubernetes-backend/package.json | 1 + plugins/kubernetes/src/api/types.ts | 1 - .../src/components/KubernetesContent/KubernetesContent.tsx | 1 - 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .changeset/chatty-radios-beam.md diff --git a/.changeset/chatty-radios-beam.md b/.changeset/chatty-radios-beam.md new file mode 100644 index 0000000000..91ad76ee88 --- /dev/null +++ b/.changeset/chatty-radios-beam.md @@ -0,0 +1,7 @@ +--- +'@backstage/catalog-model': minor +'@backstage/plugin-kubernetes': minor +'@backstage/plugin-kubernetes-backend': minor +--- + +add kubernetes selector to component model diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index e58bb4f047..be7b86dd52 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@backstage/backend-common": "^0.3.0", + "@backstage/catalog-model": "^0.2.0", "@backstage/config": "^0.1.1", "@kubernetes/client-node": "^0.12.1", "@types/express": "^4.17.6", diff --git a/plugins/kubernetes/src/api/types.ts b/plugins/kubernetes/src/api/types.ts index 663841ddf9..90c9e33504 100644 --- a/plugins/kubernetes/src/api/types.ts +++ b/plugins/kubernetes/src/api/types.ts @@ -19,7 +19,6 @@ import { KubernetesRequestBody, ObjectsByEntityResponse, } from '@backstage/plugin-kubernetes-backend'; -import { V1LabelSelector } from '@kubernetes/client-node'; export const kubernetesApiRef = createApiRef({ id: 'plugin.kubernetes.service', diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx index 4e1560be70..f87ab04ece 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx @@ -41,7 +41,6 @@ import { ExtensionsV1beta1Ingress, V1ConfigMap, V1HorizontalPodAutoscaler, - V1LabelSelector, V1Service, } from '@kubernetes/client-node'; import { Services } from '../Services'; From a2f791836e80386fca7fb7613cb3e073fb8e6ab5 Mon Sep 17 00:00:00 2001 From: Moustafa Baiou Date: Mon, 9 Nov 2020 01:45:54 -0500 Subject: [PATCH 52/94] refactor name of api method --- plugins/kubernetes/src/api/KubernetesBackendClient.ts | 2 +- plugins/kubernetes/src/api/types.ts | 2 +- .../src/components/KubernetesContent/KubernetesContent.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.ts index d8fa800ab6..b81a799d6b 100644 --- a/plugins/kubernetes/src/api/KubernetesBackendClient.ts +++ b/plugins/kubernetes/src/api/KubernetesBackendClient.ts @@ -50,7 +50,7 @@ export class KubernetesBackendClient implements KubernetesApi { return await response.json(); } - async getObjectsByLabelSelector( + async getObjectsByEntity( requestBody: KubernetesRequestBody, ): Promise { return await this.getRequired( diff --git a/plugins/kubernetes/src/api/types.ts b/plugins/kubernetes/src/api/types.ts index 90c9e33504..498e74a626 100644 --- a/plugins/kubernetes/src/api/types.ts +++ b/plugins/kubernetes/src/api/types.ts @@ -27,7 +27,7 @@ export const kubernetesApiRef = createApiRef({ }); export interface KubernetesApi { - getObjectsByLabelSelector( + getObjectsByEntity( requestBody: KubernetesRequestBody, ): Promise; } diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx index f87ab04ece..14cda3b5f7 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx @@ -133,7 +133,7 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => { // TODO: Add validation on contents/format of requestBody kubernetesApi - .getObjectsByLabelSelector(requestBody) + .getObjectsByEntity(requestBody) .then(result => { setKubernetesObjects(result); }) From 75d63ee0a08cd07169dae07eae4bda871de4d718 Mon Sep 17 00:00:00 2001 From: Moustafa Baiou Date: Mon, 9 Nov 2020 01:58:33 -0500 Subject: [PATCH 53/94] fix test --- ...ubernetesObjectsByServiceIdHandler.test.ts | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts index f95b9a7242..4fdd65f474 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts @@ -16,6 +16,7 @@ import { handleGetKubernetesObjectsForService } from './getKubernetesObjectsForServiceHandler'; import { getVoidLogger } from '@backstage/backend-common'; +import { ComponentEntityV1alpha1 } from '@backstage/catalog-model'; import { ClusterDetails } from '..'; const TEST_SERVICE_ID = 'my-service'; @@ -24,6 +25,26 @@ const fetchObjectsForService = jest.fn(); const getClustersByServiceId = jest.fn(); +const goodEntity: ComponentEntityV1alpha1 = { + apiVersion: 'backstage.io/v1beta1', + kind: 'Component', + metadata: { + name: 'test-component', + }, + spec: { + type: 'service', + lifecycle: 'production', + owner: 'joe', + kubernetes: { + selector: { + matchLabels: { + 'backstage.io/test-label': 'test-component', + }, + }, + }, + }, +}; + const mockFetch = (mock: jest.Mock) => { mock.mockImplementation((serviceId: string, clusterDetails: ClusterDetails) => Promise.resolve({ @@ -90,7 +111,7 @@ describe('handleGetKubernetesObjectsForService', () => { getClustersByServiceId, }, getVoidLogger(), - {}, + { entity: goodEntity }, ); expect(getClustersByServiceId.mock.calls.length).toBe(1); @@ -165,11 +186,11 @@ describe('handleGetKubernetesObjectsForService', () => { }, getVoidLogger(), { + entity: goodEntity, auth: { google: 'google_token_123', }, }, - '', ); expect(getClustersByServiceId.mock.calls.length).toBe(1); From 728767e3d39a0fd0c149357ecf96da2275f91809 Mon Sep 17 00:00:00 2001 From: Moustafa Baiou Date: Mon, 9 Nov 2020 14:00:13 -0500 Subject: [PATCH 54/94] cleanup --- .../src/service/KubernetesFetcher.test.ts | 49 ++++++++++--------- .../src/service/KubernetesFetcher.ts | 15 +++--- ...ubernetesObjectsByServiceIdHandler.test.ts | 2 +- .../getKubernetesObjectsForServiceHandler.ts | 28 +++++------ plugins/kubernetes-backend/src/types/types.ts | 12 +++-- plugins/kubernetes/README.md | 36 +++++++------- 6 files changed, 71 insertions(+), 71 deletions(-) diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts index f37a81f9f8..29233cf7fd 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -16,6 +16,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; +import { ObjectFetchParams } from '..'; describe('KubernetesClientProvider', () => { let clientMock: any; @@ -57,17 +58,17 @@ describe('KubernetesClientProvider', () => { clientMock.listServiceForAllNamespaces.mockRejectedValue(errorResponse); - const result = await sut.fetchObjectsForService( - 'some-service', - { + const result = await sut.fetchObjectsForService({ + serviceId: 'some-service', + clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', serviceAccountToken: 'token', authProvider: 'serviceAccount', }, - new Set(['pods', 'services']), - '', - ); + objectTypesToFetch: new Set(['pods', 'services']), + labelSelector: '', + }); expect(result).toStrictEqual({ errors: [expectedResult], @@ -121,17 +122,17 @@ describe('KubernetesClientProvider', () => { }, }); - const result = await sut.fetchObjectsForService( - 'some-service', - { + const result = await sut.fetchObjectsForService({ + serviceId: 'some-service', + clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', serviceAccountToken: 'token', authProvider: 'serviceAccount', }, - new Set(['pods', 'services']), - '', - ); + objectTypesToFetch: new Set(['pods', 'services']), + labelSelector: '', + }); expect(result).toStrictEqual({ errors: [], @@ -171,17 +172,17 @@ describe('KubernetesClientProvider', () => { }); it('should throw error on unknown type', () => { expect(() => - sut.fetchObjectsForService( - 'some-service', - { + sut.fetchObjectsForService({ + serviceId: 'some-service', + clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', serviceAccountToken: 'token', authProvider: 'serviceAccount', }, - new Set(['foo']), - '', - ), + objectTypesToFetch: new Set(['foo']), + labelSelector: '', + }), ).toThrow('unrecognised type=foo'); expect(clientMock.listPodForAllNamespaces.mock.calls.length).toBe(0); @@ -282,17 +283,17 @@ describe('KubernetesClientProvider', () => { }, }); - await sut.fetchObjectsForService( - 'some-service', - { + await sut.fetchObjectsForService({ + serviceId: 'some-service', + clusterDetails: { name: 'cluster1', url: 'http://localhost:9999', serviceAccountToken: 'token', authProvider: 'serviceAccount', }, - new Set(['pods', 'services']), - '', - ); + objectTypesToFetch: new Set(['pods', 'services']), + labelSelector: '', + }); const mockCall = clientMock.listPodForAllNamespaces.mock.calls[0]; const actualSelector = mockCall[mockCall.length - 1]; diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts index 11917efc52..d9c6ee6d1a 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -38,6 +38,7 @@ import { FetchResponseWrapper, KubernetesFetchError, KubernetesErrorTypes, + ObjectFetchParams, } from '..'; import lodash, { Dictionary } from 'lodash'; @@ -106,18 +107,14 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { } fetchObjectsForService( - serviceId: string, - clusterDetails: ClusterDetails, - objectTypesToFetch: Set, - labelSelector: string, + params: ObjectFetchParams, ): Promise { - const fetchResults = Array.from(objectTypesToFetch).map(type => { + const fetchResults = Array.from(params.objectTypesToFetch).map(type => { return this.fetchByObjectType( - clusterDetails, + params.clusterDetails, type, - labelSelector.length !== 0 - ? labelSelector - : `backstage.io/kubernetes-id=${serviceId}`, + params.labelSelector || + `backstage.io/kubernetes-id=${params.serviceId}`, ).catch(captureKubernetesErrorsRethrowOthers); }); diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts index 4fdd65f474..789c7062b9 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts @@ -25,7 +25,7 @@ const fetchObjectsForService = jest.fn(); const getClustersByServiceId = jest.fn(); -const goodEntity: ComponentEntityV1alpha1 = { +const goodEntity: ComponentEntityV1alpha1 = { apiVersion: 'backstage.io/v1beta1', kind: 'Component', metadata: { diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts index 46f3f27c73..cec4fa8b66 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts @@ -15,6 +15,7 @@ */ import { Logger } from 'winston'; +import { ComponentEntityV1alpha1 } from '@backstage/catalog-model'; import { KubernetesRequestBody, ClusterDetails, @@ -22,10 +23,10 @@ import { KubernetesFetcher, KubernetesObjectTypes, ObjectsByEntityResponse, + ObjectFetchParams, } from '../types/types'; import { KubernetesAuthTranslator } from '../kubernetes-auth-translator/types'; import { KubernetesAuthTranslatorGenerator } from '../kubernetes-auth-translator/KubernetesAuthTranslatorGenerator'; -import { ComponentEntityV1alpha1 } from '@backstage/catalog-model'; export type GetKubernetesObjectsForServiceHandler = ( serviceId: string, @@ -47,18 +48,10 @@ const DEFAULT_OBJECTS = new Set([ ]); function parseLabelSelector(entity: ComponentEntityV1alpha1): string { - if ( - entity && - entity.spec && - entity.spec.kubernetes && - entity.spec.kubernetes.selector - ) { + const matchLabels = entity?.spec?.kubernetes?.selector?.matchLabels; + if (matchLabels) { // TODO: figure out how to convert the selector to the full query param from the yaml // (as shown here https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector.go) - const { matchLabels } = entity.spec.kubernetes.selector; - if (!matchLabels) { - return ''; - } return Object.keys(matchLabels) .map(key => `${key}=${matchLabels[key.toString()]}`) .join(','); @@ -73,7 +66,7 @@ export const handleGetKubernetesObjectsForService: GetKubernetesObjectsForServic serviceLocator, logger, requestBody, - objectsToFetch = DEFAULT_OBJECTS, + objectTypesToFetch = DEFAULT_OBJECTS, ) => { const clusterDetails: ClusterDetails[] = await serviceLocator.getClustersByServiceId( serviceId, @@ -102,13 +95,18 @@ export const handleGetKubernetesObjectsForService: GetKubernetesObjectsForServic const labelSelector = parseLabelSelector(requestBody.entity); return Promise.all( - clusterDetailsDecoratedForAuth.map(cd => { + clusterDetailsDecoratedForAuth.map(clusterDetails => { return fetcher - .fetchObjectsForService(serviceId, cd, objectsToFetch, labelSelector) + .fetchObjectsForService({ + serviceId, + clusterDetails, + objectTypesToFetch, + labelSelector, + }) .then(result => { return { cluster: { - name: cd.name, + name: clusterDetails.name, }, resources: result.responses, errors: result.errors, diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 738bc89b21..b2a5ef432c 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -109,14 +109,18 @@ export interface IngressesFetchResponse { resources: Array; } +export interface ObjectFetchParams { + serviceId: string; + clusterDetails: ClusterDetails; + objectTypesToFetch: Set; + labelSelector: string; +} + // Fetches information from a kubernetes cluster using the cluster details object // to target a specific cluster export interface KubernetesFetcher { fetchObjectsForService( - serviceId: string, - clusterDetails: ClusterDetails, - objectTypesToFetch: Set, - labelSelector: string, + params: ObjectFetchParams, ): Promise; } diff --git a/plugins/kubernetes/README.md b/plugins/kubernetes/README.md index e6f1f2355a..8f3542a46e 100644 --- a/plugins/kubernetes/README.md +++ b/plugins/kubernetes/README.md @@ -14,10 +14,26 @@ It is only meant for local development, and the setup for it can be found inside ## Surfacing your Kubernetes components as part of an entity -There are 2 ways to surface your kubernetes components as part of an entity. +There are two ways to surface your kubernetes components as part of an entity. The label selector takes precedence over the annotation/service id. -### Common `backstage.io/kubernetes-id` label +### Full label selector + +#### Adding the entity label selector to the spec + +In order for Backstage to detect that an entity has kubernetes components the `kubernetes.selector` must have a valid selector. (Currently only matchLabels is supported) + +```yaml +spec: + kubernetes: + selector: + matchLabels: + someKey: someValue + other-key: other-value + app.kubernetes.io/name: dice-roller +``` + +### Common `backstage.io/kubernetes-id` label on objects #### Adding the entity annotation @@ -37,19 +53,3 @@ as a part of an entity, Kubernetes components must be labeled with the following ```yaml 'backstage.io/kubernetes-id': ``` - -### Full label selector - -#### Adding the entity label selector to the spec - -In order for Backstage to detect that an entity has kubernetes components the `kubernetes.selector` must have a valid selector. (Currently only matchLabels is supported) - -```yaml -spec: - kubernetes: - selector: - matchLabels: - someKey: someValue - other-key: other-value - app.kubernetes.io/name: dice-roller -``` From 74102d917635bf59c3062d64b846151844c0d8cd Mon Sep 17 00:00:00 2001 From: Moustafa Baiou Date: Mon, 9 Nov 2020 14:20:32 -0500 Subject: [PATCH 55/94] fix test after refactor --- .../getKubernetesObjectsByServiceIdHandler.test.ts | 8 ++++---- .../src/service/getKubernetesObjectsForServiceHandler.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts index 789c7062b9..fd76cab662 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts @@ -46,7 +46,7 @@ const goodEntity: ComponentEntityV1alpha1 = { }; const mockFetch = (mock: jest.Mock) => { - mock.mockImplementation((serviceId: string, clusterDetails: ClusterDetails) => + mock.mockImplementation((params: ObjectFetchParams) => Promise.resolve({ errors: [], responses: [ @@ -55,7 +55,7 @@ const mockFetch = (mock: jest.Mock) => { resources: [ { metadata: { - name: `my-pods-${serviceId}-${clusterDetails.name}`, + name: `my-pods-${params.serviceId}-${params.clusterDetails.name}`, }, }, ], @@ -65,7 +65,7 @@ const mockFetch = (mock: jest.Mock) => { resources: [ { metadata: { - name: `my-configmaps-${serviceId}-${clusterDetails.name}`, + name: `my-configmaps-${params.serviceId}-${params.clusterDetails.name}`, }, }, ], @@ -75,7 +75,7 @@ const mockFetch = (mock: jest.Mock) => { resources: [ { metadata: { - name: `my-services-${serviceId}-${clusterDetails.name}`, + name: `my-services-${params.serviceId}-${params.clusterDetails.name}`, }, }, ], diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts index cec4fa8b66..1b0cf4f783 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts @@ -34,7 +34,7 @@ export type GetKubernetesObjectsForServiceHandler = ( serviceLocator: KubernetesServiceLocator, logger: Logger, requestBody: KubernetesRequestBody, - objectsToFetch?: Set, + objectTypesToFetch?: Set, ) => Promise; const DEFAULT_OBJECTS = new Set([ From 44ea3101a7e6659a6dbae7e20dd1e953078b9367 Mon Sep 17 00:00:00 2001 From: Moustafa Baiou Date: Mon, 9 Nov 2020 14:23:40 -0500 Subject: [PATCH 56/94] remove unneeded import --- .../src/service/getKubernetesObjectsByServiceIdHandler.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts index fd76cab662..bb0ffddc26 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts @@ -17,7 +17,6 @@ import { handleGetKubernetesObjectsForService } from './getKubernetesObjectsForServiceHandler'; import { getVoidLogger } from '@backstage/backend-common'; import { ComponentEntityV1alpha1 } from '@backstage/catalog-model'; -import { ClusterDetails } from '..'; const TEST_SERVICE_ID = 'my-service'; From 18d0fd10a03c941ed87b734eada71a284586f8f3 Mon Sep 17 00:00:00 2001 From: Moustafa Baiou Date: Mon, 9 Nov 2020 14:49:12 -0500 Subject: [PATCH 57/94] add needed import --- .../src/service/getKubernetesObjectsByServiceIdHandler.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts index bb0ffddc26..adebebe1c5 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts @@ -17,6 +17,7 @@ import { handleGetKubernetesObjectsForService } from './getKubernetesObjectsForServiceHandler'; import { getVoidLogger } from '@backstage/backend-common'; import { ComponentEntityV1alpha1 } from '@backstage/catalog-model'; +import { ObjectFetchParams } from '..'; const TEST_SERVICE_ID = 'my-service'; From d20f605082f3f2c1a0812c9d8172aafb298d6fd1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 23 Nov 2020 12:43:54 +0100 Subject: [PATCH 58/94] cli: add test for versions:bump command --- .../cli/src/commands/versions/bump.test.ts | 161 ++++++++++++++++++ packages/cli/src/commands/versions/bump.ts | 2 +- .../cli/src/lib/versioning/packages.test.ts | 2 +- packages/cli/src/lib/versioning/packages.ts | 9 +- 4 files changed, 167 insertions(+), 7 deletions(-) create mode 100644 packages/cli/src/commands/versions/bump.test.ts diff --git a/packages/cli/src/commands/versions/bump.test.ts b/packages/cli/src/commands/versions/bump.test.ts new file mode 100644 index 0000000000..687708b074 --- /dev/null +++ b/packages/cli/src/commands/versions/bump.test.ts @@ -0,0 +1,161 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import mockFs from 'mock-fs'; +import { resolve as resolvePath } from 'path'; +import { paths } from '../../lib/paths'; +import { mapDependencies } from '../../lib/versioning'; +import * as runObj from '../../lib/run'; +import bump from './bump'; +import { withLogCollector } from '@backstage/test-utils'; + +const REGISTRY_VERSIONS: { [name: string]: string } = { + '@backstage/core': '1.0.7', + '@backstage/theme': '2.0.0', +}; + +const HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + +`; + +const lockfileMock = `${HEADER} +"@backstage/core@^1.0.5": + version "1.0.6" + resolved "https://my-registry/a-1.0.01.tgz#abc123" + integrity sha512-xyz + +"@backstage/core@^1.0.3": + version "1.0.3" + resolved "https://my-registry/a-1.0.01.tgz#abc123" + integrity sha512-xyz + +"@backstage/theme@^1.0.0": + version "1.0.0" + resolved "https://my-registry/a-1.0.01.tgz#abc123" + integrity sha512-xyz +`; + +const lockfileMockResult = `${HEADER} +"@backstage/core@^1.0.3", "@backstage/core@^1.0.5": + version "1.0.6" + resolved "https://my-registry/a-1.0.01.tgz#abc123" + integrity sha512-xyz + +"@backstage/theme@^1.0.0": + version "1.0.0" + resolved "https://my-registry/a-1.0.01.tgz#abc123" + integrity sha512-xyz +`; + +describe('bump', () => { + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + + it('should bump backstage dependencies', async () => { + // Make sure all modules involved in package discovery are in the module cache before we mock fs + await mapDependencies(paths.targetDir); + + mockFs({ + '/yarn.lock': lockfileMock, + '/lerna.json': JSON.stringify({ + packages: ['packages/*'], + }), + '/packages/a/package.json': JSON.stringify({ + name: 'a', + dependencies: { + '@backstage/core': '^1.0.5', + }, + }), + '/packages/b/package.json': JSON.stringify({ + name: 'b', + dependencies: { + '@backstage/core': '^1.0.3', + '@backstage/theme': '^1.0.0', + }, + }), + }); + + paths.targetDir = '/'; + jest + .spyOn(paths, 'resolveTargetRoot') + .mockImplementation((...paths) => resolvePath('/', ...paths)); + jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) => + JSON.stringify({ + type: 'inspect', + data: { + name: name, + 'dist-tags': { + latest: REGISTRY_VERSIONS[name], + }, + }, + }), + ); + jest.spyOn(runObj, 'run').mockResolvedValue(undefined); + + const { log: logs } = await withLogCollector(['log'], async () => { + await bump(); + }); + expect(logs.filter(Boolean)).toEqual([ + 'Checking for updates of @backstage/theme', + 'Checking for updates of @backstage/core', + 'Some packages are outdated, updating', + 'Bumping @backstage/theme in b to ^2.0.0', + "Running 'yarn install' to install new versions", + 'Removing duplicate dependencies from yarn.lock', + "Running 'yarn install' to remove duplicates from node_modules", + ]); + + expect(runObj.runPlain).toHaveBeenCalledTimes(2); + expect(runObj.runPlain).toHaveBeenCalledWith( + 'yarn', + 'info', + '--json', + '@backstage/core', + ); + expect(runObj.runPlain).toHaveBeenCalledWith( + 'yarn', + 'info', + '--json', + '@backstage/theme', + ); + + expect(runObj.run).toHaveBeenCalledTimes(2); + expect(runObj.run).toHaveBeenCalledWith('yarn', ['install']); + + const lockfileContents = await fs.readFile('/yarn.lock', 'utf8'); + expect(lockfileContents).toBe(lockfileMockResult); + + const packageA = await fs.readJson('/packages/a/package.json'); + expect(packageA).toEqual({ + name: 'a', + dependencies: { + '@backstage/core': '^1.0.5', // not bumped since new version is within range + }, + }); + const packageB = await fs.readJson('/packages/b/package.json'); + expect(packageB).toEqual({ + name: 'b', + dependencies: { + '@backstage/core': '^1.0.3', // not bumped + '@backstage/theme': '^2.0.0', // bumped since newer + }, + }); + }); +}); diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index eec8abed9b..00944c5db9 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -40,7 +40,7 @@ type PkgVersionInfo = { export default async () => { // First we discover all Backstage dependencies within our own repo - const dependencyMap = await mapDependencies(); + const dependencyMap = await mapDependencies(paths.targetDir); // Next check with the package registry what the latest version of all of those dependencies are const targetVersions = new Map(); diff --git a/packages/cli/src/lib/versioning/packages.test.ts b/packages/cli/src/lib/versioning/packages.test.ts index fd37d7af95..8155d4b5c0 100644 --- a/packages/cli/src/lib/versioning/packages.test.ts +++ b/packages/cli/src/lib/versioning/packages.test.ts @@ -75,7 +75,7 @@ describe('mapDependencies', () => { const oldDir = paths.targetDir; paths.targetDir = '/'; - const dependencyMap = await mapDependencies(); + const dependencyMap = await mapDependencies(paths.targetDir); expect(Array.from(dependencyMap)).toEqual([ [ '@backstage/core', diff --git a/packages/cli/src/lib/versioning/packages.ts b/packages/cli/src/lib/versioning/packages.ts index 239fe575d6..76e5dc49b0 100644 --- a/packages/cli/src/lib/versioning/packages.ts +++ b/packages/cli/src/lib/versioning/packages.ts @@ -15,7 +15,6 @@ */ import { runPlain } from '../../lib/run'; -import { paths } from '../../lib/paths'; const PREFIX = '@backstage'; @@ -59,11 +58,11 @@ export async function fetchPackageInfo( } /** Map all dependencies in the repo as dependency => dependents */ -export async function mapDependencies(): Promise< - Map -> { +export async function mapDependencies( + targetDir: string, +): Promise> { const LernaProject = require('@lerna/project'); - const project = new LernaProject(paths.targetDir); + const project = new LernaProject(targetDir); const packages = await project.getPackages(); const dependencyMap = new Map(); From eeda19c7cefb9ab9650c1c0546cdc69a0c313f1f Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Mon, 23 Nov 2020 17:07:38 +0100 Subject: [PATCH 59/94] [Search] name in search result clickable (#3349) * fix(search): wrap value in link component for rows in component id column * fix(search result): change name of column from component id to name * fix(search): construct url in search api instead of search result component * fix(search): use entity default constant from catalog model as fallback to namespace, add entity type * fix(search): replace unknown with undefined if owner or lifecycle doesn't exist on entity, lowercase namespace and kind in url * fix owner fallback and delete description fallback to show empty cells instead --- plugins/search/package.json | 1 + plugins/search/src/apis.ts | 27 ++++++++++++------- .../components/SearchResult/SearchResult.tsx | 11 +++++--- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/plugins/search/package.json b/plugins/search/package.json index 0b1941d9e0..df5c6c25b9 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -22,6 +22,7 @@ "dependencies": { "@backstage/core": "^0.3.1", "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.2.0", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/search/src/apis.ts b/plugins/search/src/apis.ts index f3a050b2a2..e74741e27b 100644 --- a/plugins/search/src/apis.ts +++ b/plugins/search/src/apis.ts @@ -15,13 +15,15 @@ */ import { CatalogApi } from '@backstage/plugin-catalog'; +import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; export type Result = { name: string; - description: string; - owner: string; + description: string | undefined; + owner: string | undefined; kind: string; - lifecycle: string; + lifecycle: string | undefined; + url: string; }; export type SearchResults = Array; @@ -35,12 +37,19 @@ class SearchApi { private async entities() { const entities = await this.catalogApi.getEntities(); - return entities.items.map((result: any) => ({ - name: result.metadata.name, - description: result.metadata.description, - owner: result.spec.owner, - kind: result.kind, - lifecycle: result.spec.lifecycle, + return entities.items.map((entity: Entity) => ({ + name: entity.metadata.name, + description: entity.metadata.description, + owner: + typeof entity.spec?.owner === 'string' ? entity.spec?.owner : undefined, + kind: entity.kind, + lifecycle: + typeof entity.spec?.lifecycle === 'string' + ? entity.spec?.lifecycle + : undefined, + url: `/catalog/${ + entity.metadata.namespace?.toLowerCase() || ENTITY_DEFAULT_NAMESPACE + }/${entity.kind.toLowerCase()}/${entity.metadata.name}`, })); } diff --git a/plugins/search/src/components/SearchResult/SearchResult.tsx b/plugins/search/src/components/SearchResult/SearchResult.tsx index a5dc11aa89..f1e160e792 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.tsx @@ -19,6 +19,7 @@ import { useAsync } from 'react-use'; import { makeStyles, Typography, Grid, Divider } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; import { + Link, EmptyState, Progress, Table, @@ -66,9 +67,12 @@ type Filters = { // TODO: move out column to make the search result component more generic const columns: TableColumn[] = [ { - title: 'Component Id', + title: 'Name', field: 'name', highlight: true, + render: (result: Partial) => ( + {result.name} + ), }, { title: 'Description', @@ -150,8 +154,9 @@ export const SearchResult = ({ searchQuery }: SearchResultProps) => { // filter on checked if (filters.checked.length > 0) { - withFilters = withFilters.filter((result: Result) => - filters.checked.includes(result.lifecycle), + withFilters = withFilters.filter( + (result: Result) => + result.lifecycle && filters.checked.includes(result.lifecycle), ); } From b47dce06fc8538da931abef8ef740abaff119c34 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 23 Nov 2020 17:14:28 +0100 Subject: [PATCH 60/94] backend-common: make integrations host and urls visibile in the frontend --- .changeset/tame-rockets-explode.md | 5 +++++ packages/backend-common/config.d.ts | 35 +++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 .changeset/tame-rockets-explode.md diff --git a/.changeset/tame-rockets-explode.md b/.changeset/tame-rockets-explode.md new file mode 100644 index 0000000000..123472701e --- /dev/null +++ b/.changeset/tame-rockets-explode.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Make integration host and url configurations visible in the frontend diff --git a/packages/backend-common/config.d.ts b/packages/backend-common/config.d.ts index 7ebcd01e8c..7a134ac506 100644 --- a/packages/backend-common/config.d.ts +++ b/packages/backend-common/config.d.ts @@ -87,7 +87,10 @@ export interface Config { integrations?: { /** Integration configuration for Azure */ azure?: Array<{ - /** The hostname of the given Azure instance */ + /** + * The hostname of the given Azure instance + * @visibility frontend + */ host: string; /** * Token used to authenticate requests. @@ -98,14 +101,20 @@ export interface Config { /** Integration configuration for BitBucket */ bitbucket?: Array<{ - /** The hostname of the given Bitbucket instance */ + /** + * The hostname of the given Bitbucket instance + * @visibility frontend + */ host: string; /** * Token used to authenticate requests. * @visibility secret */ token?: string; - /** The base url for the BitBucket API, for example https://api.bitbucket.org/2.0 */ + /** + * The base url for the BitBucket API, for example https://api.bitbucket.org/2.0 + * @visibility frontend + */ apiBaseUrl?: string; /** * The username to use for authenticated requests. @@ -121,22 +130,34 @@ export interface Config { /** Integration configuration for GitHub */ github?: Array<{ - /** The hostname of the given GitHub instance */ + /** + * The hostname of the given GitHub instance + * @visibility frontend + */ host: string; /** * Token used to authenticate requests. * @visibility secret */ token?: string; - /** The base url for the GitHub API, for example https://api.github.com */ + /** + * The base url for the GitHub API, for example https://api.github.com + * @visibility frontend + */ apiBaseUrl?: string; - /** The base url for GitHub raw resources, for example https://raw.githubusercontent.com */ + /** + * The base url for GitHub raw resources, for example https://raw.githubusercontent.com + * @visibility frontend + */ rawBaseUrl?: string; }>; /** Integration configuration for GitLab */ gitlab?: Array<{ - /** The hostname of the given GitLab instance */ + /** + * The hostname of the given GitLab instance + * @visibility frontend + */ host: string; /** * Token used to authenticate requests. From 8fc3fc9629bc605521fc4d6641974f79852cb2d3 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Fri, 20 Nov 2020 13:29:26 +0100 Subject: [PATCH 61/94] TechDocs: Document basic and recommended architecture --- docs/features/techdocs/architecture.md | 149 +++++++++++++++++++++---- mkdocs.yml | 6 +- plugins/techdocs/src/api.ts | 18 +++ plugins/techdocs/src/plugin.ts | 1 + plugins/techdocs/src/types.ts | 6 + 5 files changed, 157 insertions(+), 23 deletions(-) diff --git a/docs/features/techdocs/architecture.md b/docs/features/techdocs/architecture.md index 45a7713c39..01497b31bc 100644 --- a/docs/features/techdocs/architecture.md +++ b/docs/features/techdocs/architecture.md @@ -1,44 +1,153 @@ --- id: architecture -title: Architecture -description: Documentation on Architecture +title: TechDocs Architecture +description: Documentation on TechDocs Architecture --- ## Basic (out-of-the-box) When you deploy Backstage (with TechDocs enabled by default), you get a basic -out-of-the box experience. This is how the architecture of it looks. - -Note: See below for our recommended production architecture which takes care of -stability, scalability and speed. +out-of-the box experience. ![TechDocs Architecture diagram](../../assets/techdocs/architecture-basic.drawio.svg) -We store the timestamp in memory. +> Note: See below for our recommended deployment architecture which takes care +> of stability, scalability and speed. -Publishing to external storage is also possible. But status: none of the cloud -storage solutions are supported yet. +When you open a TechDocs site in Backstage, the +[TechDocs Reader](./concepts.md#techdocs-reader) makes a request to +`techdocs-backend` with the entity ID and the path of the current page you are +looking at. In response, it receives the static files (HTML, CSS, JSON, etc.) to +render on the page in TechDocs/Backstage. -Speed is a bottleneck if doc repositories are very large (>100MBs). +The static files consist of HTML, CSS and Images generated by MkDocs. We remove +all the Javascript before adding them to Backstage for security reasons. And +there are some additional techdocs metadata JSON files that TechDocs needs to +render a site. + +The TechDocs Reader then applies a list of "Transformers" (see +[Concepts](./concepts.md)) which modify the generated static HTML files for a +number of use cases e.g. Remove certain headers, filter out some HTML tags, etc. + +Currently, we use the Backstage server's (or techdocs-backend's) local file +system to store the generated files. Publishing to an external storage system +(AWS S3, GCS, etc.) is also possible, but has not been implemented yet. + +A word about `UrlReader` vs Git preparer - Right now, we have two ways to fetch +files from its source repository for docs site generation. 1. By using Git +and 2. By directly using Source control (GitHub, Azure, etc.) APIs. This work is +heavily in progress. Please reach out to us on Discord in the #docs-like-code +channel to talk about it. ## Recommended deployment -This is how we recommend deploying TechDocs for potential production use. +This is how we recommend deploying TechDocs in production environment. ![TechDocs Architecture diagram](../../assets/techdocs/architecture-recommended.drawio.svg) -TechDocs Backend: Responsible for access control and sending files over to -TechDocs. +The key difference in the recommended deployment approach is where the docs are +built. -Tokens (if possible): Read and Write tokens. +We assume each entity lives in a repository somewhere (GitHub, GitLab, etc.). We +recommend using a CI/CD pipeline with the repository that has a dedicated +step/job to build docs for TechDocs. The generated static files are then stored +in a cloud storage solution of your choice. +[Track progress here](https://github.com/backstage/backstage/issues/3096). -## FAQ +Similar to how it is done in the Basic setup, the TechDocs Reader requests +`techdocs-backend` plugin for the docs site. `techdocs-backend` then requests +your configured storage solution for the necessary files and returns them to +TechDocs Reader. -Q: Why don't use Backstage server to serve static files? A: Not scalable. -[Investigate more] +We will provide instructions, scripts and/or templates (e.g. GitHub actions) to +build docs in your CI/CD system. +[Track progress here.](https://github.com/backstage/backstage/issues/3400) You +will be able to use `techdocs-cli` to build docs and publish the generated docs +site files to your cloud storage system. + +Note about caching: We have noticed internally that some storage providers can +be quite slow, which is why we are recommending a cache that sits between the +TechDocs Reader and the Storage. + +_Feel free to suggest better ideas to us in #docs-like-code channel in Discord +or via a GitHub issue._ + +### Security consideration + +Our biggest security concern is managing the access to the docs in the cloud +storage. We also want to have only one security solution for all different types +of storage (GCS, AWS, custom SFTP server, etc.) Restricting access to the +storage and only allowing `techdocs-backend` to fetch files is a good way to +achieve this. + +This would also allow us to use the access control management Backstage when +that is ready. +[Track progress here.](https://github.com/backstage/backstage/issues/3218) + +In theory, you can directly enable TechDocs Reader to read from your storage. +But, you will have to think about how to do it without the docs being public and +how access to user groups is managed. + +For cloud storage access tokens, `techdocs-backend` only needs a token with Read +permissions. But in your CI/CD system, there needs to be a token with Write +permissions to publish the generated docs site files. + +## FAQs + +**Q: Why do you have separate "basic" and "recommended" deployment approaches?** + +A: The basic or out-of-the-box setup is what you get when you create a new app +or do a git clone of the Backstage repository. We want the first experience to +_just work magically_ so that you can have your first experience with TechDocs +which is smooth. However, if you decide to deploy Backstage/TechDocs for +production use, the basic setup would work but there are going to be downsides +as you scale with the number of documentation sites and sizes of them. So you +would want to make sure the deployment is as stable as possible. Hence there is +a recommended approach. There can be even more deployment approaches to TechDocs +and we welcome such "Alternative" ideas from the community. + +**Q: Why don't you recommend techdocs-backend local filesystem to serve static +files?** + +A: It would make scaling a Backstage instance harder. Think about the case where +we have distributed Backstage deployments. Using a separate file storage system +for TechDocs makes it easier to do some operations like delete a docs site and +wipe its contents. + +**Q: Why aren't docs built on the fly i.e. when users visits a page, generate +docs site in real-time?** + +A: Generating the content from Markdown on the fly is not optimal (although that +is how the basic out-of-the-box setup is implemented). Storage solutions act as +a cache for the generated static content. TechDocs is also currently built on +MkDocs which does not allow us to build docs per-page, so we would have to build +all docs for a entity on every request. # Future work -Instead of storing frontend assets in cloud storage, it can only store texts and -images. And all of frontend rendering is handled by Backstage and TechDocs -reader. +_Ideas here are far fetched and not in the project's milestone for near future +(~6 months)._ + +We currently depend on MkDocs to parse doc sites written in Markdown. And we +store the generated static assets and re-use it later to render in Backstage. A +better (futuristic) approach will be to directly parse whatever type of source +files you have in your docs repository and directly render in Backstage in +real-time. + +# Features status + +Status of all the features mentioned above. + +**In place ✅** + +- Basic setup with techdocs-backend file server as storage. + +**Work in progress 🚧** + +- Basic setup with cloud storage solution. + +**Not implemented yet ❌** + +- `techdocs-cli` is able to generate docs in CI/CD environment. +- `techdocs-cli` is able to publish docs site to any storage. +- `techdocs-backend` integration with Backstage access control management. diff --git a/mkdocs.yml b/mkdocs.yml index 8a4a78ddf1..1086259710 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,6 +1,9 @@ site_name: 'Backstage' site_description: 'Main documentation for Backstage features and platform APIs' +plugins: + - techdocs-core + nav: - Overview: - What is Backstage?: 'overview/what-is-backstage.md' @@ -111,6 +114,3 @@ nav: - 'support/support.md' - 'support/project-structure.md' - FAQ: FAQ.md - -plugins: - - techdocs-core diff --git a/plugins/techdocs/src/api.ts b/plugins/techdocs/src/api.ts index 31e4a10e4c..194ad14d30 100644 --- a/plugins/techdocs/src/api.ts +++ b/plugins/techdocs/src/api.ts @@ -41,6 +41,11 @@ export interface TechDocs { getMetadata(metadataType: string, entityId: ParsedEntityId): Promise; } +/** + * API to talk to techdocs-backend. + * + * @property {string} apiOrigin Set to techdocs.requestUrl as the URL for techdocs-backend API + */ export class TechDocsApi implements TechDocs { public apiOrigin: string; @@ -60,6 +65,11 @@ export class TechDocsApi implements TechDocs { } } +/** + * API which talks to TechDocs storage to fetch files to render. + * + * @property {string} apiOrigin Set to techdocs.requestUrl as the URL for techdocs-backend API + */ export class TechDocsStorageApi implements TechDocsStorage { public apiOrigin: string; @@ -67,6 +77,14 @@ export class TechDocsStorageApi implements TechDocsStorage { this.apiOrigin = apiOrigin; } + /** + * Fetch HTML content as text for an individual docs page in an entity's docs site. + * + * @param {ParsedEntityId} entityId Object containing entity data like name, namespace, etc. + * @param {string} path The unique path to an individual docs page e.g. overview/what-is-new + * @returns {string} HTML content of the docs page as string + * @throws {Error} Throws error when the page is not found. + */ async getEntityDocs(entityId: ParsedEntityId, path: string) { const { kind, namespace, name } = entityId; diff --git a/plugins/techdocs/src/plugin.ts b/plugins/techdocs/src/plugin.ts index ee2cab60fa..576ba0a292 100644 --- a/plugins/techdocs/src/plugin.ts +++ b/plugins/techdocs/src/plugin.ts @@ -57,6 +57,7 @@ export const rootCatalogDocsRouteRef = createRouteRef({ title: 'Docs', }); +// TODO: Use discovery API for frontend to get URL for techdocs-backend instead of requestUrl export const plugin = createPlugin({ id: 'techdocs', apis: [ diff --git a/plugins/techdocs/src/types.ts b/plugins/techdocs/src/types.ts index ec907cc6fa..341d3f0c92 100644 --- a/plugins/techdocs/src/types.ts +++ b/plugins/techdocs/src/types.ts @@ -14,6 +14,12 @@ * limitations under the License. */ +/** + * To uniquely identify an entity in Backstage. + * @property {string} kind + * @property {string} namespace + * @property {string} name + */ export type ParsedEntityId = { kind: string; namespace?: string; From a8de7f554ca590e8419405d6b7bcca8c47f40ded Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Mon, 23 Nov 2020 11:44:42 -0500 Subject: [PATCH 62/94] feat: improve circleci builds table --- .changeset/mighty-cycles-impress.md | 6 + .../app/src/components/catalog/EntityPage.tsx | 4 +- plugins/circleci/package.json | 2 + .../BuildWithStepsPage/BuildWithStepsPage.tsx | 14 +- .../BuildsPage/lib/Builds/Builds.tsx | 4 +- .../BuildsPage/lib/CITable/CITable.tsx | 128 ++++++++++++++++-- plugins/circleci/src/state/useBuilds.ts | 50 +++++-- plugins/circleci/src/util/index.ts | 17 +++ plugins/circleci/src/util/time.test.ts | 31 +++++ plugins/circleci/src/util/time.ts | 35 +++++ 10 files changed, 263 insertions(+), 28 deletions(-) create mode 100644 .changeset/mighty-cycles-impress.md create mode 100644 plugins/circleci/src/util/index.ts create mode 100644 plugins/circleci/src/util/time.test.ts create mode 100644 plugins/circleci/src/util/time.ts diff --git a/.changeset/mighty-cycles-impress.md b/.changeset/mighty-cycles-impress.md new file mode 100644 index 0000000000..33b2aa008c --- /dev/null +++ b/.changeset/mighty-cycles-impress.md @@ -0,0 +1,6 @@ +--- +'example-app': patch +'@backstage/plugin-circleci': patch +--- + +Improved CircleCI builds table to show more information and relevant links diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index d826bfa25e..c201fe88ad 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -79,10 +79,10 @@ export const CICDSwitcher = ({ entity }: { entity: Entity }) => { return ; case isBuildkiteAvailable(entity): return ; - case isGitHubActionsAvailable(entity): - return ; case isCircleCIAvailable(entity): return ; + case isGitHubActionsAvailable(entity): + return ; case isCloudbuildAvailable(entity): return ; case isTravisCIAvailable(entity): diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index c9ad54659f..eaa8c319a0 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -29,6 +29,8 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "circleci-api": "^4.0.0", + "dayjs": "^1.9.4", + "lodash": "^4.17.15", "moment": "^2.25.3", "react": "^16.13.1", "react-dom": "^16.13.1", diff --git a/plugins/circleci/src/components/BuildWithStepsPage/BuildWithStepsPage.tsx b/plugins/circleci/src/components/BuildWithStepsPage/BuildWithStepsPage.tsx index 30994f5ce3..18ae5a2d04 100644 --- a/plugins/circleci/src/components/BuildWithStepsPage/BuildWithStepsPage.tsx +++ b/plugins/circleci/src/components/BuildWithStepsPage/BuildWithStepsPage.tsx @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { FC, useEffect } from 'react'; + +import React, { useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { InfoCard, Progress, Link } from '@backstage/core'; import { BuildWithSteps, BuildStepAction } from '../../api'; @@ -31,7 +32,8 @@ import LaunchIcon from '@material-ui/icons/Launch'; import { useBuildWithSteps } from '../../state/useBuildWithSteps'; const IconLink = (IconButton as any) as typeof MaterialLink; -const BuildName: FC<{ build?: BuildWithSteps }> = ({ build }) => ( + +const BuildName = ({ build }: { build?: BuildWithSteps }) => ( #{build?.build_num} - {build?.subject} @@ -39,6 +41,7 @@ const BuildName: FC<{ build?: BuildWithSteps }> = ({ build }) => ( ); + const useStyles = makeStyles(theme => ({ neutral: {}, failed: { @@ -96,7 +99,7 @@ const pickClassName = ( return classes.neutral; }; -const BuildsList: FC<{ build?: BuildWithSteps }> = ({ build }) => ( +const BuildsList = ({ build }: { build?: BuildWithSteps }) => ( {build && build.steps && @@ -108,8 +111,11 @@ const BuildsList: FC<{ build?: BuildWithSteps }> = ({ build }) => ( ); -const ActionsList: FC<{ actions: BuildStepAction[]; name: string }> = ({ +const ActionsList = ({ actions, +}: { + actions: BuildStepAction[]; + name: string; }) => { const classes = useStyles(); return ( diff --git a/plugins/circleci/src/components/BuildsPage/lib/Builds/Builds.tsx b/plugins/circleci/src/components/BuildsPage/lib/Builds/Builds.tsx index bed96bb24c..685e61a5af 100644 --- a/plugins/circleci/src/components/BuildsPage/lib/Builds/Builds.tsx +++ b/plugins/circleci/src/components/BuildsPage/lib/Builds/Builds.tsx @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { FC } from 'react'; +import React from 'react'; import { CITable } from '../CITable'; import { useBuilds } from '../../../../state'; -export const Builds: FC<{}> = () => { +export const Builds = () => { const [ { total, loading, value, projectName, page, pageSize }, { setPage, retry, setPageSize }, diff --git a/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx b/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx index 8e693429c2..e5e8b951be 100644 --- a/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx +++ b/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx @@ -13,10 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { FC } from 'react'; -import { Link, Typography, Box, IconButton } from '@material-ui/core'; + +import React from 'react'; +import { + Avatar, + Link, + Typography, + Box, + IconButton, + makeStyles, +} from '@material-ui/core'; import RetryIcon from '@material-ui/icons/Replay'; import GitHubIcon from '@material-ui/icons/GitHub'; +import LaunchIcon from '@material-ui/icons/Launch'; import { Link as RouterLink, generatePath } from 'react-router-dom'; import { StatusError, @@ -27,17 +36,22 @@ import { Table, TableColumn, } from '@backstage/core'; +import { durationHumanized, relativeTimeTo } from '../../../../util'; import { circleCIBuildRouteRef } from '../../../../route-refs'; export type CITableBuildInfo = { id: string; buildName: string; buildUrl?: string; + startTime?: string; + stopTime?: string; source: { branchName: string; commit: { hash: string; + shortHash: string; url: string; + committerName?: string; }; }; status: string; @@ -48,6 +62,18 @@ export type CITableBuildInfo = { failed: number; testUrl: string; // fixme better name }; + workflow: { + id: string; + url: string; + name?: string; + jobName?: string; + }; + user: { + isUser: boolean; + login: string; + name?: string; + avatarUrl?: string; + }; onRestartClick: () => void; }; @@ -69,6 +95,43 @@ const getStatusComponent = (status: string | undefined = '') => { } }; +const useStyles = makeStyles(theme => ({ + root: { + display: 'flex', + '& > *': { + margin: theme.spacing(1), + verticalAlign: 'center', + }, + }, + small: { + width: theme.spacing(3), + height: theme.spacing(3), + }, +})); + +const SourceInfo = ({ build }: { build: CITableBuildInfo }) => { + const classes = useStyles(); + const { user, source } = build; + + return ( + + + + {source?.branchName} + + {source?.commit?.url !== undefined ? ( + + {source?.commit.shortHash} + + ) : ( + source?.commit.shortHash + )} + + + + ); +}; + const generatedColumns: TableColumn[] = [ { title: 'ID', @@ -80,26 +143,43 @@ const generatedColumns: TableColumn[] = [ title: 'Build', field: 'buildName', highlight: true, + width: '20%', render: (row: Partial) => ( - {row.buildName} + {row.buildName ? row.buildName : row?.workflow?.name} + + ), + }, + { + title: 'Job', + field: 'buildName', + highlight: true, + render: (row: Partial) => ( + + + + + {row?.workflow?.jobName} + ), }, { title: 'Source', + field: 'source.commit.hash', + highlight: true, render: (row: Partial) => ( - <> -

{row.source?.branchName}

-

{row.source?.commit.hash}

- + ), }, { title: 'Status', + field: 'status', render: (row: Partial) => ( {getStatusComponent(row.status)} @@ -108,14 +188,32 @@ const generatedColumns: TableColumn[] = [ ), }, + { + title: 'Time', + field: 'startTime', + render: (row: Partial) => ( + <> + + run {relativeTimeTo(row?.startTime)} + + + took {durationHumanized(row?.startTime, row?.stopTime)} + + + ), + }, + { + title: 'Workflow', + field: 'workflow.name', + }, { title: 'Actions', + width: '10%', render: (row: Partial) => ( ), - width: '10%', }, ]; @@ -130,7 +228,8 @@ type Props = { pageSize: number; onChangePageSize: (pageSize: number) => void; }; -export const CITable: FC = ({ + +export const CITable = ({ projectName, loading, pageSize, @@ -140,11 +239,16 @@ export const CITable: FC = ({ onChangePage, onChangePageSize, total, -}) => { +}: Props) => { return ( { if (!status) return ''; @@ -41,6 +43,39 @@ const makeReadableStatus = (status: string | undefined) => { } as Record)[status]; }; +const mapWorkflowDetails = (buildData: BuildSummary) => { + // Workflows should be an object: fixed in https://github.com/worldturtlemedia/circleci-api/pull/787 + const { workflows } = (buildData as any) ?? {}; + + return { + id: workflows?.workflow_id, + url: `${buildData.build_url}/workflows/${workflows?.workflow_id}`, + jobName: workflows?.job_name, + name: workflows?.workflow_name, + }; +}; + +const mapSourceDetails = (buildData: BuildSummary) => { + const commitDetails = getOr({}, 'all_commit_details[0]', buildData); + + return { + branchName: String(buildData.branch), + commit: { + hash: String(buildData.vcs_revision), + shortHash: String(buildData.vcs_revision).substr(0, 7), + committerName: buildData.committer_name, + url: commitDetails.commit_url, + }, + }; +}; + +const mapUser = (buildData: BuildSummary) => ({ + isUser: buildData?.user?.is_user || false, + login: buildData?.user?.login || 'none', + name: (buildData?.user as any)?.name, + avatarUrl: (buildData?.user as any)?.avatar_url, +}); + export const transform = ( buildsData: BuildSummary[], restartBuild: { (buildId: number): Promise }, @@ -52,16 +87,14 @@ export const transform = ( ? buildData.subject + (buildData.retry_of ? ` (retry of #${buildData.retry_of})` : '') : '', + startTime: buildData.start_time, + stopTime: buildData.stop_time, onRestartClick: () => typeof buildData.build_num !== 'undefined' && restartBuild(buildData.build_num), - source: { - branchName: String(buildData.branch), - commit: { - hash: String(buildData.vcs_revision), - url: 'todo', - }, - }, + source: mapSourceDetails(buildData), + workflow: mapWorkflowDetails(buildData), + user: mapUser(buildData), status: makeReadableStatus(buildData.status), buildUrl: buildData.build_url, }; @@ -79,6 +112,7 @@ export const useProjectSlugFromEntity = () => { export function mapVcsType(vcs: string): GitType { switch (vcs) { + case 'gh': case 'github': return GitType.GITHUB; default: @@ -93,7 +127,7 @@ export function useBuilds() { const [total, setTotal] = useState(0); const [page, setPage] = useState(0); - const [pageSize, setPageSize] = useState(5); + const [pageSize, setPageSize] = useState(10); const getBuilds = useCallback( async ({ limit, offset }: { limit: number; offset: number }) => { diff --git a/plugins/circleci/src/util/index.ts b/plugins/circleci/src/util/index.ts new file mode 100644 index 0000000000..55bb001ae4 --- /dev/null +++ b/plugins/circleci/src/util/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './time'; diff --git a/plugins/circleci/src/util/time.test.ts b/plugins/circleci/src/util/time.test.ts new file mode 100644 index 0000000000..dae029c8d2 --- /dev/null +++ b/plugins/circleci/src/util/time.test.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { durationHumanized, relativeTimeTo } from './time'; + +describe('times utils', () => { + describe('toRelativeTime', () => { + it('should give a relative time of x from today', () => { + expect(relativeTimeTo('2020-01-01')).toEqual(expect.any(String)); + }); + }); + + describe('durationHumanized', () => { + it('should give a humanized duration', () => { + expect(durationHumanized('2020-11-01', '2020-11-03')).toBe('2 days'); + }); + }); +}); diff --git a/plugins/circleci/src/util/time.ts b/plugins/circleci/src/util/time.ts new file mode 100644 index 0000000000..8756333a87 --- /dev/null +++ b/plugins/circleci/src/util/time.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import dayjs from 'dayjs'; +import durationPlugin from 'dayjs/plugin/duration'; +import relativeTimePlugin from 'dayjs/plugin/relativeTime'; + +dayjs.extend(durationPlugin); +dayjs.extend(relativeTimePlugin); + +type DateTimeObject = Date | string | number | undefined; + +export function relativeTimeTo(time: DateTimeObject, withoutSuffix = false) { + return dayjs().to(dayjs(time), withoutSuffix); +} + +export function durationHumanized( + startTime: DateTimeObject, + endTime: DateTimeObject, +) { + return dayjs.duration(dayjs(startTime).diff(dayjs(endTime))).humanize(); +} From 05b916761d036f1366abf084f1fb7f4d1782b491 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 23 Nov 2020 21:05:37 +0100 Subject: [PATCH 63/94] fix(kubernetes-backend): Fix Typescript error stopping backend to start --- .../src/service/getKubernetesObjectsForServiceHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts index 1b0cf4f783..82220f5d6c 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts @@ -97,12 +97,12 @@ export const handleGetKubernetesObjectsForService: GetKubernetesObjectsForServic return Promise.all( clusterDetailsDecoratedForAuth.map(clusterDetails => { return fetcher - .fetchObjectsForService({ + .fetchObjectsForService({ serviceId, clusterDetails, objectTypesToFetch, labelSelector, - }) + } as ObjectFetchParams) .then(result => { return { cluster: { From ef5810f51358fb8752f1917af5480746f0c4bbbc Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 23 Nov 2020 21:40:12 +0100 Subject: [PATCH 64/94] eslint: Enforce consistent TS type assertions https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/consistent-type-assertions.md --- packages/cli/config/eslint.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/config/eslint.js b/packages/cli/config/eslint.js index 9b1880c1db..e2a807e42e 100644 --- a/packages/cli/config/eslint.js +++ b/packages/cli/config/eslint.js @@ -58,6 +58,7 @@ module.exports = { ], 'no-unused-expressions': 'off', '@typescript-eslint/no-unused-expressions': 'error', + '@typescript-eslint/consistent-type-assertions': 'error', '@typescript-eslint/no-unused-vars': [ 'warn', { From faf311c261be51c547469432c6551fa926741d08 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 23 Nov 2020 21:57:34 +0100 Subject: [PATCH 65/94] backstage-cli: Add changeset about new lint rule --- .changeset/new-ladybugs-nail.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/new-ladybugs-nail.md diff --git a/.changeset/new-ladybugs-nail.md b/.changeset/new-ladybugs-nail.md new file mode 100644 index 0000000000..5351b6dff3 --- /dev/null +++ b/.changeset/new-ladybugs-nail.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +New lint rule to disallow assertions and promote `as` assertions. - @typescript-eslint/consistent-type-assertions From 5a8c24c7110782b02ba745bf9f37b92646fa0b37 Mon Sep 17 00:00:00 2001 From: Mateusz Lewtak <36006588+lewtakm@users.noreply.github.com> Date: Tue, 24 Nov 2020 10:44:08 +0100 Subject: [PATCH 66/94] Feat: Add Jira plugin to marketplace (#3405) * Feat: Add Jira plugin to marketplace * Update jira.yaml --- microsite/data/plugins/jira.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 microsite/data/plugins/jira.yaml diff --git a/microsite/data/plugins/jira.yaml b/microsite/data/plugins/jira.yaml new file mode 100644 index 0000000000..579243d11c --- /dev/null +++ b/microsite/data/plugins/jira.yaml @@ -0,0 +1,9 @@ +--- +title: Jira +author: roadie.io +authorUrl: https://roadie.io +category: Project Management +description: View Jira summary for your projects in Backstage. +documentation: https://roadie.io/backstage/plugins/jira +iconUrl: https://roadie.io/images/logos/jira.png +npmPackageName: '@roadiehq/backstage-plugin-jira' From 27733fdfc6844148e2706b49f6188d4f1349e2fa Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Tue, 24 Nov 2020 04:49:05 -0500 Subject: [PATCH 67/94] Update .changeset/mighty-cycles-impress.md Co-authored-by: Ben Lambert --- .changeset/mighty-cycles-impress.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/mighty-cycles-impress.md b/.changeset/mighty-cycles-impress.md index 33b2aa008c..56ec35098f 100644 --- a/.changeset/mighty-cycles-impress.md +++ b/.changeset/mighty-cycles-impress.md @@ -1,5 +1,4 @@ --- -'example-app': patch '@backstage/plugin-circleci': patch --- From fefb0e18e02d67adca5839d27bb855ff4aee2ed5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 24 Nov 2020 10:17:02 +0000 Subject: [PATCH 68/94] Version Packages --- .changeset/breezy-cobras-deny.md | 5 --- .changeset/chatty-radios-beam.md | 7 ---- .changeset/clever-moons-shake.md | 5 --- .changeset/cli-links.md | 5 --- .changeset/cost-insights-quiet-bikes-occur.md | 5 --- .../cost-insights-tiny-llamas-perform.md | 5 --- .changeset/curly-yaks-invite.md | 5 --- .changeset/dull-pans-sip.md | 6 --- .changeset/empty-kids-look.md | 5 --- .changeset/forty-buses-watch.md | 14 ------- .changeset/long-flowers-wink.md | 5 --- .changeset/mighty-cycles-impress.md | 5 --- .changeset/new-ladybugs-nail.md | 5 --- .changeset/orange-cherries-run.md | 5 --- .changeset/poor-mails-marry.md | 5 --- .changeset/small-worms-check.md | 6 --- .changeset/tame-rockets-explode.md | 5 --- .changeset/tidy-brooms-look.md | 5 --- packages/app/CHANGELOG.md | 37 ++++++++++++++++++ packages/app/package.json | 38 +++++++++---------- packages/backend-common/CHANGELOG.md | 7 ++++ packages/backend-common/package.json | 4 +- packages/backend/CHANGELOG.md | 21 ++++++++++ packages/backend/package.json | 20 +++++----- packages/catalog-client/CHANGELOG.md | 8 ++++ packages/catalog-client/package.json | 6 +-- packages/catalog-model/CHANGELOG.md | 18 +++++++++ packages/catalog-model/package.json | 4 +- packages/cli/CHANGELOG.md | 9 +++++ packages/cli/package.json | 6 +-- packages/core/CHANGELOG.md | 6 +++ packages/core/package.json | 4 +- packages/create-app/package.json | 32 ++++++++-------- plugins/api-docs/CHANGELOG.md | 11 ++++++ plugins/api-docs/package.json | 10 ++--- plugins/auth-backend/CHANGELOG.md | 12 ++++++ plugins/auth-backend/package.json | 10 ++--- plugins/catalog-backend/CHANGELOG.md | 22 +++++++++++ plugins/catalog-backend/package.json | 8 ++-- plugins/catalog-graphql/CHANGELOG.md | 11 ++++++ plugins/catalog-graphql/package.json | 8 ++-- plugins/catalog/CHANGELOG.md | 14 +++++++ plugins/catalog/package.json | 14 +++---- plugins/circleci/CHANGELOG.md | 12 ++++++ plugins/circleci/package.json | 10 ++--- plugins/cloudbuild/CHANGELOG.md | 11 ++++++ plugins/cloudbuild/package.json | 10 ++--- plugins/cost-insights/CHANGELOG.md | 9 +++++ plugins/cost-insights/package.json | 6 +-- plugins/github-actions/CHANGELOG.md | 11 ++++++ plugins/github-actions/package.json | 10 ++--- plugins/jenkins/CHANGELOG.md | 11 ++++++ plugins/jenkins/package.json | 10 ++--- plugins/kubernetes-backend/CHANGELOG.md | 15 ++++++++ plugins/kubernetes-backend/package.json | 8 ++-- plugins/kubernetes/CHANGELOG.md | 15 ++++++++ plugins/kubernetes/package.json | 10 ++--- plugins/lighthouse/CHANGELOG.md | 11 ++++++ plugins/lighthouse/package.json | 10 ++--- plugins/register-component/CHANGELOG.md | 12 ++++++ plugins/register-component/package.json | 10 ++--- plugins/rollbar/CHANGELOG.md | 11 ++++++ plugins/rollbar/package.json | 10 ++--- plugins/scaffolder-backend/CHANGELOG.md | 13 +++++++ plugins/scaffolder-backend/package.json | 8 ++-- plugins/scaffolder/CHANGELOG.md | 12 ++++++ plugins/scaffolder/package.json | 10 ++--- plugins/search/CHANGELOG.md | 13 +++++++ plugins/search/package.json | 10 ++--- plugins/sentry/CHANGELOG.md | 10 +++++ plugins/sentry/package.json | 8 ++-- plugins/sonarqube/CHANGELOG.md | 11 ++++++ plugins/sonarqube/package.json | 8 ++-- plugins/techdocs-backend/CHANGELOG.md | 11 ++++++ plugins/techdocs-backend/package.json | 8 ++-- plugins/techdocs/CHANGELOG.md | 11 ++++++ plugins/techdocs/package.json | 10 ++--- 77 files changed, 535 insertions(+), 263 deletions(-) delete mode 100644 .changeset/breezy-cobras-deny.md delete mode 100644 .changeset/chatty-radios-beam.md delete mode 100644 .changeset/clever-moons-shake.md delete mode 100644 .changeset/cli-links.md delete mode 100644 .changeset/cost-insights-quiet-bikes-occur.md delete mode 100644 .changeset/cost-insights-tiny-llamas-perform.md delete mode 100644 .changeset/curly-yaks-invite.md delete mode 100644 .changeset/dull-pans-sip.md delete mode 100644 .changeset/empty-kids-look.md delete mode 100644 .changeset/forty-buses-watch.md delete mode 100644 .changeset/long-flowers-wink.md delete mode 100644 .changeset/mighty-cycles-impress.md delete mode 100644 .changeset/new-ladybugs-nail.md delete mode 100644 .changeset/orange-cherries-run.md delete mode 100644 .changeset/poor-mails-marry.md delete mode 100644 .changeset/small-worms-check.md delete mode 100644 .changeset/tame-rockets-explode.md delete mode 100644 .changeset/tidy-brooms-look.md create mode 100644 plugins/search/CHANGELOG.md diff --git a/.changeset/breezy-cobras-deny.md b/.changeset/breezy-cobras-deny.md deleted file mode 100644 index 5f035c05ce..0000000000 --- a/.changeset/breezy-cobras-deny.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core': patch ---- - -Clear sidebar search field once a search is executed diff --git a/.changeset/chatty-radios-beam.md b/.changeset/chatty-radios-beam.md deleted file mode 100644 index 91ad76ee88..0000000000 --- a/.changeset/chatty-radios-beam.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/catalog-model': minor -'@backstage/plugin-kubernetes': minor -'@backstage/plugin-kubernetes-backend': minor ---- - -add kubernetes selector to component model diff --git a/.changeset/clever-moons-shake.md b/.changeset/clever-moons-shake.md deleted file mode 100644 index 0fb6c690c2..0000000000 --- a/.changeset/clever-moons-shake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Added readTree support to AzureUrlReader diff --git a/.changeset/cli-links.md b/.changeset/cli-links.md deleted file mode 100644 index 0a9779de93..0000000000 --- a/.changeset/cli-links.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -The CLI now detects and transforms linked packages. You can link in external packages by adding them to both the `lerna.json` and `package.json` workspace paths. diff --git a/.changeset/cost-insights-quiet-bikes-occur.md b/.changeset/cost-insights-quiet-bikes-occur.md deleted file mode 100644 index aac283bbd4..0000000000 --- a/.changeset/cost-insights-quiet-bikes-occur.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-cost-insights': patch ---- - -fix product icon configuration diff --git a/.changeset/cost-insights-tiny-llamas-perform.md b/.changeset/cost-insights-tiny-llamas-perform.md deleted file mode 100644 index c81c28f8f8..0000000000 --- a/.changeset/cost-insights-tiny-llamas-perform.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-cost-insights': patch ---- - -truncate large percentages > 1000% diff --git a/.changeset/curly-yaks-invite.md b/.changeset/curly-yaks-invite.md deleted file mode 100644 index b9191a3895..0000000000 --- a/.changeset/curly-yaks-invite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Add support for reading groups and users from the Microsoft Graph API. diff --git a/.changeset/dull-pans-sip.md b/.changeset/dull-pans-sip.md deleted file mode 100644 index 13616c1f8d..0000000000 --- a/.changeset/dull-pans-sip.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-scaffolder-backend': patch ---- - -Move constructing the catalog-info.yaml URL for scaffolded components to the publishers diff --git a/.changeset/empty-kids-look.md b/.changeset/empty-kids-look.md deleted file mode 100644 index 680cd7f25c..0000000000 --- a/.changeset/empty-kids-look.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-register-component': patch ---- - -Remove catalog link on validate popup diff --git a/.changeset/forty-buses-watch.md b/.changeset/forty-buses-watch.md deleted file mode 100644 index 4074566fe7..0000000000 --- a/.changeset/forty-buses-watch.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/catalog-model': patch -'@backstage/plugin-catalog-backend': patch ---- - -Marked the `Group` entity fields `ancestors` and `descendants` for deprecation on Dec 6th, 2020. See https://github.com/backstage/backstage/issues/3049 for details. - -Code that consumes these fields should remove those usages as soon as possible. There is no current or planned replacement for these fields. - -The BuiltinKindsEntityProcessor has been updated to inject these fields as empty arrays if they are missing. Therefore, if you are on a catalog instance that uses the updated version of this code, you can start removing the fields from your source catalog-info.yaml data as well, without breaking validation. - -After Dec 6th, the fields will be removed from types and classes of the Backstage repository. At the first release after that, they will not be present in released packages either. - -If your catalog-info.yaml files still contain these fields after the deletion, they will still be valid and your ingestion will not break, but they won't be visible in the types for consuming code. diff --git a/.changeset/long-flowers-wink.md b/.changeset/long-flowers-wink.md deleted file mode 100644 index 7f9cf12315..0000000000 --- a/.changeset/long-flowers-wink.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-sonarqube': patch ---- - -Add configuration schema diff --git a/.changeset/mighty-cycles-impress.md b/.changeset/mighty-cycles-impress.md deleted file mode 100644 index 56ec35098f..0000000000 --- a/.changeset/mighty-cycles-impress.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-circleci': patch ---- - -Improved CircleCI builds table to show more information and relevant links diff --git a/.changeset/new-ladybugs-nail.md b/.changeset/new-ladybugs-nail.md deleted file mode 100644 index 5351b6dff3..0000000000 --- a/.changeset/new-ladybugs-nail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -New lint rule to disallow assertions and promote `as` assertions. - @typescript-eslint/consistent-type-assertions diff --git a/.changeset/orange-cherries-run.md b/.changeset/orange-cherries-run.md deleted file mode 100644 index 3c06c27b47..0000000000 --- a/.changeset/orange-cherries-run.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Add experimental backend:bundle command diff --git a/.changeset/poor-mails-marry.md b/.changeset/poor-mails-marry.md deleted file mode 100644 index fdc7e6f1b8..0000000000 --- a/.changeset/poor-mails-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Add new `versions:check` and `versions:bump` commands to simplify version management and avoid conflicts diff --git a/.changeset/small-worms-check.md b/.changeset/small-worms-check.md deleted file mode 100644 index e39265f8f9..0000000000 --- a/.changeset/small-worms-check.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'example-app': patch -'@backstage/plugin-search': patch ---- - -Using the search field in the sidebar now navigates to the search result page. diff --git a/.changeset/tame-rockets-explode.md b/.changeset/tame-rockets-explode.md deleted file mode 100644 index 123472701e..0000000000 --- a/.changeset/tame-rockets-explode.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Make integration host and url configurations visible in the frontend diff --git a/.changeset/tidy-brooms-look.md b/.changeset/tidy-brooms-look.md deleted file mode 100644 index f2c2a9064e..0000000000 --- a/.changeset/tidy-brooms-look.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Fix React entity YAML filename to new standard diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index ab00924a26..f70cd198b4 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,42 @@ # example-app +## 0.2.3 + +### Patch Changes + +- 475fc0aaa: Using the search field in the sidebar now navigates to the search result page. +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [29a0ccab2] +- Updated dependencies [8e6728e25] +- Updated dependencies [c93a14b49] +- Updated dependencies [ef2831dde] +- Updated dependencies [2a71f4bab] +- Updated dependencies [1185919f3] +- Updated dependencies [a8de7f554] +- Updated dependencies [faf311c26] +- Updated dependencies [31d8b6979] +- Updated dependencies [991345969] +- Updated dependencies [475fc0aaa] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-kubernetes@0.3.0 + - @backstage/cli@0.3.1 + - @backstage/plugin-cost-insights@0.4.1 + - @backstage/plugin-scaffolder@0.3.1 + - @backstage/plugin-register-component@0.2.2 + - @backstage/plugin-circleci@0.2.2 + - @backstage/plugin-search@0.2.1 + - @backstage/plugin-api-docs@0.2.2 + - @backstage/plugin-catalog@0.2.3 + - @backstage/plugin-cloudbuild@0.2.2 + - @backstage/plugin-github-actions@0.2.2 + - @backstage/plugin-jenkins@0.3.1 + - @backstage/plugin-lighthouse@0.2.3 + - @backstage/plugin-rollbar@0.2.3 + - @backstage/plugin-sentry@0.2.3 + - @backstage/plugin-techdocs@0.2.3 + ## 0.2.2 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 042fd47188..9e54005cd3 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,33 +1,33 @@ { "name": "example-app", - "version": "0.2.2", + "version": "0.2.3", "private": true, "bundled": true, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/cli": "^0.3.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-api-docs": "^0.2.1", - "@backstage/plugin-catalog": "^0.2.2", - "@backstage/plugin-circleci": "^0.2.1", - "@backstage/plugin-cloudbuild": "^0.2.1", - "@backstage/plugin-cost-insights": "^0.4.0", + "@backstage/catalog-model": "^0.3.0", + "@backstage/cli": "^0.3.1", + "@backstage/core": "^0.3.2", + "@backstage/plugin-api-docs": "^0.2.2", + "@backstage/plugin-catalog": "^0.2.3", + "@backstage/plugin-circleci": "^0.2.2", + "@backstage/plugin-cloudbuild": "^0.2.2", + "@backstage/plugin-cost-insights": "^0.4.1", "@backstage/plugin-explore": "^0.2.1", "@backstage/plugin-gcp-projects": "^0.2.1", - "@backstage/plugin-github-actions": "^0.2.1", + "@backstage/plugin-github-actions": "^0.2.2", "@backstage/plugin-gitops-profiles": "^0.2.1", "@backstage/plugin-graphiql": "^0.2.1", - "@backstage/plugin-jenkins": "^0.3.0", - "@backstage/plugin-kubernetes": "^0.2.1", - "@backstage/plugin-lighthouse": "^0.2.2", + "@backstage/plugin-jenkins": "^0.3.1", + "@backstage/plugin-kubernetes": "^0.3.0", + "@backstage/plugin-lighthouse": "^0.2.3", "@backstage/plugin-newrelic": "^0.2.1", - "@backstage/plugin-register-component": "^0.2.1", - "@backstage/plugin-rollbar": "^0.2.2", - "@backstage/plugin-scaffolder": "^0.3.0", - "@backstage/plugin-sentry": "^0.2.2", - "@backstage/plugin-search": "^0.2.0", + "@backstage/plugin-register-component": "^0.2.2", + "@backstage/plugin-rollbar": "^0.2.3", + "@backstage/plugin-scaffolder": "^0.3.1", + "@backstage/plugin-sentry": "^0.2.3", + "@backstage/plugin-search": "^0.2.1", "@backstage/plugin-tech-radar": "^0.3.0", - "@backstage/plugin-techdocs": "^0.2.2", + "@backstage/plugin-techdocs": "^0.2.3", "@backstage/plugin-user-settings": "^0.2.2", "@backstage/plugin-welcome": "^0.2.1", "@backstage/test-utils": "^0.1.3", diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 4822032c7f..793a554905 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/backend-common +## 0.3.1 + +### Patch Changes + +- bff3305aa: Added readTree support to AzureUrlReader +- b47dce06f: Make integration host and url configurations visible in the frontend + ## 0.3.0 ### Minor Changes diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index b22e4638f5..731e2679cb 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -67,7 +67,7 @@ } }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/test-utils": "^0.1.3", "@types/archiver": "^3.1.1", "@types/compression": "^1.7.0", diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index a8bc1fef7d..bc2a08e983 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,26 @@ # example-backend +## 0.2.3 + +### Patch Changes + +- Updated dependencies [1166fcc36] +- Updated dependencies [bff3305aa] +- Updated dependencies [0c2121240] +- Updated dependencies [ef2831dde] +- Updated dependencies [1185919f3] +- Updated dependencies [475fc0aaa] +- Updated dependencies [b47dce06f] +- Updated dependencies [5a1d8dca3] + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-kubernetes-backend@0.2.0 + - @backstage/backend-common@0.3.1 + - @backstage/plugin-catalog-backend@0.2.2 + - @backstage/plugin-scaffolder-backend@0.3.2 + - example-app@0.2.3 + - @backstage/plugin-auth-backend@0.2.3 + - @backstage/plugin-techdocs-backend@0.2.2 + ## 0.2.2 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 014a3f78d5..5d3678a284 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.2", + "version": "0.2.3", "main": "dist/index.cjs.js", "types": "src/index.ts", "private": true, @@ -18,24 +18,24 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "@backstage/plugin-app-backend": "^0.3.0", - "@backstage/plugin-auth-backend": "^0.2.2", - "@backstage/plugin-catalog-backend": "^0.2.1", + "@backstage/plugin-auth-backend": "^0.2.3", + "@backstage/plugin-catalog-backend": "^0.2.2", "@backstage/plugin-graphql-backend": "^0.1.3", - "@backstage/plugin-kubernetes-backend": "^0.1.3", + "@backstage/plugin-kubernetes-backend": "^0.2.0", "@backstage/plugin-proxy-backend": "^0.2.1", "@backstage/plugin-rollbar-backend": "^0.1.3", - "@backstage/plugin-scaffolder-backend": "^0.3.1", + "@backstage/plugin-scaffolder-backend": "^0.3.2", "@backstage/plugin-sentry-backend": "^0.1.3", - "@backstage/plugin-techdocs-backend": "^0.2.1", + "@backstage/plugin-techdocs-backend": "^0.2.2", "@gitbeaker/node": "^25.2.0", "@octokit/rest": "^18.0.0", "azure-devops-node-api": "^10.1.1", "dockerode": "^3.2.0", - "example-app": "^0.2.2", + "example-app": "^0.2.3", "express": "^4.17.1", "express-promise-router": "^3.0.3", "knex": "^0.21.6", @@ -45,7 +45,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@types/dockerode": "^2.5.32", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5", diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index 574cd301bf..3454a6c8e0 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/catalog-client +## 0.3.1 + +### Patch Changes + +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/catalog-model@0.3.0 + ## 0.3.0 ### Minor Changes diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index 2482f9579d..4509b7d547 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-client", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,12 +20,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "cross-fetch": "^3.0.6" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@types/jest": "^26.0.7", "msw": "^0.21.2" }, diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index 051aa82c80..10bf540b46 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/catalog-model +## 0.3.0 + +### Minor Changes + +- 1166fcc36: add kubernetes selector to component model + +### Patch Changes + +- 1185919f3: Marked the `Group` entity fields `ancestors` and `descendants` for deprecation on Dec 6th, 2020. See https://github.com/backstage/backstage/issues/3049 for details. + + Code that consumes these fields should remove those usages as soon as possible. There is no current or planned replacement for these fields. + + The BuiltinKindsEntityProcessor has been updated to inject these fields as empty arrays if they are missing. Therefore, if you are on a catalog instance that uses the updated version of this code, you can start removing the fields from your source catalog-info.yaml data as well, without breaking validation. + + After Dec 6th, the fields will be removed from types and classes of the Backstage repository. At the first release after that, they will not be present in released packages either. + + If your catalog-info.yaml files still contain these fields after the deletion, they will still be valid and your ingestion will not break, but they won't be visible in the types for consuming code. + ## 0.2.0 ### Minor Changes diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index e712e66f58..7956454ca9 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-model", - "version": "0.2.0", + "version": "0.3.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,7 +29,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@types/express": "^4.17.6", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index a5bcc6bcfd..d198444c86 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/cli +## 0.3.1 + +### Patch Changes + +- 29a0ccab2: The CLI now detects and transforms linked packages. You can link in external packages by adding them to both the `lerna.json` and `package.json` workspace paths. +- faf311c26: New lint rule to disallow assertions and promote `as` assertions. - @typescript-eslint/consistent-type-assertions +- 31d8b6979: Add experimental backend:bundle command +- 991345969: Add new `versions:check` and `versions:bump` commands to simplify version management and avoid conflicts + ## 0.3.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index eb56b19971..034c8a3de5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.3.0", + "version": "0.3.1", "private": false, "publishConfig": { "access": "public" @@ -111,9 +111,9 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.3.0", + "@backstage/backend-common": "^0.3.1", "@backstage/config": "^0.1.1", - "@backstage/core": "^0.3.1", + "@backstage/core": "^0.3.2", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@backstage/theme": "^0.2.1", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 213e2b5246..d29bc53452 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/core +## 0.3.2 + +### Patch Changes + +- 475fc0aaa: Clear sidebar search field once a search is executed + ## 0.3.1 ### Patch Changes diff --git a/packages/core/package.json b/packages/core/package.json index d65f0a16e5..f667ffe7be 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core", "description": "Core API used by Backstage plugins and apps", - "version": "0.3.1", + "version": "0.3.2", "private": false, "publishConfig": { "access": "public", @@ -64,7 +64,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/packages/create-app/package.json b/packages/create-app/package.json index c01d4d3dbb..6e91a916a1 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -37,28 +37,28 @@ "recursive-readdir": "^2.2.2" }, "devDependencies": { - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", - "@backstage/cli": "^0.3.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/config": "^0.1.1", - "@backstage/core": "^0.3.1", - "@backstage/plugin-api-docs": "^0.2.1", + "@backstage/core": "^0.3.2", + "@backstage/plugin-api-docs": "^0.2.2", "@backstage/plugin-app-backend": "^0.3.0", - "@backstage/plugin-auth-backend": "^0.2.2", - "@backstage/plugin-catalog": "^0.2.2", - "@backstage/plugin-catalog-backend": "^0.2.1", - "@backstage/plugin-circleci": "^0.2.1", + "@backstage/plugin-auth-backend": "^0.2.3", + "@backstage/plugin-catalog": "^0.2.3", + "@backstage/plugin-catalog-backend": "^0.2.2", + "@backstage/plugin-circleci": "^0.2.2", "@backstage/plugin-explore": "^0.2.1", - "@backstage/plugin-github-actions": "^0.2.1", - "@backstage/plugin-lighthouse": "^0.2.2", + "@backstage/plugin-github-actions": "^0.2.2", + "@backstage/plugin-lighthouse": "^0.2.3", "@backstage/plugin-proxy-backend": "^0.2.1", - "@backstage/plugin-register-component": "^0.2.1", + "@backstage/plugin-register-component": "^0.2.2", "@backstage/plugin-rollbar-backend": "^0.1.3", - "@backstage/plugin-scaffolder": "^0.3.0", - "@backstage/plugin-scaffolder-backend": "^0.3.1", + "@backstage/plugin-scaffolder": "^0.3.1", + "@backstage/plugin-scaffolder-backend": "^0.3.2", "@backstage/plugin-tech-radar": "^0.3.0", - "@backstage/plugin-techdocs": "^0.2.2", - "@backstage/plugin-techdocs-backend": "^0.2.1", + "@backstage/plugin-techdocs": "^0.2.3", + "@backstage/plugin-techdocs-backend": "^0.2.2", "@backstage/plugin-user-settings": "^0.2.2", "@backstage/test-utils": "^0.1.3", "@backstage/theme": "^0.2.1", diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 5f1ad97e64..60a5caf55b 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-api-docs +## 0.2.2 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.1 ### Patch Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index bfefaef3a5..691f2680b2 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@kyma-project/asyncapi-react": "^0.14.2", "@material-icons/font": "^1.0.2", @@ -39,7 +39,7 @@ "swagger-ui-react": "^3.31.1" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 546855c92f..4a2161ad71 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-auth-backend +## 0.2.3 + +### Patch Changes + +- Updated dependencies [1166fcc36] +- Updated dependencies [bff3305aa] +- Updated dependencies [1185919f3] +- Updated dependencies [b47dce06f] + - @backstage/catalog-model@0.3.0 + - @backstage/backend-common@0.3.1 + - @backstage/catalog-client@0.3.1 + ## 0.2.2 ### Patch Changes diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 4d1e52aa21..43e2c59f88 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-client": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-client": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "@types/express": "^4.17.6", "compression": "^1.7.4", @@ -55,7 +55,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 1a3199ee14..3bf25a494b 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-catalog-backend +## 0.2.2 + +### Patch Changes + +- 0c2121240: Add support for reading groups and users from the Microsoft Graph API. +- 1185919f3: Marked the `Group` entity fields `ancestors` and `descendants` for deprecation on Dec 6th, 2020. See https://github.com/backstage/backstage/issues/3049 for details. + + Code that consumes these fields should remove those usages as soon as possible. There is no current or planned replacement for these fields. + + The BuiltinKindsEntityProcessor has been updated to inject these fields as empty arrays if they are missing. Therefore, if you are on a catalog instance that uses the updated version of this code, you can start removing the fields from your source catalog-info.yaml data as well, without breaking validation. + + After Dec 6th, the fields will be removed from types and classes of the Backstage repository. At the first release after that, they will not be present in released packages either. + + If your catalog-info.yaml files still contain these fields after the deletion, they will still be valid and your ingestion will not break, but they won't be visible in the types for consuming code. + +- Updated dependencies [1166fcc36] +- Updated dependencies [bff3305aa] +- Updated dependencies [1185919f3] +- Updated dependencies [b47dce06f] + - @backstage/catalog-model@0.3.0 + - @backstage/backend-common@0.3.1 + ## 0.2.1 ### Patch Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 12a17d6365..51a9978f30 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ }, "dependencies": { "@azure/msal-node": "^1.0.0-alpha.8", - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "@octokit/graphql": "^4.5.6", "@types/express": "^4.17.6", @@ -47,7 +47,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/test-utils": "^0.1.3", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/catalog-graphql/CHANGELOG.md b/plugins/catalog-graphql/CHANGELOG.md index 35737b7dd1..56911b9c45 100644 --- a/plugins/catalog-graphql/CHANGELOG.md +++ b/plugins/catalog-graphql/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-graphql +## 0.2.2 + +### Patch Changes + +- Updated dependencies [1166fcc36] +- Updated dependencies [bff3305aa] +- Updated dependencies [1185919f3] +- Updated dependencies [b47dce06f] + - @backstage/catalog-model@0.3.0 + - @backstage/backend-common@0.3.1 + ## 0.2.1 ### Patch Changes diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index c55be89b62..a489d66e5b 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graphql", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "@graphql-modules/core": "^0.7.17", "apollo-server": "^2.16.1", @@ -32,7 +32,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/test-utils": "^0.1.3", "@graphql-codegen/cli": "^1.17.7", "@graphql-codegen/typescript": "^1.17.7", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 1b8b884f1c..9137b7e948 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog +## 0.2.3 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [ef2831dde] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-scaffolder@0.3.1 + - @backstage/catalog-client@0.3.1 + - @backstage/plugin-techdocs@0.2.3 + ## 0.2.2 ### Patch Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 2c65096fd6..7b58358734 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,11 +21,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-client": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-scaffolder": "^0.3.0", - "@backstage/plugin-techdocs": "^0.2.2", + "@backstage/catalog-client": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-scaffolder": "^0.3.1", + "@backstage/plugin-techdocs": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -43,7 +43,7 @@ "swr": "^0.3.0" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@microsoft/microsoft-graph-types": "^1.25.0", diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md index 2894ce1bd3..a81531e28d 100644 --- a/plugins/circleci/CHANGELOG.md +++ b/plugins/circleci/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-circleci +## 0.2.2 + +### Patch Changes + +- a8de7f554: Improved CircleCI builds table to show more information and relevant links +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.1 ### Patch Changes diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index eaa8c319a0..fd32ff9891 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-circleci", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "postpack": "backstage-cli postpack" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -40,7 +40,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md index 9d02fd1ea3..4cb2d01377 100644 --- a/plugins/cloudbuild/CHANGELOG.md +++ b/plugins/cloudbuild/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-cloudbuild +## 0.2.2 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.1 ### Patch Changes diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index c0b36ef1e7..3e7f0c9175 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cloudbuild", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -39,7 +39,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index 157d92f251..b7bdfa9d50 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-cost-insights +## 0.4.1 + +### Patch Changes + +- 8e6728e25: fix product icon configuration +- c93a14b49: truncate large percentages > 1000% +- Updated dependencies [475fc0aaa] + - @backstage/core@0.3.2 + ## 0.4.0 ### Minor Changes diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 8ebeac91c0..06083b855b 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cost-insights", - "version": "0.4.0", + "version": "0.4.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,7 +22,7 @@ }, "dependencies": { "@backstage/config": "^0.1.1", - "@backstage/core": "^0.3.1", + "@backstage/core": "^0.3.2", "@backstage/test-utils": "^0.1.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", @@ -46,7 +46,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index b679d6844b..96f3876802 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-github-actions +## 0.2.2 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.1 ### Patch Changes diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 49ecfc5423..b1924034f1 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-actions", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", "@backstage/core-api": "^0.2.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -40,7 +40,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index 44febbc63d..aeeb069f19 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-jenkins +## 0.3.1 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.3.0 ### Minor Changes diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 0279592a75..961f551dfa 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -36,7 +36,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 7c55a7407d..2877264e54 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-kubernetes-backend +## 0.2.0 + +### Minor Changes + +- 1166fcc36: add kubernetes selector to component model + +### Patch Changes + +- Updated dependencies [1166fcc36] +- Updated dependencies [bff3305aa] +- Updated dependencies [1185919f3] +- Updated dependencies [b47dce06f] + - @backstage/catalog-model@0.3.0 + - @backstage/backend-common@0.3.1 + ## 0.1.3 ### Patch Changes diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index be7b86dd52..e607e51f80 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-backend", - "version": "0.1.3", + "version": "0.2.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "@kubernetes/client-node": "^0.12.1", "@types/express": "^4.17.6", @@ -38,7 +38,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index e5611e4953..652fc394a2 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-kubernetes +## 0.3.0 + +### Minor Changes + +- 1166fcc36: add kubernetes selector to component model + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-kubernetes-backend@0.2.0 + ## 0.2.1 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 36954cc5b9..2afff2ae3f 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.2.1", + "version": "0.3.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,10 +20,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", - "@backstage/core": "^0.3.1", - "@backstage/plugin-kubernetes-backend": "^0.1.3", + "@backstage/core": "^0.3.2", + "@backstage/plugin-kubernetes-backend": "^0.2.0", "@backstage/theme": "^0.2.1", "@kubernetes/client-node": "^0.12.1", "@material-ui/core": "^4.11.0", @@ -35,7 +35,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md index 666e7c4411..e35f3dece0 100644 --- a/plugins/lighthouse/CHANGELOG.md +++ b/plugins/lighthouse/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-lighthouse +## 0.2.3 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.2 ### Patch Changes diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 90230f5248..ad1264cfbb 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-lighthouse", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,11 +21,11 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", - "@backstage/core": "^0.3.1", + "@backstage/core": "^0.3.2", "@backstage/core-api": "^0.2.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -38,7 +38,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/register-component/CHANGELOG.md b/plugins/register-component/CHANGELOG.md index 3e966ef0d0..c569046855 100644 --- a/plugins/register-component/CHANGELOG.md +++ b/plugins/register-component/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-register-component +## 0.2.2 + +### Patch Changes + +- 2a71f4bab: Remove catalog link on validate popup +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.1 ### Patch Changes diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index 06ae17cb0e..1564faae0e 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-register-component", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -36,7 +36,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/rollbar/CHANGELOG.md b/plugins/rollbar/CHANGELOG.md index e4a6671f90..1ccfe16c41 100644 --- a/plugins/rollbar/CHANGELOG.md +++ b/plugins/rollbar/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-rollbar +## 0.2.3 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.2 ### Patch Changes diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index c107573ca8..1177a83eb6 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-rollbar", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -37,7 +37,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index a388a29a00..ea7e5c85ee 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-scaffolder-backend +## 0.3.2 + +### Patch Changes + +- ef2831dde: Move constructing the catalog-info.yaml URL for scaffolded components to the publishers +- 5a1d8dca3: Fix React entity YAML filename to new standard +- Updated dependencies [1166fcc36] +- Updated dependencies [bff3305aa] +- Updated dependencies [1185919f3] +- Updated dependencies [b47dce06f] + - @backstage/catalog-model@0.3.0 + - @backstage/backend-common@0.3.1 + ## 0.3.1 ### Patch Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 73c07d158f..a095167a08 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "0.3.1", + "version": "0.3.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "@gitbeaker/core": "^25.2.0", "@gitbeaker/node": "^25.2.0", @@ -48,7 +48,7 @@ "cross-fetch": "^3.0.6" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@octokit/types": "^5.4.1", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 9eac595997..c78e05c3e0 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder +## 0.3.1 + +### Patch Changes + +- ef2831dde: Move constructing the catalog-info.yaml URL for scaffolded components to the publishers +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.3.0 ### Minor Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 439fc0ad9a..791221bc62 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -41,7 +41,7 @@ "swr": "^0.3.0" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md new file mode 100644 index 0000000000..3545914e96 --- /dev/null +++ b/plugins/search/CHANGELOG.md @@ -0,0 +1,13 @@ +# @backstage/plugin-search + +## 0.2.1 + +### Patch Changes + +- 475fc0aaa: Using the search field in the sidebar now navigates to the search result page. +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 diff --git a/plugins/search/package.json b/plugins/search/package.json index df5c6c25b9..021e3f71f3 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "0.2.0", + "version": "0.2.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", - "@backstage/catalog-model": "^0.2.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", + "@backstage/catalog-model": "^0.3.0", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -34,7 +34,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index 92327c1668..b388548d8e 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-sentry +## 0.2.3 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + ## 0.2.2 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 80a37c574e..4c10936148 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -36,7 +36,7 @@ "timeago.js": "^4.0.2" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/sonarqube/CHANGELOG.md b/plugins/sonarqube/CHANGELOG.md index 0296bd0416..9ebd73b812 100644 --- a/plugins/sonarqube/CHANGELOG.md +++ b/plugins/sonarqube/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-sonarqube +## 0.1.4 + +### Patch Changes + +- 26484d413: Add configuration schema +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + ## 0.1.3 ### Patch Changes diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index be8a878e50..6a9d0fe0d4 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sonarqube", - "version": "0.1.3", + "version": "0.1.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -35,7 +35,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 4a3fbf6821..d9054230e2 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-techdocs-backend +## 0.2.2 + +### Patch Changes + +- Updated dependencies [1166fcc36] +- Updated dependencies [bff3305aa] +- Updated dependencies [1185919f3] +- Updated dependencies [b47dce06f] + - @backstage/catalog-model@0.3.0 + - @backstage/backend-common@0.3.1 + ## 0.2.1 ### Patch Changes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index b139bb9acf..1d9f3ea7c1 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "@types/dockerode": "^2.5.34", "@types/express": "^4.17.6", @@ -37,7 +37,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index bd3ae21ab8..3f46e1358b 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-techdocs +## 0.2.3 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.2 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 35b196e124..23d7a25b57 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", "@backstage/core-api": "^0.2.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/test-utils": "^0.1.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", @@ -39,7 +39,7 @@ "sanitize-html": "^1.27.0" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", From f3bb55ee31ca217ccdaf1b9ced8dc5018e3952bf Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 20 Oct 2020 10:40:58 +0200 Subject: [PATCH 69/94] Make api-docs customizable --- .changeset/chilled-zoos-rush.md | 7 + .../app/src/components/catalog/EntityPage.tsx | 133 ++++++++++++------ plugins/api-docs/package.json | 2 +- .../ApiEntityPage/ApiEntityPage.test.tsx | 71 ---------- .../ApiEntityPage/ApiEntityPage.tsx | 127 ----------------- .../src/components/ApiEntityPage/index.ts | 17 --- .../ApiExplorerTable/ApiExplorerTable.tsx | 16 +-- plugins/api-docs/src/index.ts | 3 +- plugins/api-docs/src/plugin.ts | 4 +- plugins/api-docs/src/routes.ts | 6 - 10 files changed, 106 insertions(+), 280 deletions(-) create mode 100644 .changeset/chilled-zoos-rush.md delete mode 100644 plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.test.tsx delete mode 100644 plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx delete mode 100644 plugins/api-docs/src/components/ApiEntityPage/index.ts diff --git a/.changeset/chilled-zoos-rush.md b/.changeset/chilled-zoos-rush.md new file mode 100644 index 0000000000..6830cb7fcf --- /dev/null +++ b/.changeset/chilled-zoos-rush.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-api-docs': minor +--- + +APIs now have real entity pages that are customizable in the app. +Therefore the old entity page from this plugin is removed. +See the `packages/app` on how to create and customize the API entity page. diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index c201fe88ad..d30de6abd1 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -13,63 +13,66 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { ApiEntity, Entity } from '@backstage/catalog-model'; +import { EmptyState } from '@backstage/core'; import { - isPluginApplicableToEntity as isTravisCIAvailable, - RecentTravisCIBuildsWidget, - Router as TravisCIRouter, -} from '@roadiehq/backstage-plugin-travis-ci'; + ApiDefinitionCard, + Router as ApiDocsRouter, +} from '@backstage/plugin-api-docs'; +import { + AboutCard, + EntityPageLayout, + useEntity, +} from '@backstage/plugin-catalog'; +import { + isPluginApplicableToEntity as isCircleCIAvailable, + Router as CircleCIRouter, +} from '@backstage/plugin-circleci'; +import { + isPluginApplicableToEntity as isCloudbuildAvailable, + Router as CloudbuildRouter, +} from '@backstage/plugin-cloudbuild'; import { isPluginApplicableToEntity as isGitHubActionsAvailable, RecentWorkflowRunsCard, Router as GitHubActionsRouter, } from '@backstage/plugin-github-actions'; import { - Router as CloudbuildRouter, - isPluginApplicableToEntity as isCloudbuildAvailable, -} from '@backstage/plugin-cloudbuild'; -import { - Router as JenkinsRouter, isPluginApplicableToEntity as isJenkinsAvailable, LatestRunCard as JenkinsLatestRunCard, + Router as JenkinsRouter, } from '@backstage/plugin-jenkins'; -import { - isPluginApplicableToEntity as isCircleCIAvailable, - Router as CircleCIRouter, -} from '@backstage/plugin-circleci'; -import { Router as ApiDocsRouter } from '@backstage/plugin-api-docs'; -import { Router as SentryRouter } from '@backstage/plugin-sentry'; -import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs'; import { Router as KubernetesRouter } from '@backstage/plugin-kubernetes'; -import { - Router as GitHubInsightsRouter, - isPluginApplicableToEntity as isGitHubAvailable, - ReadMeCard, - LanguagesCard, - ReleasesCard, -} from '@roadiehq/backstage-plugin-github-insights'; -import React, { ReactNode } from 'react'; -import { - AboutCard, - EntityPageLayout, - useEntity, -} from '@backstage/plugin-catalog'; -import { Entity } from '@backstage/catalog-model'; -import { Button, Grid } from '@material-ui/core'; -import { EmptyState } from '@backstage/core'; import { EmbeddedRouter as LighthouseRouter, - LastLighthouseAuditCard, isPluginApplicableToEntity as isLighthouseAvailable, + LastLighthouseAuditCard, } from '@backstage/plugin-lighthouse/'; +import { Router as SentryRouter } from '@backstage/plugin-sentry'; +import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs'; +import { Button, Grid } from '@material-ui/core'; +import { + isPluginApplicableToEntity as isBuildkiteAvailable, + Router as BuildkiteRouter, +} from '@roadiehq/backstage-plugin-buildkite'; +import { + isPluginApplicableToEntity as isGitHubAvailable, + LanguagesCard, + ReadMeCard, + ReleasesCard, + Router as GitHubInsightsRouter, +} from '@roadiehq/backstage-plugin-github-insights'; import { - Router as PullRequestsRouter, isPluginApplicableToEntity as isPullRequestsAvailable, PullRequestsStatsCard, + Router as PullRequestsRouter, } from '@roadiehq/backstage-plugin-github-pull-requests'; import { - Router as BuildkiteRouter, - isPluginApplicableToEntity as isBuildkiteAvailable, -} from '@roadiehq/backstage-plugin-buildkite'; + isPluginApplicableToEntity as isTravisCIAvailable, + RecentTravisCIBuildsWidget, + Router as TravisCIRouter, +} from '@roadiehq/backstage-plugin-travis-ci'; +import React, { ReactNode } from 'react'; export const CICDSwitcher = ({ entity }: { entity: Entity }) => { // This component is just an example of how you can implement your company's logic in entity page. @@ -134,7 +137,7 @@ const RecentCICDRunsSwitcher = ({ entity }: { entity: Entity }) => { ); }; -const OverviewContent = ({ entity }: { entity: Entity }) => ( +const ComponentOverviewContent = ({ entity }: { entity: Entity }) => ( @@ -169,7 +172,7 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => ( } + element={} /> ( } + element={} /> ( /> ); + const DefaultEntityPage = ({ entity }: { entity: Entity }) => ( } + element={} /> ( ); -export const EntityPage = () => { - const { entity } = useEntity(); +export const ComponentEntityPage = ({ entity }: { entity: Entity }) => { switch (entity?.spec?.type) { case 'service': return ; @@ -279,3 +282,47 @@ export const EntityPage = () => { return ; } }; + +const ApiOverviewContent = ({ entity }: { entity: Entity }) => ( + + + + + +); + +const ApiDefinitionContent = ({ entity }: { entity: ApiEntity }) => ( + + + + + +); + +const ApiEntityPage = ({ entity }: { entity: Entity }) => ( + + } + /> + } + /> + +); + +export const EntityPage = () => { + const { entity } = useEntity(); + + switch (entity?.kind?.toLowerCase()) { + case 'component': + return ; + case 'api': + return ; + default: + return ; + } +}; diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index bfefaef3a5..e8127b7732 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -29,6 +29,7 @@ "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", + "@types/react": "^16.9", "graphiql": "^1.0.0-alpha.10", "graphql": "^15.3.0", "react": "^16.13.1", @@ -47,7 +48,6 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "@types/react": "^16.9", "@types/swagger-ui-react": "^3.23.3", "cross-fetch": "^3.0.6", "msw": "^0.21.2" diff --git a/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.test.tsx b/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.test.tsx deleted file mode 100644 index 9a2bc0cfb1..0000000000 --- a/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.test.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; -import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog'; -import { wrapInTestApp } from '@backstage/test-utils'; -import { render, waitFor } from '@testing-library/react'; -import * as React from 'react'; -import { ApiEntityPage } from './ApiEntityPage'; - -jest.mock('react-router-dom', () => { - const actual = jest.requireActual('react-router-dom'); - const mockNavigate = jest.fn(); - return { - ...actual, - useNavigate: jest.fn(() => mockNavigate), - useParams: jest.fn(), - }; -}); - -const { - useParams, - useNavigate, -}: { useParams: jest.Mock; useNavigate: () => jest.Mock } = jest.requireMock( - 'react-router-dom', -); - -const errorApi = { post: () => {} }; - -describe('ApiEntityPage', () => { - it('should redirect to catalog page when name is not provided', async () => { - useParams.mockReturnValue({ - kind: 'Component', - optionalNamespaceAndName: '', - }); - - render( - wrapInTestApp( - ) as CatalogApi, - ], - ])} - > - - , - ), - ); - - await waitFor(() => - expect(useNavigate()).toHaveBeenCalledWith('/api-docs'), - ); - }); -}); diff --git a/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx b/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx deleted file mode 100644 index f755705ac7..0000000000 --- a/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ApiEntity, Entity } from '@backstage/catalog-model'; -import { - Content, - errorApiRef, - Header, - Page, - Progress, - useApi, -} from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog'; -import { Box } from '@material-ui/core'; -import { Alert } from '@material-ui/lab'; -import React, { useEffect } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { useAsync } from 'react-use'; -import { ApiDefinitionCard } from '../ApiDefinitionCard'; - -const REDIRECT_DELAY = 1000; - -function headerProps( - kind: string, - namespace: string | undefined, - name: string, - entity: Entity | undefined, -): { headerTitle: string; headerType: string } { - return { - headerTitle: `${name}${namespace ? ` in ${namespace}` : ''}`, - headerType: (() => { - let t = kind.toLowerCase(); - if (entity && entity.spec && 'type' in entity.spec) { - t += ' — '; - t += (entity.spec as { type: string }).type.toLowerCase(); - } - return t; - })(), - }; -} - -type EntityPageTitleProps = { - title: string; - entity: Entity | undefined; -}; - -const EntityPageTitle = ({ title }: EntityPageTitleProps) => ( - - {title} - -); - -export const ApiEntityPage = () => { - const { optionalNamespaceAndName } = useParams() as { - optionalNamespaceAndName: string; - }; - const navigate = useNavigate(); - const [name, namespace] = optionalNamespaceAndName.split(':').reverse(); - - const errorApi = useApi(errorApiRef); - const catalogApi = useApi(catalogApiRef); - - const { value: entity, error, loading } = useAsync( - () => catalogApi.getEntityByName({ kind: 'API', namespace, name }), - [catalogApi, namespace, name], - ); - - useEffect(() => { - if (!error && !loading && !entity) { - errorApi.post(new Error('Entity not found!')); - setTimeout(() => { - navigate('/'); - }, REDIRECT_DELAY); - } - }, [errorApi, navigate, error, loading, entity]); - - if (!name) { - navigate('/api-docs'); - return null; - } - - const { headerTitle, headerType } = headerProps( - 'API', - namespace, - name, - entity, - ); - - return ( - -
} - pageTitleOverride={headerTitle} - type={headerType} - /> - - {loading && } - - {error && ( - - {error.toString()} - - )} - - {entity && ( - <> - - - - - )} - - ); -}; diff --git a/plugins/api-docs/src/components/ApiEntityPage/index.ts b/plugins/api-docs/src/components/ApiEntityPage/index.ts deleted file mode 100644 index 561350744b..0000000000 --- a/plugins/api-docs/src/components/ApiEntityPage/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { ApiEntityPage } from './ApiEntityPage'; diff --git a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx index 9222623aed..f4a66f02f2 100644 --- a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx +++ b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx @@ -23,12 +23,12 @@ import { useApi, useQueryParamState, } from '@backstage/core'; +import { entityRoute, entityRouteParams } from '@backstage/plugin-catalog'; import { Chip, Link } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; import React from 'react'; import { generatePath, Link as RouterLink } from 'react-router-dom'; import { apiDocsConfigRef } from '../../config'; -import { entityRoute } from '../../routes'; const ApiTypeTitle = ({ apiEntity }: { apiEntity: ApiEntityV1alpha1 }) => { const config = useApi(apiDocsConfigRef); @@ -46,16 +46,10 @@ const columns: TableColumn[] = [ render: (entity: any) => ( {entity.metadata.name} diff --git a/plugins/api-docs/src/index.ts b/plugins/api-docs/src/index.ts index 958355f063..dbb32cee7b 100644 --- a/plugins/api-docs/src/index.ts +++ b/plugins/api-docs/src/index.ts @@ -14,5 +14,6 @@ * limitations under the License. */ -export { Router } from './catalog'; +export * from './catalog'; +export * from './components'; export { plugin } from './plugin'; diff --git a/plugins/api-docs/src/plugin.ts b/plugins/api-docs/src/plugin.ts index 3fcbe8a1f4..06db03f2e1 100644 --- a/plugins/api-docs/src/plugin.ts +++ b/plugins/api-docs/src/plugin.ts @@ -18,8 +18,7 @@ import { ApiEntity } from '@backstage/catalog-model'; import { createApiFactory, createPlugin } from '@backstage/core'; import { ApiExplorerPage } from './components/ApiExplorerPage/ApiExplorerPage'; import { defaultDefinitionWidgets } from './components/ApiDefinitionCard'; -import { ApiEntityPage } from './components/ApiEntityPage/ApiEntityPage'; -import { entityRoute, rootRoute } from './routes'; +import { rootRoute } from './routes'; import { apiDocsConfigRef } from './config'; export const plugin = createPlugin({ @@ -40,6 +39,5 @@ export const plugin = createPlugin({ ], register({ router }) { router.addRoute(rootRoute, ApiExplorerPage); - router.addRoute(entityRoute, ApiEntityPage); }, }); diff --git a/plugins/api-docs/src/routes.ts b/plugins/api-docs/src/routes.ts index d2d8ef10c5..6adff78e47 100644 --- a/plugins/api-docs/src/routes.ts +++ b/plugins/api-docs/src/routes.ts @@ -24,12 +24,6 @@ export const rootRoute = createRouteRef({ title: 'APIs', }); -export const entityRoute = createRouteRef({ - icon: NoIcon, - path: '/api-docs/:optionalNamespaceAndName/', - title: 'API', -}); - export const catalogRoute = createRouteRef({ icon: NoIcon, path: '', From 61152848127c77716322109734b94c5eb5196717 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 24 Nov 2020 14:11:41 +0100 Subject: [PATCH 70/94] chore: yarn.lock update --- yarn.lock | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/yarn.lock b/yarn.lock index 6bc39ab733..3679e7a60f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1307,6 +1307,19 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@backstage/catalog-model@^0.2.0": + version "0.2.0" + resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.2.0.tgz#e3fe2a4ddeb6a9b6ec480c80cb2b9c39cb245576" + integrity sha512-Y1ocdRpBlxK/VrJQjHlQd0bgADECd1B2NRjwd8ss46ibT5hwLvMOfD80+Fa7oPLu0ktJrH4lq0pNIIJIml48zA== + dependencies: + "@backstage/config" "^0.1.1" + "@types/json-schema" "^7.0.5" + "@types/yup" "^0.29.8" + json-schema "^0.2.5" + lodash "^4.17.15" + uuid "^8.0.0" + yup "^0.29.3" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" From a39968ca57900777172d2cfb56d3a83549371528 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 24 Nov 2020 14:33:39 +0100 Subject: [PATCH 71/94] chore(diff): accept diff changes --- plugins/explore/package.json | 4 ++-- plugins/gcp-projects/package.json | 4 ++-- plugins/gitops-profiles/package.json | 4 ++-- plugins/graphiql/package.json | 4 ++-- plugins/newrelic/package.json | 4 ++-- plugins/tech-radar/package.json | 4 ++-- plugins/user-settings/package.json | 4 ++-- plugins/welcome/package.json | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 153184bdb1..f5f73b4faa 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -21,7 +21,7 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.3.1", + "@backstage/core": "^0.3.2", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -33,7 +33,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 0415a5ffb3..59bdcc547c 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.3.1", + "@backstage/core": "^0.3.2", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -31,7 +31,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index ad80d26e61..67edcca51e 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -21,7 +21,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.3.1", + "@backstage/core": "^0.3.2", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -32,7 +32,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index a5523afc6f..bde5dea9a3 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.3.1", + "@backstage/core": "^0.3.2", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -43,7 +43,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 0e1f4e1352..1c2b951bc6 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -21,7 +21,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.3.1", + "@backstage/core": "^0.3.2", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -31,7 +31,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 7f5707764b..849ce3bc4d 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -21,7 +21,7 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.3.1", + "@backstage/core": "^0.3.2", "@backstage/test-utils": "^0.1.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", @@ -35,7 +35,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 1277ae214a..80bcdeffdb 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -21,7 +21,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.3.1", + "@backstage/core": "^0.3.2", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -32,7 +32,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 47b02f971c..5d81e569b8 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -21,7 +21,7 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.3.1", + "@backstage/core": "^0.3.2", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -32,7 +32,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", From 7d564164fdd07b0eab5ea851922b5a737d7117f7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 24 Nov 2020 13:34:37 +0000 Subject: [PATCH 72/94] Version Packages --- .changeset/breezy-cobras-deny.md | 5 --- .changeset/chatty-radios-beam.md | 7 ---- .changeset/clever-moons-shake.md | 5 --- .changeset/cli-links.md | 5 --- .changeset/cost-insights-quiet-bikes-occur.md | 5 --- .../cost-insights-tiny-llamas-perform.md | 5 --- .changeset/curly-yaks-invite.md | 5 --- .changeset/dull-pans-sip.md | 6 --- .changeset/empty-kids-look.md | 5 --- .changeset/forty-buses-watch.md | 14 ------- .changeset/long-flowers-wink.md | 5 --- .changeset/mighty-cycles-impress.md | 5 --- .changeset/new-ladybugs-nail.md | 5 --- .changeset/orange-cherries-run.md | 5 --- .changeset/poor-mails-marry.md | 5 --- .changeset/small-worms-check.md | 6 --- .changeset/tame-rockets-explode.md | 5 --- .changeset/tidy-brooms-look.md | 5 --- packages/app/CHANGELOG.md | 37 ++++++++++++++++++ packages/app/package.json | 38 +++++++++---------- packages/backend-common/CHANGELOG.md | 7 ++++ packages/backend-common/package.json | 4 +- packages/backend/CHANGELOG.md | 21 ++++++++++ packages/backend/package.json | 20 +++++----- packages/catalog-client/CHANGELOG.md | 8 ++++ packages/catalog-client/package.json | 6 +-- packages/catalog-model/CHANGELOG.md | 18 +++++++++ packages/catalog-model/package.json | 4 +- packages/cli/CHANGELOG.md | 9 +++++ packages/cli/package.json | 6 +-- packages/core/CHANGELOG.md | 6 +++ packages/core/package.json | 4 +- packages/create-app/package.json | 32 ++++++++-------- plugins/api-docs/CHANGELOG.md | 11 ++++++ plugins/api-docs/package.json | 10 ++--- plugins/auth-backend/CHANGELOG.md | 12 ++++++ plugins/auth-backend/package.json | 10 ++--- plugins/catalog-backend/CHANGELOG.md | 22 +++++++++++ plugins/catalog-backend/package.json | 8 ++-- plugins/catalog-graphql/CHANGELOG.md | 11 ++++++ plugins/catalog-graphql/package.json | 8 ++-- plugins/catalog/CHANGELOG.md | 14 +++++++ plugins/catalog/package.json | 14 +++---- plugins/circleci/CHANGELOG.md | 12 ++++++ plugins/circleci/package.json | 10 ++--- plugins/cloudbuild/CHANGELOG.md | 11 ++++++ plugins/cloudbuild/package.json | 10 ++--- plugins/cost-insights/CHANGELOG.md | 9 +++++ plugins/cost-insights/package.json | 6 +-- plugins/github-actions/CHANGELOG.md | 11 ++++++ plugins/github-actions/package.json | 10 ++--- plugins/jenkins/CHANGELOG.md | 11 ++++++ plugins/jenkins/package.json | 10 ++--- plugins/kubernetes-backend/CHANGELOG.md | 15 ++++++++ plugins/kubernetes-backend/package.json | 8 ++-- plugins/kubernetes/CHANGELOG.md | 15 ++++++++ plugins/kubernetes/package.json | 10 ++--- plugins/lighthouse/CHANGELOG.md | 11 ++++++ plugins/lighthouse/package.json | 10 ++--- plugins/register-component/CHANGELOG.md | 12 ++++++ plugins/register-component/package.json | 10 ++--- plugins/rollbar/CHANGELOG.md | 11 ++++++ plugins/rollbar/package.json | 10 ++--- plugins/scaffolder-backend/CHANGELOG.md | 13 +++++++ plugins/scaffolder-backend/package.json | 8 ++-- plugins/scaffolder/CHANGELOG.md | 12 ++++++ plugins/scaffolder/package.json | 10 ++--- plugins/search/CHANGELOG.md | 13 +++++++ plugins/search/package.json | 10 ++--- plugins/sentry/CHANGELOG.md | 10 +++++ plugins/sentry/package.json | 8 ++-- plugins/sonarqube/CHANGELOG.md | 11 ++++++ plugins/sonarqube/package.json | 8 ++-- plugins/techdocs-backend/CHANGELOG.md | 11 ++++++ plugins/techdocs-backend/package.json | 8 ++-- plugins/techdocs/CHANGELOG.md | 11 ++++++ plugins/techdocs/package.json | 10 ++--- 77 files changed, 535 insertions(+), 263 deletions(-) delete mode 100644 .changeset/breezy-cobras-deny.md delete mode 100644 .changeset/chatty-radios-beam.md delete mode 100644 .changeset/clever-moons-shake.md delete mode 100644 .changeset/cli-links.md delete mode 100644 .changeset/cost-insights-quiet-bikes-occur.md delete mode 100644 .changeset/cost-insights-tiny-llamas-perform.md delete mode 100644 .changeset/curly-yaks-invite.md delete mode 100644 .changeset/dull-pans-sip.md delete mode 100644 .changeset/empty-kids-look.md delete mode 100644 .changeset/forty-buses-watch.md delete mode 100644 .changeset/long-flowers-wink.md delete mode 100644 .changeset/mighty-cycles-impress.md delete mode 100644 .changeset/new-ladybugs-nail.md delete mode 100644 .changeset/orange-cherries-run.md delete mode 100644 .changeset/poor-mails-marry.md delete mode 100644 .changeset/small-worms-check.md delete mode 100644 .changeset/tame-rockets-explode.md delete mode 100644 .changeset/tidy-brooms-look.md create mode 100644 plugins/search/CHANGELOG.md diff --git a/.changeset/breezy-cobras-deny.md b/.changeset/breezy-cobras-deny.md deleted file mode 100644 index 5f035c05ce..0000000000 --- a/.changeset/breezy-cobras-deny.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core': patch ---- - -Clear sidebar search field once a search is executed diff --git a/.changeset/chatty-radios-beam.md b/.changeset/chatty-radios-beam.md deleted file mode 100644 index 91ad76ee88..0000000000 --- a/.changeset/chatty-radios-beam.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/catalog-model': minor -'@backstage/plugin-kubernetes': minor -'@backstage/plugin-kubernetes-backend': minor ---- - -add kubernetes selector to component model diff --git a/.changeset/clever-moons-shake.md b/.changeset/clever-moons-shake.md deleted file mode 100644 index 0fb6c690c2..0000000000 --- a/.changeset/clever-moons-shake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Added readTree support to AzureUrlReader diff --git a/.changeset/cli-links.md b/.changeset/cli-links.md deleted file mode 100644 index 0a9779de93..0000000000 --- a/.changeset/cli-links.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -The CLI now detects and transforms linked packages. You can link in external packages by adding them to both the `lerna.json` and `package.json` workspace paths. diff --git a/.changeset/cost-insights-quiet-bikes-occur.md b/.changeset/cost-insights-quiet-bikes-occur.md deleted file mode 100644 index aac283bbd4..0000000000 --- a/.changeset/cost-insights-quiet-bikes-occur.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-cost-insights': patch ---- - -fix product icon configuration diff --git a/.changeset/cost-insights-tiny-llamas-perform.md b/.changeset/cost-insights-tiny-llamas-perform.md deleted file mode 100644 index c81c28f8f8..0000000000 --- a/.changeset/cost-insights-tiny-llamas-perform.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-cost-insights': patch ---- - -truncate large percentages > 1000% diff --git a/.changeset/curly-yaks-invite.md b/.changeset/curly-yaks-invite.md deleted file mode 100644 index b9191a3895..0000000000 --- a/.changeset/curly-yaks-invite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Add support for reading groups and users from the Microsoft Graph API. diff --git a/.changeset/dull-pans-sip.md b/.changeset/dull-pans-sip.md deleted file mode 100644 index 13616c1f8d..0000000000 --- a/.changeset/dull-pans-sip.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-scaffolder-backend': patch ---- - -Move constructing the catalog-info.yaml URL for scaffolded components to the publishers diff --git a/.changeset/empty-kids-look.md b/.changeset/empty-kids-look.md deleted file mode 100644 index 680cd7f25c..0000000000 --- a/.changeset/empty-kids-look.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-register-component': patch ---- - -Remove catalog link on validate popup diff --git a/.changeset/forty-buses-watch.md b/.changeset/forty-buses-watch.md deleted file mode 100644 index 4074566fe7..0000000000 --- a/.changeset/forty-buses-watch.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/catalog-model': patch -'@backstage/plugin-catalog-backend': patch ---- - -Marked the `Group` entity fields `ancestors` and `descendants` for deprecation on Dec 6th, 2020. See https://github.com/backstage/backstage/issues/3049 for details. - -Code that consumes these fields should remove those usages as soon as possible. There is no current or planned replacement for these fields. - -The BuiltinKindsEntityProcessor has been updated to inject these fields as empty arrays if they are missing. Therefore, if you are on a catalog instance that uses the updated version of this code, you can start removing the fields from your source catalog-info.yaml data as well, without breaking validation. - -After Dec 6th, the fields will be removed from types and classes of the Backstage repository. At the first release after that, they will not be present in released packages either. - -If your catalog-info.yaml files still contain these fields after the deletion, they will still be valid and your ingestion will not break, but they won't be visible in the types for consuming code. diff --git a/.changeset/long-flowers-wink.md b/.changeset/long-flowers-wink.md deleted file mode 100644 index 7f9cf12315..0000000000 --- a/.changeset/long-flowers-wink.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-sonarqube': patch ---- - -Add configuration schema diff --git a/.changeset/mighty-cycles-impress.md b/.changeset/mighty-cycles-impress.md deleted file mode 100644 index 56ec35098f..0000000000 --- a/.changeset/mighty-cycles-impress.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-circleci': patch ---- - -Improved CircleCI builds table to show more information and relevant links diff --git a/.changeset/new-ladybugs-nail.md b/.changeset/new-ladybugs-nail.md deleted file mode 100644 index 5351b6dff3..0000000000 --- a/.changeset/new-ladybugs-nail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -New lint rule to disallow assertions and promote `as` assertions. - @typescript-eslint/consistent-type-assertions diff --git a/.changeset/orange-cherries-run.md b/.changeset/orange-cherries-run.md deleted file mode 100644 index 3c06c27b47..0000000000 --- a/.changeset/orange-cherries-run.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Add experimental backend:bundle command diff --git a/.changeset/poor-mails-marry.md b/.changeset/poor-mails-marry.md deleted file mode 100644 index fdc7e6f1b8..0000000000 --- a/.changeset/poor-mails-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Add new `versions:check` and `versions:bump` commands to simplify version management and avoid conflicts diff --git a/.changeset/small-worms-check.md b/.changeset/small-worms-check.md deleted file mode 100644 index e39265f8f9..0000000000 --- a/.changeset/small-worms-check.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'example-app': patch -'@backstage/plugin-search': patch ---- - -Using the search field in the sidebar now navigates to the search result page. diff --git a/.changeset/tame-rockets-explode.md b/.changeset/tame-rockets-explode.md deleted file mode 100644 index 123472701e..0000000000 --- a/.changeset/tame-rockets-explode.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Make integration host and url configurations visible in the frontend diff --git a/.changeset/tidy-brooms-look.md b/.changeset/tidy-brooms-look.md deleted file mode 100644 index f2c2a9064e..0000000000 --- a/.changeset/tidy-brooms-look.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Fix React entity YAML filename to new standard diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index ab00924a26..f70cd198b4 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,42 @@ # example-app +## 0.2.3 + +### Patch Changes + +- 475fc0aaa: Using the search field in the sidebar now navigates to the search result page. +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [29a0ccab2] +- Updated dependencies [8e6728e25] +- Updated dependencies [c93a14b49] +- Updated dependencies [ef2831dde] +- Updated dependencies [2a71f4bab] +- Updated dependencies [1185919f3] +- Updated dependencies [a8de7f554] +- Updated dependencies [faf311c26] +- Updated dependencies [31d8b6979] +- Updated dependencies [991345969] +- Updated dependencies [475fc0aaa] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-kubernetes@0.3.0 + - @backstage/cli@0.3.1 + - @backstage/plugin-cost-insights@0.4.1 + - @backstage/plugin-scaffolder@0.3.1 + - @backstage/plugin-register-component@0.2.2 + - @backstage/plugin-circleci@0.2.2 + - @backstage/plugin-search@0.2.1 + - @backstage/plugin-api-docs@0.2.2 + - @backstage/plugin-catalog@0.2.3 + - @backstage/plugin-cloudbuild@0.2.2 + - @backstage/plugin-github-actions@0.2.2 + - @backstage/plugin-jenkins@0.3.1 + - @backstage/plugin-lighthouse@0.2.3 + - @backstage/plugin-rollbar@0.2.3 + - @backstage/plugin-sentry@0.2.3 + - @backstage/plugin-techdocs@0.2.3 + ## 0.2.2 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 042fd47188..9e54005cd3 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,33 +1,33 @@ { "name": "example-app", - "version": "0.2.2", + "version": "0.2.3", "private": true, "bundled": true, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/cli": "^0.3.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-api-docs": "^0.2.1", - "@backstage/plugin-catalog": "^0.2.2", - "@backstage/plugin-circleci": "^0.2.1", - "@backstage/plugin-cloudbuild": "^0.2.1", - "@backstage/plugin-cost-insights": "^0.4.0", + "@backstage/catalog-model": "^0.3.0", + "@backstage/cli": "^0.3.1", + "@backstage/core": "^0.3.2", + "@backstage/plugin-api-docs": "^0.2.2", + "@backstage/plugin-catalog": "^0.2.3", + "@backstage/plugin-circleci": "^0.2.2", + "@backstage/plugin-cloudbuild": "^0.2.2", + "@backstage/plugin-cost-insights": "^0.4.1", "@backstage/plugin-explore": "^0.2.1", "@backstage/plugin-gcp-projects": "^0.2.1", - "@backstage/plugin-github-actions": "^0.2.1", + "@backstage/plugin-github-actions": "^0.2.2", "@backstage/plugin-gitops-profiles": "^0.2.1", "@backstage/plugin-graphiql": "^0.2.1", - "@backstage/plugin-jenkins": "^0.3.0", - "@backstage/plugin-kubernetes": "^0.2.1", - "@backstage/plugin-lighthouse": "^0.2.2", + "@backstage/plugin-jenkins": "^0.3.1", + "@backstage/plugin-kubernetes": "^0.3.0", + "@backstage/plugin-lighthouse": "^0.2.3", "@backstage/plugin-newrelic": "^0.2.1", - "@backstage/plugin-register-component": "^0.2.1", - "@backstage/plugin-rollbar": "^0.2.2", - "@backstage/plugin-scaffolder": "^0.3.0", - "@backstage/plugin-sentry": "^0.2.2", - "@backstage/plugin-search": "^0.2.0", + "@backstage/plugin-register-component": "^0.2.2", + "@backstage/plugin-rollbar": "^0.2.3", + "@backstage/plugin-scaffolder": "^0.3.1", + "@backstage/plugin-sentry": "^0.2.3", + "@backstage/plugin-search": "^0.2.1", "@backstage/plugin-tech-radar": "^0.3.0", - "@backstage/plugin-techdocs": "^0.2.2", + "@backstage/plugin-techdocs": "^0.2.3", "@backstage/plugin-user-settings": "^0.2.2", "@backstage/plugin-welcome": "^0.2.1", "@backstage/test-utils": "^0.1.3", diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 4822032c7f..793a554905 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/backend-common +## 0.3.1 + +### Patch Changes + +- bff3305aa: Added readTree support to AzureUrlReader +- b47dce06f: Make integration host and url configurations visible in the frontend + ## 0.3.0 ### Minor Changes diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index b22e4638f5..731e2679cb 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -67,7 +67,7 @@ } }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/test-utils": "^0.1.3", "@types/archiver": "^3.1.1", "@types/compression": "^1.7.0", diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index a8bc1fef7d..bc2a08e983 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,26 @@ # example-backend +## 0.2.3 + +### Patch Changes + +- Updated dependencies [1166fcc36] +- Updated dependencies [bff3305aa] +- Updated dependencies [0c2121240] +- Updated dependencies [ef2831dde] +- Updated dependencies [1185919f3] +- Updated dependencies [475fc0aaa] +- Updated dependencies [b47dce06f] +- Updated dependencies [5a1d8dca3] + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-kubernetes-backend@0.2.0 + - @backstage/backend-common@0.3.1 + - @backstage/plugin-catalog-backend@0.2.2 + - @backstage/plugin-scaffolder-backend@0.3.2 + - example-app@0.2.3 + - @backstage/plugin-auth-backend@0.2.3 + - @backstage/plugin-techdocs-backend@0.2.2 + ## 0.2.2 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 014a3f78d5..5d3678a284 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.2", + "version": "0.2.3", "main": "dist/index.cjs.js", "types": "src/index.ts", "private": true, @@ -18,24 +18,24 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "@backstage/plugin-app-backend": "^0.3.0", - "@backstage/plugin-auth-backend": "^0.2.2", - "@backstage/plugin-catalog-backend": "^0.2.1", + "@backstage/plugin-auth-backend": "^0.2.3", + "@backstage/plugin-catalog-backend": "^0.2.2", "@backstage/plugin-graphql-backend": "^0.1.3", - "@backstage/plugin-kubernetes-backend": "^0.1.3", + "@backstage/plugin-kubernetes-backend": "^0.2.0", "@backstage/plugin-proxy-backend": "^0.2.1", "@backstage/plugin-rollbar-backend": "^0.1.3", - "@backstage/plugin-scaffolder-backend": "^0.3.1", + "@backstage/plugin-scaffolder-backend": "^0.3.2", "@backstage/plugin-sentry-backend": "^0.1.3", - "@backstage/plugin-techdocs-backend": "^0.2.1", + "@backstage/plugin-techdocs-backend": "^0.2.2", "@gitbeaker/node": "^25.2.0", "@octokit/rest": "^18.0.0", "azure-devops-node-api": "^10.1.1", "dockerode": "^3.2.0", - "example-app": "^0.2.2", + "example-app": "^0.2.3", "express": "^4.17.1", "express-promise-router": "^3.0.3", "knex": "^0.21.6", @@ -45,7 +45,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@types/dockerode": "^2.5.32", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5", diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index 574cd301bf..3454a6c8e0 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/catalog-client +## 0.3.1 + +### Patch Changes + +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/catalog-model@0.3.0 + ## 0.3.0 ### Minor Changes diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index 2482f9579d..4509b7d547 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-client", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,12 +20,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "cross-fetch": "^3.0.6" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@types/jest": "^26.0.7", "msw": "^0.21.2" }, diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index 051aa82c80..10bf540b46 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/catalog-model +## 0.3.0 + +### Minor Changes + +- 1166fcc36: add kubernetes selector to component model + +### Patch Changes + +- 1185919f3: Marked the `Group` entity fields `ancestors` and `descendants` for deprecation on Dec 6th, 2020. See https://github.com/backstage/backstage/issues/3049 for details. + + Code that consumes these fields should remove those usages as soon as possible. There is no current or planned replacement for these fields. + + The BuiltinKindsEntityProcessor has been updated to inject these fields as empty arrays if they are missing. Therefore, if you are on a catalog instance that uses the updated version of this code, you can start removing the fields from your source catalog-info.yaml data as well, without breaking validation. + + After Dec 6th, the fields will be removed from types and classes of the Backstage repository. At the first release after that, they will not be present in released packages either. + + If your catalog-info.yaml files still contain these fields after the deletion, they will still be valid and your ingestion will not break, but they won't be visible in the types for consuming code. + ## 0.2.0 ### Minor Changes diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index e712e66f58..7956454ca9 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-model", - "version": "0.2.0", + "version": "0.3.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,7 +29,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@types/express": "^4.17.6", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index a5bcc6bcfd..d198444c86 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/cli +## 0.3.1 + +### Patch Changes + +- 29a0ccab2: The CLI now detects and transforms linked packages. You can link in external packages by adding them to both the `lerna.json` and `package.json` workspace paths. +- faf311c26: New lint rule to disallow assertions and promote `as` assertions. - @typescript-eslint/consistent-type-assertions +- 31d8b6979: Add experimental backend:bundle command +- 991345969: Add new `versions:check` and `versions:bump` commands to simplify version management and avoid conflicts + ## 0.3.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index eb56b19971..034c8a3de5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.3.0", + "version": "0.3.1", "private": false, "publishConfig": { "access": "public" @@ -111,9 +111,9 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.3.0", + "@backstage/backend-common": "^0.3.1", "@backstage/config": "^0.1.1", - "@backstage/core": "^0.3.1", + "@backstage/core": "^0.3.2", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@backstage/theme": "^0.2.1", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 213e2b5246..d29bc53452 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/core +## 0.3.2 + +### Patch Changes + +- 475fc0aaa: Clear sidebar search field once a search is executed + ## 0.3.1 ### Patch Changes diff --git a/packages/core/package.json b/packages/core/package.json index d65f0a16e5..f667ffe7be 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core", "description": "Core API used by Backstage plugins and apps", - "version": "0.3.1", + "version": "0.3.2", "private": false, "publishConfig": { "access": "public", @@ -64,7 +64,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/packages/create-app/package.json b/packages/create-app/package.json index c01d4d3dbb..6e91a916a1 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -37,28 +37,28 @@ "recursive-readdir": "^2.2.2" }, "devDependencies": { - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", - "@backstage/cli": "^0.3.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/config": "^0.1.1", - "@backstage/core": "^0.3.1", - "@backstage/plugin-api-docs": "^0.2.1", + "@backstage/core": "^0.3.2", + "@backstage/plugin-api-docs": "^0.2.2", "@backstage/plugin-app-backend": "^0.3.0", - "@backstage/plugin-auth-backend": "^0.2.2", - "@backstage/plugin-catalog": "^0.2.2", - "@backstage/plugin-catalog-backend": "^0.2.1", - "@backstage/plugin-circleci": "^0.2.1", + "@backstage/plugin-auth-backend": "^0.2.3", + "@backstage/plugin-catalog": "^0.2.3", + "@backstage/plugin-catalog-backend": "^0.2.2", + "@backstage/plugin-circleci": "^0.2.2", "@backstage/plugin-explore": "^0.2.1", - "@backstage/plugin-github-actions": "^0.2.1", - "@backstage/plugin-lighthouse": "^0.2.2", + "@backstage/plugin-github-actions": "^0.2.2", + "@backstage/plugin-lighthouse": "^0.2.3", "@backstage/plugin-proxy-backend": "^0.2.1", - "@backstage/plugin-register-component": "^0.2.1", + "@backstage/plugin-register-component": "^0.2.2", "@backstage/plugin-rollbar-backend": "^0.1.3", - "@backstage/plugin-scaffolder": "^0.3.0", - "@backstage/plugin-scaffolder-backend": "^0.3.1", + "@backstage/plugin-scaffolder": "^0.3.1", + "@backstage/plugin-scaffolder-backend": "^0.3.2", "@backstage/plugin-tech-radar": "^0.3.0", - "@backstage/plugin-techdocs": "^0.2.2", - "@backstage/plugin-techdocs-backend": "^0.2.1", + "@backstage/plugin-techdocs": "^0.2.3", + "@backstage/plugin-techdocs-backend": "^0.2.2", "@backstage/plugin-user-settings": "^0.2.2", "@backstage/test-utils": "^0.1.3", "@backstage/theme": "^0.2.1", diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 5f1ad97e64..60a5caf55b 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-api-docs +## 0.2.2 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.1 ### Patch Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index bfefaef3a5..691f2680b2 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@kyma-project/asyncapi-react": "^0.14.2", "@material-icons/font": "^1.0.2", @@ -39,7 +39,7 @@ "swagger-ui-react": "^3.31.1" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 546855c92f..4a2161ad71 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-auth-backend +## 0.2.3 + +### Patch Changes + +- Updated dependencies [1166fcc36] +- Updated dependencies [bff3305aa] +- Updated dependencies [1185919f3] +- Updated dependencies [b47dce06f] + - @backstage/catalog-model@0.3.0 + - @backstage/backend-common@0.3.1 + - @backstage/catalog-client@0.3.1 + ## 0.2.2 ### Patch Changes diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 4d1e52aa21..43e2c59f88 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-client": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-client": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "@types/express": "^4.17.6", "compression": "^1.7.4", @@ -55,7 +55,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 1a3199ee14..3bf25a494b 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-catalog-backend +## 0.2.2 + +### Patch Changes + +- 0c2121240: Add support for reading groups and users from the Microsoft Graph API. +- 1185919f3: Marked the `Group` entity fields `ancestors` and `descendants` for deprecation on Dec 6th, 2020. See https://github.com/backstage/backstage/issues/3049 for details. + + Code that consumes these fields should remove those usages as soon as possible. There is no current or planned replacement for these fields. + + The BuiltinKindsEntityProcessor has been updated to inject these fields as empty arrays if they are missing. Therefore, if you are on a catalog instance that uses the updated version of this code, you can start removing the fields from your source catalog-info.yaml data as well, without breaking validation. + + After Dec 6th, the fields will be removed from types and classes of the Backstage repository. At the first release after that, they will not be present in released packages either. + + If your catalog-info.yaml files still contain these fields after the deletion, they will still be valid and your ingestion will not break, but they won't be visible in the types for consuming code. + +- Updated dependencies [1166fcc36] +- Updated dependencies [bff3305aa] +- Updated dependencies [1185919f3] +- Updated dependencies [b47dce06f] + - @backstage/catalog-model@0.3.0 + - @backstage/backend-common@0.3.1 + ## 0.2.1 ### Patch Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 12a17d6365..51a9978f30 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ }, "dependencies": { "@azure/msal-node": "^1.0.0-alpha.8", - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "@octokit/graphql": "^4.5.6", "@types/express": "^4.17.6", @@ -47,7 +47,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/test-utils": "^0.1.3", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/catalog-graphql/CHANGELOG.md b/plugins/catalog-graphql/CHANGELOG.md index 35737b7dd1..56911b9c45 100644 --- a/plugins/catalog-graphql/CHANGELOG.md +++ b/plugins/catalog-graphql/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-graphql +## 0.2.2 + +### Patch Changes + +- Updated dependencies [1166fcc36] +- Updated dependencies [bff3305aa] +- Updated dependencies [1185919f3] +- Updated dependencies [b47dce06f] + - @backstage/catalog-model@0.3.0 + - @backstage/backend-common@0.3.1 + ## 0.2.1 ### Patch Changes diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index c55be89b62..a489d66e5b 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graphql", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "@graphql-modules/core": "^0.7.17", "apollo-server": "^2.16.1", @@ -32,7 +32,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/test-utils": "^0.1.3", "@graphql-codegen/cli": "^1.17.7", "@graphql-codegen/typescript": "^1.17.7", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 1b8b884f1c..9137b7e948 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog +## 0.2.3 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [ef2831dde] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-scaffolder@0.3.1 + - @backstage/catalog-client@0.3.1 + - @backstage/plugin-techdocs@0.2.3 + ## 0.2.2 ### Patch Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 2c65096fd6..7b58358734 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,11 +21,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-client": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-scaffolder": "^0.3.0", - "@backstage/plugin-techdocs": "^0.2.2", + "@backstage/catalog-client": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-scaffolder": "^0.3.1", + "@backstage/plugin-techdocs": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -43,7 +43,7 @@ "swr": "^0.3.0" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@microsoft/microsoft-graph-types": "^1.25.0", diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md index 2894ce1bd3..a81531e28d 100644 --- a/plugins/circleci/CHANGELOG.md +++ b/plugins/circleci/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-circleci +## 0.2.2 + +### Patch Changes + +- a8de7f554: Improved CircleCI builds table to show more information and relevant links +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.1 ### Patch Changes diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index eaa8c319a0..fd32ff9891 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-circleci", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "postpack": "backstage-cli postpack" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -40,7 +40,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md index 9d02fd1ea3..4cb2d01377 100644 --- a/plugins/cloudbuild/CHANGELOG.md +++ b/plugins/cloudbuild/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-cloudbuild +## 0.2.2 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.1 ### Patch Changes diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index c0b36ef1e7..3e7f0c9175 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cloudbuild", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -39,7 +39,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index 157d92f251..b7bdfa9d50 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-cost-insights +## 0.4.1 + +### Patch Changes + +- 8e6728e25: fix product icon configuration +- c93a14b49: truncate large percentages > 1000% +- Updated dependencies [475fc0aaa] + - @backstage/core@0.3.2 + ## 0.4.0 ### Minor Changes diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 8ebeac91c0..06083b855b 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cost-insights", - "version": "0.4.0", + "version": "0.4.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,7 +22,7 @@ }, "dependencies": { "@backstage/config": "^0.1.1", - "@backstage/core": "^0.3.1", + "@backstage/core": "^0.3.2", "@backstage/test-utils": "^0.1.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", @@ -46,7 +46,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index b679d6844b..96f3876802 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-github-actions +## 0.2.2 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.1 ### Patch Changes diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 49ecfc5423..b1924034f1 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-actions", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", "@backstage/core-api": "^0.2.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -40,7 +40,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index 44febbc63d..aeeb069f19 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-jenkins +## 0.3.1 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.3.0 ### Minor Changes diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 0279592a75..961f551dfa 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -36,7 +36,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 7c55a7407d..2877264e54 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-kubernetes-backend +## 0.2.0 + +### Minor Changes + +- 1166fcc36: add kubernetes selector to component model + +### Patch Changes + +- Updated dependencies [1166fcc36] +- Updated dependencies [bff3305aa] +- Updated dependencies [1185919f3] +- Updated dependencies [b47dce06f] + - @backstage/catalog-model@0.3.0 + - @backstage/backend-common@0.3.1 + ## 0.1.3 ### Patch Changes diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index be7b86dd52..e607e51f80 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-backend", - "version": "0.1.3", + "version": "0.2.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "@kubernetes/client-node": "^0.12.1", "@types/express": "^4.17.6", @@ -38,7 +38,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index e5611e4953..652fc394a2 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-kubernetes +## 0.3.0 + +### Minor Changes + +- 1166fcc36: add kubernetes selector to component model + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-kubernetes-backend@0.2.0 + ## 0.2.1 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 36954cc5b9..2afff2ae3f 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.2.1", + "version": "0.3.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,10 +20,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", - "@backstage/core": "^0.3.1", - "@backstage/plugin-kubernetes-backend": "^0.1.3", + "@backstage/core": "^0.3.2", + "@backstage/plugin-kubernetes-backend": "^0.2.0", "@backstage/theme": "^0.2.1", "@kubernetes/client-node": "^0.12.1", "@material-ui/core": "^4.11.0", @@ -35,7 +35,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md index 666e7c4411..e35f3dece0 100644 --- a/plugins/lighthouse/CHANGELOG.md +++ b/plugins/lighthouse/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-lighthouse +## 0.2.3 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.2 ### Patch Changes diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 90230f5248..ad1264cfbb 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-lighthouse", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,11 +21,11 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", - "@backstage/core": "^0.3.1", + "@backstage/core": "^0.3.2", "@backstage/core-api": "^0.2.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -38,7 +38,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/register-component/CHANGELOG.md b/plugins/register-component/CHANGELOG.md index 3e966ef0d0..c569046855 100644 --- a/plugins/register-component/CHANGELOG.md +++ b/plugins/register-component/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-register-component +## 0.2.2 + +### Patch Changes + +- 2a71f4bab: Remove catalog link on validate popup +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.1 ### Patch Changes diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index 06ae17cb0e..1564faae0e 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-register-component", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -36,7 +36,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/rollbar/CHANGELOG.md b/plugins/rollbar/CHANGELOG.md index e4a6671f90..1ccfe16c41 100644 --- a/plugins/rollbar/CHANGELOG.md +++ b/plugins/rollbar/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-rollbar +## 0.2.3 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.2 ### Patch Changes diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index c107573ca8..1177a83eb6 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-rollbar", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -37,7 +37,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index a388a29a00..ea7e5c85ee 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-scaffolder-backend +## 0.3.2 + +### Patch Changes + +- ef2831dde: Move constructing the catalog-info.yaml URL for scaffolded components to the publishers +- 5a1d8dca3: Fix React entity YAML filename to new standard +- Updated dependencies [1166fcc36] +- Updated dependencies [bff3305aa] +- Updated dependencies [1185919f3] +- Updated dependencies [b47dce06f] + - @backstage/catalog-model@0.3.0 + - @backstage/backend-common@0.3.1 + ## 0.3.1 ### Patch Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 73c07d158f..a095167a08 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "0.3.1", + "version": "0.3.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "@gitbeaker/core": "^25.2.0", "@gitbeaker/node": "^25.2.0", @@ -48,7 +48,7 @@ "cross-fetch": "^3.0.6" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@octokit/types": "^5.4.1", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 9eac595997..c78e05c3e0 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder +## 0.3.1 + +### Patch Changes + +- ef2831dde: Move constructing the catalog-info.yaml URL for scaffolded components to the publishers +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.3.0 ### Minor Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 439fc0ad9a..791221bc62 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -41,7 +41,7 @@ "swr": "^0.3.0" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md new file mode 100644 index 0000000000..3545914e96 --- /dev/null +++ b/plugins/search/CHANGELOG.md @@ -0,0 +1,13 @@ +# @backstage/plugin-search + +## 0.2.1 + +### Patch Changes + +- 475fc0aaa: Using the search field in the sidebar now navigates to the search result page. +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 diff --git a/plugins/search/package.json b/plugins/search/package.json index df5c6c25b9..021e3f71f3 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "0.2.0", + "version": "0.2.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", - "@backstage/catalog-model": "^0.2.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", + "@backstage/catalog-model": "^0.3.0", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -34,7 +34,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index 92327c1668..b388548d8e 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-sentry +## 0.2.3 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + ## 0.2.2 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 80a37c574e..4c10936148 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -36,7 +36,7 @@ "timeago.js": "^4.0.2" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/sonarqube/CHANGELOG.md b/plugins/sonarqube/CHANGELOG.md index 0296bd0416..9ebd73b812 100644 --- a/plugins/sonarqube/CHANGELOG.md +++ b/plugins/sonarqube/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-sonarqube +## 0.1.4 + +### Patch Changes + +- 26484d413: Add configuration schema +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + ## 0.1.3 ### Patch Changes diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index be8a878e50..6a9d0fe0d4 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sonarqube", - "version": "0.1.3", + "version": "0.1.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -35,7 +35,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 4a3fbf6821..d9054230e2 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-techdocs-backend +## 0.2.2 + +### Patch Changes + +- Updated dependencies [1166fcc36] +- Updated dependencies [bff3305aa] +- Updated dependencies [1185919f3] +- Updated dependencies [b47dce06f] + - @backstage/catalog-model@0.3.0 + - @backstage/backend-common@0.3.1 + ## 0.2.1 ### Patch Changes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index b139bb9acf..1d9f3ea7c1 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "@types/dockerode": "^2.5.34", "@types/express": "^4.17.6", @@ -37,7 +37,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index bd3ae21ab8..3f46e1358b 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-techdocs +## 0.2.3 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.2 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 35b196e124..23d7a25b57 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", "@backstage/core-api": "^0.2.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/test-utils": "^0.1.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", @@ -39,7 +39,7 @@ "sanitize-html": "^1.27.0" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", From 451edeee30198975e7be519dafbe38a6db1512fc Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 24 Nov 2020 14:59:04 +0100 Subject: [PATCH 73/94] chore: yarn lock fixes and resolution for catalog-model --- package.json | 3 ++- yarn.lock | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 4140886bc3..3be94c19cf 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ ] }, "resolutions": { - "**/@roadiehq/backstage-plugin-*/@backstage/core": "*" + "**/@roadiehq/**/@backstage/core": "*", + "**/@roadiehq/**/@backstage/catalog-model": "*" }, "version": "1.0.0", "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 3679e7a60f..967fbb016d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1308,9 +1308,7 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.2.0": - version "0.2.0" - resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.2.0.tgz#e3fe2a4ddeb6a9b6ec480c80cb2b9c39cb245576" - integrity sha512-Y1ocdRpBlxK/VrJQjHlQd0bgADECd1B2NRjwd8ss46ibT5hwLvMOfD80+Fa7oPLu0ktJrH4lq0pNIIJIml48zA== + version "0.3.0" dependencies: "@backstage/config" "^0.1.1" "@types/json-schema" "^7.0.5" From b0f286b83cee6dc513b56e0f069249874217988b Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 24 Nov 2020 16:02:44 +0100 Subject: [PATCH 74/94] Document Substitutions In The Descriptor Format --- .../software-catalog/descriptor-format.md | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 60f1901807..ff2f92beab 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -93,6 +93,41 @@ significance and have reserved purposes and distinct shapes. See below for details about these fields. +## Substitutions In The Descriptor Format + +The descriptor format supports substitutions using `$text`, `$json`, and +`$yaml`. + +Placeholders like `$json: https://example.com/entity.json` are substituded by +the content of the referenced file. Files can be referenced from any configured +integration similar to locations by passing an absolute URL. It's also possible +to reference relative files like `./referenced.yaml` from the same location. +There are three different types of placeholders: + +- `$text`: Interprets the content of the referenced file as plain text and embed + it as a string. +- `$json`: Interprets the content of the referenced file as JSON and embed the + parsed structure. +- `$yaml`: Interprets the content of the referenced file as YAML and embed the + parsed structure. + +For example, this can be used to load the definition of an API entity from a web +server and embed it into the entity: + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: petstore + description: The Petstore API +spec: + type: openapi + lifecycle: production + owner: petstore@example.com + definition: + $text: https://petstore.swagger.io/v2/swagger.json +``` + ## Common to All Kinds: The Envelope The root envelope object has the following structure. From 6579b08725d97309d3528f9d4159d120007ef0de Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 24 Nov 2020 16:06:30 +0100 Subject: [PATCH 75/94] Link to substitutions instead of duplicating the content --- .../software-catalog/descriptor-format.md | 2 +- .../well-known-annotations.md | 19 +++---------------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index ff2f92beab..7171e0c7b8 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -98,7 +98,7 @@ See below for details about these fields. The descriptor format supports substitutions using `$text`, `$json`, and `$yaml`. -Placeholders like `$json: https://example.com/entity.json` are substituded by +Placeholders like `$json: https://example.com/entity.json` are substituted by the content of the referenced file. Files can be referenced from any configured integration similar to locations by passing an absolute URL. It's also possible to reference relative files like `./referenced.yaml` from the same location. diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index 7d16427a3c..3627c74b9a 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -238,22 +238,9 @@ annotation, with the same value format. ### backstage.io/definition-at-location -This annotation allowed to load the API definition from another location. Now -placeholders can be used instead: - -``` -apiVersion: backstage.io/v1alpha1 -kind: API -metadata: - name: petstore - description: The Petstore API -spec: - type: openapi - lifecycle: production - owner: petstore@example.com - definition: - $text: https://petstore.swagger.io/v2/swagger.json -``` +This annotation allowed to load the API definition from another location. Use +[substitution](./descriptor-format.md#substitutions-in-the-descriptor-format) +instead. ## Links From 11e4a9cbe87677752c21cb37e5b9c587c21628d5 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 24 Nov 2020 16:23:41 +0100 Subject: [PATCH 76/94] Integrate review comments --- .../features/software-catalog/descriptor-format.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 7171e0c7b8..f1d426f407 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -102,17 +102,19 @@ Placeholders like `$json: https://example.com/entity.json` are substituted by the content of the referenced file. Files can be referenced from any configured integration similar to locations by passing an absolute URL. It's also possible to reference relative files like `./referenced.yaml` from the same location. -There are three different types of placeholders: +Relative references are handled relative to the folder of the +`catalog-info.yaml` that contains the placeholder. There are three different +types of placeholders: -- `$text`: Interprets the content of the referenced file as plain text and embed - it as a string. -- `$json`: Interprets the content of the referenced file as JSON and embed the +- `$text`: Interprets the contents of the referenced file as plain text and + embeds it as a string. +- `$json`: Interprets the contents of the referenced file as JSON and embeds the parsed structure. -- `$yaml`: Interprets the content of the referenced file as YAML and embed the +- `$yaml`: Interprets the contents of the referenced file as YAML and embeds the parsed structure. For example, this can be used to load the definition of an API entity from a web -server and embed it into the entity: +server and embed it as a string in the field `spec.definition`: ```yaml apiVersion: backstage.io/v1alpha1 From 121af544b105421c8afb705f053e03b77fbf39d0 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 24 Nov 2020 17:29:29 +0100 Subject: [PATCH 77/94] feat: added config schema for k8s plugins --- plugins/kubernetes-backend/package.json | 4 ++- plugins/kubernetes-backend/schema.d.ts | 27 +++++++++++++++ plugins/kubernetes/package.json | 4 ++- plugins/kubernetes/schema.d.ts | 45 +++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 plugins/kubernetes-backend/schema.d.ts create mode 100644 plugins/kubernetes/schema.d.ts diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index be7b86dd52..dfc9e5377c 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -10,6 +10,7 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "configSchema": "schema.d.ts", "scripts": { "start": "backstage-cli backend:dev", "build": "backstage-cli backend:build", @@ -42,6 +43,7 @@ "supertest": "^4.0.2" }, "files": [ - "dist" + "dist", + "schema.d.ts" ] } diff --git a/plugins/kubernetes-backend/schema.d.ts b/plugins/kubernetes-backend/schema.d.ts new file mode 100644 index 0000000000..862cb6d2ce --- /dev/null +++ b/plugins/kubernetes-backend/schema.d.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export interface Config { + kubernetes?: { + serviceLocatorMethod: 'multiTenant'; + clusterLocatorMethods: 'config'[]; + clusters: { + url: string; + name: string; + serviceAccountToken: string; + authProvider: 'serviceAccount'; + }[]; + }; +} diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 36954cc5b9..3b6d48249d 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -9,6 +9,7 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "configSchema": "schema.d.ts", "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", @@ -47,6 +48,7 @@ "msw": "^0.21.2" }, "files": [ - "dist" + "dist", + "schema.d.ts" ] } diff --git a/plugins/kubernetes/schema.d.ts b/plugins/kubernetes/schema.d.ts new file mode 100644 index 0000000000..f3f3c98f8c --- /dev/null +++ b/plugins/kubernetes/schema.d.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export interface Config { + kubernetes?: { + /** + * @visibility frontend + */ + serviceLocatorMethod: 'multiTenant'; + /** + * @visibility frontend + */ + clusterLocatorMethods: 'config'[]; + clusters: { + /** + * @visibility frontend + */ + url: string; + /** + * @visibility frontend + */ + name: string; + /** + * @visibility secret + */ + serviceAccountToken: string; + /** + * @visibility frontend + */ + authProvider: 'serviceAccount'; + }[]; + }; +} From 0c0c48bcc5c166d001167263119d2ab6dda833e7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 24 Nov 2020 16:56:00 +0000 Subject: [PATCH 78/94] Version Packages --- .changeset/breezy-cobras-deny.md | 5 --- .changeset/chatty-radios-beam.md | 7 ---- .changeset/clever-moons-shake.md | 5 --- .changeset/cli-links.md | 5 --- .changeset/cost-insights-quiet-bikes-occur.md | 5 --- .../cost-insights-tiny-llamas-perform.md | 5 --- .changeset/curly-yaks-invite.md | 5 --- .changeset/dull-pans-sip.md | 6 --- .changeset/empty-kids-look.md | 5 --- .changeset/forty-buses-watch.md | 14 ------- .changeset/long-flowers-wink.md | 5 --- .changeset/mighty-cycles-impress.md | 5 --- .changeset/new-ladybugs-nail.md | 5 --- .changeset/orange-cherries-run.md | 5 --- .changeset/poor-mails-marry.md | 5 --- .changeset/small-worms-check.md | 6 --- .changeset/tame-rockets-explode.md | 5 --- .changeset/tidy-brooms-look.md | 5 --- packages/app/CHANGELOG.md | 37 ++++++++++++++++++ packages/app/package.json | 38 +++++++++---------- packages/backend-common/CHANGELOG.md | 7 ++++ packages/backend-common/package.json | 4 +- packages/backend/CHANGELOG.md | 21 ++++++++++ packages/backend/package.json | 20 +++++----- packages/catalog-client/CHANGELOG.md | 8 ++++ packages/catalog-client/package.json | 6 +-- packages/catalog-model/CHANGELOG.md | 18 +++++++++ packages/catalog-model/package.json | 4 +- packages/cli/CHANGELOG.md | 9 +++++ packages/cli/package.json | 6 +-- packages/core/CHANGELOG.md | 6 +++ packages/core/package.json | 4 +- packages/create-app/package.json | 32 ++++++++-------- plugins/api-docs/CHANGELOG.md | 11 ++++++ plugins/api-docs/package.json | 10 ++--- plugins/auth-backend/CHANGELOG.md | 12 ++++++ plugins/auth-backend/package.json | 10 ++--- plugins/catalog-backend/CHANGELOG.md | 22 +++++++++++ plugins/catalog-backend/package.json | 8 ++-- plugins/catalog-graphql/CHANGELOG.md | 11 ++++++ plugins/catalog-graphql/package.json | 8 ++-- plugins/catalog/CHANGELOG.md | 14 +++++++ plugins/catalog/package.json | 14 +++---- plugins/circleci/CHANGELOG.md | 12 ++++++ plugins/circleci/package.json | 10 ++--- plugins/cloudbuild/CHANGELOG.md | 11 ++++++ plugins/cloudbuild/package.json | 10 ++--- plugins/cost-insights/CHANGELOG.md | 9 +++++ plugins/cost-insights/package.json | 6 +-- plugins/github-actions/CHANGELOG.md | 11 ++++++ plugins/github-actions/package.json | 10 ++--- plugins/jenkins/CHANGELOG.md | 11 ++++++ plugins/jenkins/package.json | 10 ++--- plugins/kubernetes-backend/CHANGELOG.md | 15 ++++++++ plugins/kubernetes-backend/package.json | 8 ++-- plugins/kubernetes/CHANGELOG.md | 15 ++++++++ plugins/kubernetes/package.json | 10 ++--- plugins/lighthouse/CHANGELOG.md | 11 ++++++ plugins/lighthouse/package.json | 10 ++--- plugins/register-component/CHANGELOG.md | 12 ++++++ plugins/register-component/package.json | 10 ++--- plugins/rollbar/CHANGELOG.md | 11 ++++++ plugins/rollbar/package.json | 10 ++--- plugins/scaffolder-backend/CHANGELOG.md | 13 +++++++ plugins/scaffolder-backend/package.json | 8 ++-- plugins/scaffolder/CHANGELOG.md | 12 ++++++ plugins/scaffolder/package.json | 10 ++--- plugins/search/CHANGELOG.md | 13 +++++++ plugins/search/package.json | 10 ++--- plugins/sentry/CHANGELOG.md | 10 +++++ plugins/sentry/package.json | 8 ++-- plugins/sonarqube/CHANGELOG.md | 11 ++++++ plugins/sonarqube/package.json | 8 ++-- plugins/techdocs-backend/CHANGELOG.md | 11 ++++++ plugins/techdocs-backend/package.json | 8 ++-- plugins/techdocs/CHANGELOG.md | 11 ++++++ plugins/techdocs/package.json | 10 ++--- 77 files changed, 535 insertions(+), 263 deletions(-) delete mode 100644 .changeset/breezy-cobras-deny.md delete mode 100644 .changeset/chatty-radios-beam.md delete mode 100644 .changeset/clever-moons-shake.md delete mode 100644 .changeset/cli-links.md delete mode 100644 .changeset/cost-insights-quiet-bikes-occur.md delete mode 100644 .changeset/cost-insights-tiny-llamas-perform.md delete mode 100644 .changeset/curly-yaks-invite.md delete mode 100644 .changeset/dull-pans-sip.md delete mode 100644 .changeset/empty-kids-look.md delete mode 100644 .changeset/forty-buses-watch.md delete mode 100644 .changeset/long-flowers-wink.md delete mode 100644 .changeset/mighty-cycles-impress.md delete mode 100644 .changeset/new-ladybugs-nail.md delete mode 100644 .changeset/orange-cherries-run.md delete mode 100644 .changeset/poor-mails-marry.md delete mode 100644 .changeset/small-worms-check.md delete mode 100644 .changeset/tame-rockets-explode.md delete mode 100644 .changeset/tidy-brooms-look.md create mode 100644 plugins/search/CHANGELOG.md diff --git a/.changeset/breezy-cobras-deny.md b/.changeset/breezy-cobras-deny.md deleted file mode 100644 index 5f035c05ce..0000000000 --- a/.changeset/breezy-cobras-deny.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core': patch ---- - -Clear sidebar search field once a search is executed diff --git a/.changeset/chatty-radios-beam.md b/.changeset/chatty-radios-beam.md deleted file mode 100644 index 91ad76ee88..0000000000 --- a/.changeset/chatty-radios-beam.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/catalog-model': minor -'@backstage/plugin-kubernetes': minor -'@backstage/plugin-kubernetes-backend': minor ---- - -add kubernetes selector to component model diff --git a/.changeset/clever-moons-shake.md b/.changeset/clever-moons-shake.md deleted file mode 100644 index 0fb6c690c2..0000000000 --- a/.changeset/clever-moons-shake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Added readTree support to AzureUrlReader diff --git a/.changeset/cli-links.md b/.changeset/cli-links.md deleted file mode 100644 index 0a9779de93..0000000000 --- a/.changeset/cli-links.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -The CLI now detects and transforms linked packages. You can link in external packages by adding them to both the `lerna.json` and `package.json` workspace paths. diff --git a/.changeset/cost-insights-quiet-bikes-occur.md b/.changeset/cost-insights-quiet-bikes-occur.md deleted file mode 100644 index aac283bbd4..0000000000 --- a/.changeset/cost-insights-quiet-bikes-occur.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-cost-insights': patch ---- - -fix product icon configuration diff --git a/.changeset/cost-insights-tiny-llamas-perform.md b/.changeset/cost-insights-tiny-llamas-perform.md deleted file mode 100644 index c81c28f8f8..0000000000 --- a/.changeset/cost-insights-tiny-llamas-perform.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-cost-insights': patch ---- - -truncate large percentages > 1000% diff --git a/.changeset/curly-yaks-invite.md b/.changeset/curly-yaks-invite.md deleted file mode 100644 index b9191a3895..0000000000 --- a/.changeset/curly-yaks-invite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Add support for reading groups and users from the Microsoft Graph API. diff --git a/.changeset/dull-pans-sip.md b/.changeset/dull-pans-sip.md deleted file mode 100644 index 13616c1f8d..0000000000 --- a/.changeset/dull-pans-sip.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-scaffolder-backend': patch ---- - -Move constructing the catalog-info.yaml URL for scaffolded components to the publishers diff --git a/.changeset/empty-kids-look.md b/.changeset/empty-kids-look.md deleted file mode 100644 index 680cd7f25c..0000000000 --- a/.changeset/empty-kids-look.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-register-component': patch ---- - -Remove catalog link on validate popup diff --git a/.changeset/forty-buses-watch.md b/.changeset/forty-buses-watch.md deleted file mode 100644 index 4074566fe7..0000000000 --- a/.changeset/forty-buses-watch.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/catalog-model': patch -'@backstage/plugin-catalog-backend': patch ---- - -Marked the `Group` entity fields `ancestors` and `descendants` for deprecation on Dec 6th, 2020. See https://github.com/backstage/backstage/issues/3049 for details. - -Code that consumes these fields should remove those usages as soon as possible. There is no current or planned replacement for these fields. - -The BuiltinKindsEntityProcessor has been updated to inject these fields as empty arrays if they are missing. Therefore, if you are on a catalog instance that uses the updated version of this code, you can start removing the fields from your source catalog-info.yaml data as well, without breaking validation. - -After Dec 6th, the fields will be removed from types and classes of the Backstage repository. At the first release after that, they will not be present in released packages either. - -If your catalog-info.yaml files still contain these fields after the deletion, they will still be valid and your ingestion will not break, but they won't be visible in the types for consuming code. diff --git a/.changeset/long-flowers-wink.md b/.changeset/long-flowers-wink.md deleted file mode 100644 index 7f9cf12315..0000000000 --- a/.changeset/long-flowers-wink.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-sonarqube': patch ---- - -Add configuration schema diff --git a/.changeset/mighty-cycles-impress.md b/.changeset/mighty-cycles-impress.md deleted file mode 100644 index 56ec35098f..0000000000 --- a/.changeset/mighty-cycles-impress.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-circleci': patch ---- - -Improved CircleCI builds table to show more information and relevant links diff --git a/.changeset/new-ladybugs-nail.md b/.changeset/new-ladybugs-nail.md deleted file mode 100644 index 5351b6dff3..0000000000 --- a/.changeset/new-ladybugs-nail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -New lint rule to disallow assertions and promote `as` assertions. - @typescript-eslint/consistent-type-assertions diff --git a/.changeset/orange-cherries-run.md b/.changeset/orange-cherries-run.md deleted file mode 100644 index 3c06c27b47..0000000000 --- a/.changeset/orange-cherries-run.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Add experimental backend:bundle command diff --git a/.changeset/poor-mails-marry.md b/.changeset/poor-mails-marry.md deleted file mode 100644 index fdc7e6f1b8..0000000000 --- a/.changeset/poor-mails-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Add new `versions:check` and `versions:bump` commands to simplify version management and avoid conflicts diff --git a/.changeset/small-worms-check.md b/.changeset/small-worms-check.md deleted file mode 100644 index e39265f8f9..0000000000 --- a/.changeset/small-worms-check.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'example-app': patch -'@backstage/plugin-search': patch ---- - -Using the search field in the sidebar now navigates to the search result page. diff --git a/.changeset/tame-rockets-explode.md b/.changeset/tame-rockets-explode.md deleted file mode 100644 index 123472701e..0000000000 --- a/.changeset/tame-rockets-explode.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Make integration host and url configurations visible in the frontend diff --git a/.changeset/tidy-brooms-look.md b/.changeset/tidy-brooms-look.md deleted file mode 100644 index f2c2a9064e..0000000000 --- a/.changeset/tidy-brooms-look.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Fix React entity YAML filename to new standard diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index ab00924a26..f70cd198b4 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,42 @@ # example-app +## 0.2.3 + +### Patch Changes + +- 475fc0aaa: Using the search field in the sidebar now navigates to the search result page. +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [29a0ccab2] +- Updated dependencies [8e6728e25] +- Updated dependencies [c93a14b49] +- Updated dependencies [ef2831dde] +- Updated dependencies [2a71f4bab] +- Updated dependencies [1185919f3] +- Updated dependencies [a8de7f554] +- Updated dependencies [faf311c26] +- Updated dependencies [31d8b6979] +- Updated dependencies [991345969] +- Updated dependencies [475fc0aaa] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-kubernetes@0.3.0 + - @backstage/cli@0.3.1 + - @backstage/plugin-cost-insights@0.4.1 + - @backstage/plugin-scaffolder@0.3.1 + - @backstage/plugin-register-component@0.2.2 + - @backstage/plugin-circleci@0.2.2 + - @backstage/plugin-search@0.2.1 + - @backstage/plugin-api-docs@0.2.2 + - @backstage/plugin-catalog@0.2.3 + - @backstage/plugin-cloudbuild@0.2.2 + - @backstage/plugin-github-actions@0.2.2 + - @backstage/plugin-jenkins@0.3.1 + - @backstage/plugin-lighthouse@0.2.3 + - @backstage/plugin-rollbar@0.2.3 + - @backstage/plugin-sentry@0.2.3 + - @backstage/plugin-techdocs@0.2.3 + ## 0.2.2 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 042fd47188..9e54005cd3 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,33 +1,33 @@ { "name": "example-app", - "version": "0.2.2", + "version": "0.2.3", "private": true, "bundled": true, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/cli": "^0.3.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-api-docs": "^0.2.1", - "@backstage/plugin-catalog": "^0.2.2", - "@backstage/plugin-circleci": "^0.2.1", - "@backstage/plugin-cloudbuild": "^0.2.1", - "@backstage/plugin-cost-insights": "^0.4.0", + "@backstage/catalog-model": "^0.3.0", + "@backstage/cli": "^0.3.1", + "@backstage/core": "^0.3.2", + "@backstage/plugin-api-docs": "^0.2.2", + "@backstage/plugin-catalog": "^0.2.3", + "@backstage/plugin-circleci": "^0.2.2", + "@backstage/plugin-cloudbuild": "^0.2.2", + "@backstage/plugin-cost-insights": "^0.4.1", "@backstage/plugin-explore": "^0.2.1", "@backstage/plugin-gcp-projects": "^0.2.1", - "@backstage/plugin-github-actions": "^0.2.1", + "@backstage/plugin-github-actions": "^0.2.2", "@backstage/plugin-gitops-profiles": "^0.2.1", "@backstage/plugin-graphiql": "^0.2.1", - "@backstage/plugin-jenkins": "^0.3.0", - "@backstage/plugin-kubernetes": "^0.2.1", - "@backstage/plugin-lighthouse": "^0.2.2", + "@backstage/plugin-jenkins": "^0.3.1", + "@backstage/plugin-kubernetes": "^0.3.0", + "@backstage/plugin-lighthouse": "^0.2.3", "@backstage/plugin-newrelic": "^0.2.1", - "@backstage/plugin-register-component": "^0.2.1", - "@backstage/plugin-rollbar": "^0.2.2", - "@backstage/plugin-scaffolder": "^0.3.0", - "@backstage/plugin-sentry": "^0.2.2", - "@backstage/plugin-search": "^0.2.0", + "@backstage/plugin-register-component": "^0.2.2", + "@backstage/plugin-rollbar": "^0.2.3", + "@backstage/plugin-scaffolder": "^0.3.1", + "@backstage/plugin-sentry": "^0.2.3", + "@backstage/plugin-search": "^0.2.1", "@backstage/plugin-tech-radar": "^0.3.0", - "@backstage/plugin-techdocs": "^0.2.2", + "@backstage/plugin-techdocs": "^0.2.3", "@backstage/plugin-user-settings": "^0.2.2", "@backstage/plugin-welcome": "^0.2.1", "@backstage/test-utils": "^0.1.3", diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 4822032c7f..793a554905 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/backend-common +## 0.3.1 + +### Patch Changes + +- bff3305aa: Added readTree support to AzureUrlReader +- b47dce06f: Make integration host and url configurations visible in the frontend + ## 0.3.0 ### Minor Changes diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index b22e4638f5..731e2679cb 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -67,7 +67,7 @@ } }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/test-utils": "^0.1.3", "@types/archiver": "^3.1.1", "@types/compression": "^1.7.0", diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index a8bc1fef7d..bc2a08e983 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,26 @@ # example-backend +## 0.2.3 + +### Patch Changes + +- Updated dependencies [1166fcc36] +- Updated dependencies [bff3305aa] +- Updated dependencies [0c2121240] +- Updated dependencies [ef2831dde] +- Updated dependencies [1185919f3] +- Updated dependencies [475fc0aaa] +- Updated dependencies [b47dce06f] +- Updated dependencies [5a1d8dca3] + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-kubernetes-backend@0.2.0 + - @backstage/backend-common@0.3.1 + - @backstage/plugin-catalog-backend@0.2.2 + - @backstage/plugin-scaffolder-backend@0.3.2 + - example-app@0.2.3 + - @backstage/plugin-auth-backend@0.2.3 + - @backstage/plugin-techdocs-backend@0.2.2 + ## 0.2.2 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 014a3f78d5..5d3678a284 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.2", + "version": "0.2.3", "main": "dist/index.cjs.js", "types": "src/index.ts", "private": true, @@ -18,24 +18,24 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "@backstage/plugin-app-backend": "^0.3.0", - "@backstage/plugin-auth-backend": "^0.2.2", - "@backstage/plugin-catalog-backend": "^0.2.1", + "@backstage/plugin-auth-backend": "^0.2.3", + "@backstage/plugin-catalog-backend": "^0.2.2", "@backstage/plugin-graphql-backend": "^0.1.3", - "@backstage/plugin-kubernetes-backend": "^0.1.3", + "@backstage/plugin-kubernetes-backend": "^0.2.0", "@backstage/plugin-proxy-backend": "^0.2.1", "@backstage/plugin-rollbar-backend": "^0.1.3", - "@backstage/plugin-scaffolder-backend": "^0.3.1", + "@backstage/plugin-scaffolder-backend": "^0.3.2", "@backstage/plugin-sentry-backend": "^0.1.3", - "@backstage/plugin-techdocs-backend": "^0.2.1", + "@backstage/plugin-techdocs-backend": "^0.2.2", "@gitbeaker/node": "^25.2.0", "@octokit/rest": "^18.0.0", "azure-devops-node-api": "^10.1.1", "dockerode": "^3.2.0", - "example-app": "^0.2.2", + "example-app": "^0.2.3", "express": "^4.17.1", "express-promise-router": "^3.0.3", "knex": "^0.21.6", @@ -45,7 +45,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@types/dockerode": "^2.5.32", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5", diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index 574cd301bf..3454a6c8e0 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/catalog-client +## 0.3.1 + +### Patch Changes + +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/catalog-model@0.3.0 + ## 0.3.0 ### Minor Changes diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index 2482f9579d..4509b7d547 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-client", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,12 +20,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "cross-fetch": "^3.0.6" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@types/jest": "^26.0.7", "msw": "^0.21.2" }, diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index 051aa82c80..10bf540b46 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/catalog-model +## 0.3.0 + +### Minor Changes + +- 1166fcc36: add kubernetes selector to component model + +### Patch Changes + +- 1185919f3: Marked the `Group` entity fields `ancestors` and `descendants` for deprecation on Dec 6th, 2020. See https://github.com/backstage/backstage/issues/3049 for details. + + Code that consumes these fields should remove those usages as soon as possible. There is no current or planned replacement for these fields. + + The BuiltinKindsEntityProcessor has been updated to inject these fields as empty arrays if they are missing. Therefore, if you are on a catalog instance that uses the updated version of this code, you can start removing the fields from your source catalog-info.yaml data as well, without breaking validation. + + After Dec 6th, the fields will be removed from types and classes of the Backstage repository. At the first release after that, they will not be present in released packages either. + + If your catalog-info.yaml files still contain these fields after the deletion, they will still be valid and your ingestion will not break, but they won't be visible in the types for consuming code. + ## 0.2.0 ### Minor Changes diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index e712e66f58..7956454ca9 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-model", - "version": "0.2.0", + "version": "0.3.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,7 +29,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@types/express": "^4.17.6", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index a5bcc6bcfd..d198444c86 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/cli +## 0.3.1 + +### Patch Changes + +- 29a0ccab2: The CLI now detects and transforms linked packages. You can link in external packages by adding them to both the `lerna.json` and `package.json` workspace paths. +- faf311c26: New lint rule to disallow assertions and promote `as` assertions. - @typescript-eslint/consistent-type-assertions +- 31d8b6979: Add experimental backend:bundle command +- 991345969: Add new `versions:check` and `versions:bump` commands to simplify version management and avoid conflicts + ## 0.3.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index eb56b19971..034c8a3de5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.3.0", + "version": "0.3.1", "private": false, "publishConfig": { "access": "public" @@ -111,9 +111,9 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.3.0", + "@backstage/backend-common": "^0.3.1", "@backstage/config": "^0.1.1", - "@backstage/core": "^0.3.1", + "@backstage/core": "^0.3.2", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@backstage/theme": "^0.2.1", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 213e2b5246..d29bc53452 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/core +## 0.3.2 + +### Patch Changes + +- 475fc0aaa: Clear sidebar search field once a search is executed + ## 0.3.1 ### Patch Changes diff --git a/packages/core/package.json b/packages/core/package.json index d65f0a16e5..f667ffe7be 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core", "description": "Core API used by Backstage plugins and apps", - "version": "0.3.1", + "version": "0.3.2", "private": false, "publishConfig": { "access": "public", @@ -64,7 +64,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/packages/create-app/package.json b/packages/create-app/package.json index c01d4d3dbb..6e91a916a1 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -37,28 +37,28 @@ "recursive-readdir": "^2.2.2" }, "devDependencies": { - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", - "@backstage/cli": "^0.3.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/config": "^0.1.1", - "@backstage/core": "^0.3.1", - "@backstage/plugin-api-docs": "^0.2.1", + "@backstage/core": "^0.3.2", + "@backstage/plugin-api-docs": "^0.2.2", "@backstage/plugin-app-backend": "^0.3.0", - "@backstage/plugin-auth-backend": "^0.2.2", - "@backstage/plugin-catalog": "^0.2.2", - "@backstage/plugin-catalog-backend": "^0.2.1", - "@backstage/plugin-circleci": "^0.2.1", + "@backstage/plugin-auth-backend": "^0.2.3", + "@backstage/plugin-catalog": "^0.2.3", + "@backstage/plugin-catalog-backend": "^0.2.2", + "@backstage/plugin-circleci": "^0.2.2", "@backstage/plugin-explore": "^0.2.1", - "@backstage/plugin-github-actions": "^0.2.1", - "@backstage/plugin-lighthouse": "^0.2.2", + "@backstage/plugin-github-actions": "^0.2.2", + "@backstage/plugin-lighthouse": "^0.2.3", "@backstage/plugin-proxy-backend": "^0.2.1", - "@backstage/plugin-register-component": "^0.2.1", + "@backstage/plugin-register-component": "^0.2.2", "@backstage/plugin-rollbar-backend": "^0.1.3", - "@backstage/plugin-scaffolder": "^0.3.0", - "@backstage/plugin-scaffolder-backend": "^0.3.1", + "@backstage/plugin-scaffolder": "^0.3.1", + "@backstage/plugin-scaffolder-backend": "^0.3.2", "@backstage/plugin-tech-radar": "^0.3.0", - "@backstage/plugin-techdocs": "^0.2.2", - "@backstage/plugin-techdocs-backend": "^0.2.1", + "@backstage/plugin-techdocs": "^0.2.3", + "@backstage/plugin-techdocs-backend": "^0.2.2", "@backstage/plugin-user-settings": "^0.2.2", "@backstage/test-utils": "^0.1.3", "@backstage/theme": "^0.2.1", diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 5f1ad97e64..60a5caf55b 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-api-docs +## 0.2.2 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.1 ### Patch Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index bfefaef3a5..691f2680b2 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@kyma-project/asyncapi-react": "^0.14.2", "@material-icons/font": "^1.0.2", @@ -39,7 +39,7 @@ "swagger-ui-react": "^3.31.1" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 546855c92f..4a2161ad71 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-auth-backend +## 0.2.3 + +### Patch Changes + +- Updated dependencies [1166fcc36] +- Updated dependencies [bff3305aa] +- Updated dependencies [1185919f3] +- Updated dependencies [b47dce06f] + - @backstage/catalog-model@0.3.0 + - @backstage/backend-common@0.3.1 + - @backstage/catalog-client@0.3.1 + ## 0.2.2 ### Patch Changes diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 4d1e52aa21..43e2c59f88 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-client": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-client": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "@types/express": "^4.17.6", "compression": "^1.7.4", @@ -55,7 +55,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 1a3199ee14..3bf25a494b 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-catalog-backend +## 0.2.2 + +### Patch Changes + +- 0c2121240: Add support for reading groups and users from the Microsoft Graph API. +- 1185919f3: Marked the `Group` entity fields `ancestors` and `descendants` for deprecation on Dec 6th, 2020. See https://github.com/backstage/backstage/issues/3049 for details. + + Code that consumes these fields should remove those usages as soon as possible. There is no current or planned replacement for these fields. + + The BuiltinKindsEntityProcessor has been updated to inject these fields as empty arrays if they are missing. Therefore, if you are on a catalog instance that uses the updated version of this code, you can start removing the fields from your source catalog-info.yaml data as well, without breaking validation. + + After Dec 6th, the fields will be removed from types and classes of the Backstage repository. At the first release after that, they will not be present in released packages either. + + If your catalog-info.yaml files still contain these fields after the deletion, they will still be valid and your ingestion will not break, but they won't be visible in the types for consuming code. + +- Updated dependencies [1166fcc36] +- Updated dependencies [bff3305aa] +- Updated dependencies [1185919f3] +- Updated dependencies [b47dce06f] + - @backstage/catalog-model@0.3.0 + - @backstage/backend-common@0.3.1 + ## 0.2.1 ### Patch Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 12a17d6365..51a9978f30 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ }, "dependencies": { "@azure/msal-node": "^1.0.0-alpha.8", - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "@octokit/graphql": "^4.5.6", "@types/express": "^4.17.6", @@ -47,7 +47,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/test-utils": "^0.1.3", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/catalog-graphql/CHANGELOG.md b/plugins/catalog-graphql/CHANGELOG.md index 35737b7dd1..56911b9c45 100644 --- a/plugins/catalog-graphql/CHANGELOG.md +++ b/plugins/catalog-graphql/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-graphql +## 0.2.2 + +### Patch Changes + +- Updated dependencies [1166fcc36] +- Updated dependencies [bff3305aa] +- Updated dependencies [1185919f3] +- Updated dependencies [b47dce06f] + - @backstage/catalog-model@0.3.0 + - @backstage/backend-common@0.3.1 + ## 0.2.1 ### Patch Changes diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index c55be89b62..a489d66e5b 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graphql", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "@graphql-modules/core": "^0.7.17", "apollo-server": "^2.16.1", @@ -32,7 +32,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/test-utils": "^0.1.3", "@graphql-codegen/cli": "^1.17.7", "@graphql-codegen/typescript": "^1.17.7", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 1b8b884f1c..9137b7e948 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog +## 0.2.3 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [ef2831dde] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-scaffolder@0.3.1 + - @backstage/catalog-client@0.3.1 + - @backstage/plugin-techdocs@0.2.3 + ## 0.2.2 ### Patch Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 2c65096fd6..7b58358734 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,11 +21,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-client": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-scaffolder": "^0.3.0", - "@backstage/plugin-techdocs": "^0.2.2", + "@backstage/catalog-client": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-scaffolder": "^0.3.1", + "@backstage/plugin-techdocs": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -43,7 +43,7 @@ "swr": "^0.3.0" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@microsoft/microsoft-graph-types": "^1.25.0", diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md index 2894ce1bd3..a81531e28d 100644 --- a/plugins/circleci/CHANGELOG.md +++ b/plugins/circleci/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-circleci +## 0.2.2 + +### Patch Changes + +- a8de7f554: Improved CircleCI builds table to show more information and relevant links +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.1 ### Patch Changes diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index eaa8c319a0..fd32ff9891 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-circleci", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "postpack": "backstage-cli postpack" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -40,7 +40,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md index 9d02fd1ea3..4cb2d01377 100644 --- a/plugins/cloudbuild/CHANGELOG.md +++ b/plugins/cloudbuild/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-cloudbuild +## 0.2.2 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.1 ### Patch Changes diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index c0b36ef1e7..3e7f0c9175 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cloudbuild", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -39,7 +39,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index 157d92f251..b7bdfa9d50 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-cost-insights +## 0.4.1 + +### Patch Changes + +- 8e6728e25: fix product icon configuration +- c93a14b49: truncate large percentages > 1000% +- Updated dependencies [475fc0aaa] + - @backstage/core@0.3.2 + ## 0.4.0 ### Minor Changes diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 8ebeac91c0..06083b855b 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cost-insights", - "version": "0.4.0", + "version": "0.4.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,7 +22,7 @@ }, "dependencies": { "@backstage/config": "^0.1.1", - "@backstage/core": "^0.3.1", + "@backstage/core": "^0.3.2", "@backstage/test-utils": "^0.1.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", @@ -46,7 +46,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index b679d6844b..96f3876802 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-github-actions +## 0.2.2 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.1 ### Patch Changes diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 49ecfc5423..b1924034f1 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-actions", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", "@backstage/core-api": "^0.2.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -40,7 +40,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index 44febbc63d..aeeb069f19 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-jenkins +## 0.3.1 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.3.0 ### Minor Changes diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 0279592a75..961f551dfa 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -36,7 +36,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 7c55a7407d..2877264e54 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-kubernetes-backend +## 0.2.0 + +### Minor Changes + +- 1166fcc36: add kubernetes selector to component model + +### Patch Changes + +- Updated dependencies [1166fcc36] +- Updated dependencies [bff3305aa] +- Updated dependencies [1185919f3] +- Updated dependencies [b47dce06f] + - @backstage/catalog-model@0.3.0 + - @backstage/backend-common@0.3.1 + ## 0.1.3 ### Patch Changes diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index dfc9e5377c..8fe2f4c58f 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-backend", - "version": "0.1.3", + "version": "0.2.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "@kubernetes/client-node": "^0.12.1", "@types/express": "^4.17.6", @@ -39,7 +39,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index e5611e4953..652fc394a2 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-kubernetes +## 0.3.0 + +### Minor Changes + +- 1166fcc36: add kubernetes selector to component model + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-kubernetes-backend@0.2.0 + ## 0.2.1 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 3b6d48249d..66929077f3 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.2.1", + "version": "0.3.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", - "@backstage/core": "^0.3.1", - "@backstage/plugin-kubernetes-backend": "^0.1.3", + "@backstage/core": "^0.3.2", + "@backstage/plugin-kubernetes-backend": "^0.2.0", "@backstage/theme": "^0.2.1", "@kubernetes/client-node": "^0.12.1", "@material-ui/core": "^4.11.0", @@ -36,7 +36,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md index 666e7c4411..e35f3dece0 100644 --- a/plugins/lighthouse/CHANGELOG.md +++ b/plugins/lighthouse/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-lighthouse +## 0.2.3 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.2 ### Patch Changes diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 90230f5248..ad1264cfbb 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-lighthouse", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,11 +21,11 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", - "@backstage/core": "^0.3.1", + "@backstage/core": "^0.3.2", "@backstage/core-api": "^0.2.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -38,7 +38,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/register-component/CHANGELOG.md b/plugins/register-component/CHANGELOG.md index 3e966ef0d0..c569046855 100644 --- a/plugins/register-component/CHANGELOG.md +++ b/plugins/register-component/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-register-component +## 0.2.2 + +### Patch Changes + +- 2a71f4bab: Remove catalog link on validate popup +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.1 ### Patch Changes diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index 06ae17cb0e..1564faae0e 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-register-component", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -36,7 +36,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/rollbar/CHANGELOG.md b/plugins/rollbar/CHANGELOG.md index e4a6671f90..1ccfe16c41 100644 --- a/plugins/rollbar/CHANGELOG.md +++ b/plugins/rollbar/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-rollbar +## 0.2.3 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.2 ### Patch Changes diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index c107573ca8..1177a83eb6 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-rollbar", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -37,7 +37,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index a388a29a00..ea7e5c85ee 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-scaffolder-backend +## 0.3.2 + +### Patch Changes + +- ef2831dde: Move constructing the catalog-info.yaml URL for scaffolded components to the publishers +- 5a1d8dca3: Fix React entity YAML filename to new standard +- Updated dependencies [1166fcc36] +- Updated dependencies [bff3305aa] +- Updated dependencies [1185919f3] +- Updated dependencies [b47dce06f] + - @backstage/catalog-model@0.3.0 + - @backstage/backend-common@0.3.1 + ## 0.3.1 ### Patch Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 73c07d158f..a095167a08 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "0.3.1", + "version": "0.3.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "@gitbeaker/core": "^25.2.0", "@gitbeaker/node": "^25.2.0", @@ -48,7 +48,7 @@ "cross-fetch": "^3.0.6" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@octokit/types": "^5.4.1", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 9eac595997..c78e05c3e0 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder +## 0.3.1 + +### Patch Changes + +- ef2831dde: Move constructing the catalog-info.yaml URL for scaffolded components to the publishers +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.3.0 ### Minor Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 439fc0ad9a..791221bc62 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -41,7 +41,7 @@ "swr": "^0.3.0" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md new file mode 100644 index 0000000000..3545914e96 --- /dev/null +++ b/plugins/search/CHANGELOG.md @@ -0,0 +1,13 @@ +# @backstage/plugin-search + +## 0.2.1 + +### Patch Changes + +- 475fc0aaa: Using the search field in the sidebar now navigates to the search result page. +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 diff --git a/plugins/search/package.json b/plugins/search/package.json index df5c6c25b9..021e3f71f3 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "0.2.0", + "version": "0.2.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.2", - "@backstage/catalog-model": "^0.2.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.3", + "@backstage/catalog-model": "^0.3.0", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -34,7 +34,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index 92327c1668..b388548d8e 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-sentry +## 0.2.3 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + ## 0.2.2 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 80a37c574e..4c10936148 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -36,7 +36,7 @@ "timeago.js": "^4.0.2" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/sonarqube/CHANGELOG.md b/plugins/sonarqube/CHANGELOG.md index 0296bd0416..9ebd73b812 100644 --- a/plugins/sonarqube/CHANGELOG.md +++ b/plugins/sonarqube/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-sonarqube +## 0.1.4 + +### Patch Changes + +- 26484d413: Add configuration schema +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + ## 0.1.3 ### Patch Changes diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index be8a878e50..6a9d0fe0d4 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sonarqube", - "version": "0.1.3", + "version": "0.1.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -35,7 +35,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 4a3fbf6821..d9054230e2 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-techdocs-backend +## 0.2.2 + +### Patch Changes + +- Updated dependencies [1166fcc36] +- Updated dependencies [bff3305aa] +- Updated dependencies [1185919f3] +- Updated dependencies [b47dce06f] + - @backstage/catalog-model@0.3.0 + - @backstage/backend-common@0.3.1 + ## 0.2.1 ### Patch Changes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index b139bb9acf..1d9f3ea7c1 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.3.0", - "@backstage/catalog-model": "^0.2.0", + "@backstage/backend-common": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", "@backstage/config": "^0.1.1", "@types/dockerode": "^2.5.34", "@types/express": "^4.17.6", @@ -37,7 +37,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index bd3ae21ab8..3f46e1358b 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-techdocs +## 0.2.3 + +### Patch Changes + +- Updated dependencies [475fc0aaa] +- Updated dependencies [1166fcc36] +- Updated dependencies [1185919f3] + - @backstage/core@0.3.2 + - @backstage/catalog-model@0.3.0 + - @backstage/plugin-catalog@0.2.3 + ## 0.2.2 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 35b196e124..23d7a25b57 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.2.0", - "@backstage/core": "^0.3.1", + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", "@backstage/core-api": "^0.2.1", - "@backstage/plugin-catalog": "^0.2.2", + "@backstage/plugin-catalog": "^0.2.3", "@backstage/test-utils": "^0.1.3", "@backstage/theme": "^0.2.1", "@material-ui/core": "^4.11.0", @@ -39,7 +39,7 @@ "sanitize-html": "^1.27.0" }, "devDependencies": { - "@backstage/cli": "^0.3.0", + "@backstage/cli": "^0.3.1", "@backstage/dev-utils": "^0.1.4", "@backstage/test-utils": "^0.1.3", "@testing-library/jest-dom": "^5.10.1", From 5f1c1c2edf2b3f9c4463dc7d58afb1e8e7e843dd Mon Sep 17 00:00:00 2001 From: Brian Leathem Date: Mon, 23 Nov 2020 13:34:44 -0800 Subject: [PATCH 79/94] Fixed the OIDC token refresh --- .../src/providers/oidc/provider.ts | 51 ++++++++----------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 5ebb859f2f..765fd8627e 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -33,10 +33,8 @@ import { OAuthRefreshRequest, } from '../../lib/oauth'; import { - executeFetchUserProfileStrategy, executeFrameHandlerStrategy, executeRedirectStrategy, - executeRefreshTokenStrategy, PassportDoneCallback, } from '../../lib/passport'; import { RedirectInfo, AuthProviderFactory, ProfileInfo } from '../types'; @@ -45,20 +43,25 @@ type PrivateInfo = { refreshToken: string; }; +type OidcImpl = { + strategy: OidcStrategy; + client: Client; +}; + export type OidcAuthProviderOptions = OAuthProviderOptions & { metadataUrl: string; tokenSignedResponseAlg?: string; }; export class OidcAuthProvider implements OAuthHandlers { - readonly _strategy: Promise>; + readonly _implementation: Promise; constructor(options: OidcAuthProviderOptions) { - this._strategy = this.setupStrategy(options); + this._implementation = this.setupStrategy(options); } async start(req: OAuthStartRequest): Promise { - const strategy = await this._strategy; + const { strategy } = await this._implementation; return await executeRedirectStrategy(req, strategy, { accessType: 'offline', prompt: 'none', @@ -70,7 +73,7 @@ export class OidcAuthProvider implements OAuthHandlers { async handler( req: express.Request, ): Promise<{ response: OAuthResponse; refreshToken: string }> { - const strategy = await this._strategy; + const { strategy } = await this._implementation; const { response, privateInfo } = await executeFrameHandlerStrategy< OAuthResponse, PrivateInfo @@ -83,31 +86,19 @@ export class OidcAuthProvider implements OAuthHandlers { } async refresh(req: OAuthRefreshRequest): Promise { - const strategy = await this._strategy; - const refreshTokenResponse = await executeRefreshTokenStrategy( - strategy, - req.refreshToken, - req.scope, - ); - const { - accessToken, - params, - refreshToken: updatedRefreshToken, - } = refreshTokenResponse; - - const profile = await executeFetchUserProfileStrategy( - strategy, - accessToken, - params.id_token, - ); + const { client } = await this._implementation; + const tokenset = await client.refresh(req.refreshToken); + if (!tokenset.access_token) { + throw new Error('Refresh failed'); + } + const profile = await client.userinfo(tokenset.access_token); return this.populateIdentity({ providerInfo: { - accessToken, - refreshToken: updatedRefreshToken, - idToken: params.id_token, - expiresInSeconds: params.expires_in, - scope: params.scope, + accessToken: tokenset.access_token, + refreshToken: tokenset.refresh_token, + expiresInSeconds: tokenset.expires_at, + scope: tokenset.scope || '', }, profile, }); @@ -115,7 +106,7 @@ export class OidcAuthProvider implements OAuthHandlers { private async setupStrategy( options: OidcAuthProviderOptions, - ): Promise> { + ): Promise { const issuer = await Issuer.discover(options.metadataUrl); const client = new issuer.Client({ client_id: options.clientId, @@ -159,7 +150,7 @@ export class OidcAuthProvider implements OAuthHandlers { }, ); strategy.error = console.error; - return strategy; + return { strategy, client }; } // Use this function to grab the user profile info from the token From 636cc58dca2ddd8c19ce25e128b682295f8071d8 Mon Sep 17 00:00:00 2001 From: Brian Leathem Date: Mon, 23 Nov 2020 13:41:37 -0800 Subject: [PATCH 80/94] Fixed a TS type error in the OIDC provider test --- plugins/auth-backend/src/providers/oidc/provider.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts index ade69255b7..5073cd88d1 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.test.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -54,7 +54,7 @@ describe('OidcAuthProvider', () => { .get('/.well-known/openid-configuration') .reply(200, issuerMetadata); const provider = new OidcAuthProvider(clientMetadata); - const strategy = ((await provider._strategy) as any) as { + const strategy = ((await (provider as any)._strategy) as any) as { _client: ClientMetadata; _issuer: IssuerMetadata; }; From e3ad37e39d126a0c3dc9ccc8a22c061137ff0098 Mon Sep 17 00:00:00 2001 From: Brian Leathem Date: Mon, 23 Nov 2020 14:48:24 -0800 Subject: [PATCH 81/94] Fix broken tests --- .../src/providers/oidc/provider.test.ts | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts index 5073cd88d1..1851d1a473 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.test.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -22,7 +22,7 @@ import { createOidcProvider, OidcAuthProvider } from './provider'; import { JWT, JWK } from 'jose'; import { AuthProviderFactoryOptions } from '../types'; import { Config } from '@backstage/config'; -import { OAuthAdapter } from '../../lib/oauth'; +import { OAuthAdapter, OAuthStartRequest } from '../../lib/oauth'; const issuerMetadata = { issuer: 'https://oidc.test', @@ -54,9 +54,11 @@ describe('OidcAuthProvider', () => { .get('/.well-known/openid-configuration') .reply(200, issuerMetadata); const provider = new OidcAuthProvider(clientMetadata); - const strategy = ((await (provider as any)._strategy) as any) as { - _client: ClientMetadata; - _issuer: IssuerMetadata; + const { strategy } = ((await provider._implementation) as any) as { + strategy: { + _client: ClientMetadata; + _issuer: IssuerMetadata; + }; }; // Assert that the expected request to the metadaurl was made. expect(scope.isDone()).toBeTruthy(); @@ -89,27 +91,38 @@ describe('OidcAuthProvider', () => { const provider = new OidcAuthProvider(clientMetadata); const req = { method: 'GET', - url: '/?code=test2', + url: 'https://oidc.test/?code=test2', session: ({ 'oidc:oidc.test': 'test' } as any) as Session, } as express.Request; await provider.handler(req); expect(scope.isDone()).toBeTruthy(); }); - const options = { - globalConfig: { - appUrl: 'https://oidc.test', - baseUrl: 'https://oidc.test', - }, - config: ({ - keys: jest.fn(() => ['test']), - getConfig: jest.fn(() => ({ getString: () => '' })), - } as any) as Config, - } as AuthProviderFactoryOptions; - - it('createOidcProvider', () => { + it('createOidcProvider', async () => { + const scope = nock('https://oidc.test') + .get('/.well-known/openid-configuration') + .reply(200, issuerMetadata); + const options = { + globalConfig: { + appUrl: 'https://oidc.test', + baseUrl: 'https://oidc.test', + }, + config: ({ + keys: jest.fn(() => ['test']), + getConfig: jest.fn(() => ({ + getString: (key: string) => { + const conf = { + ...clientMetadata, + metadataUrl: 'https://oidc.test/.well-known/openid-configuration', + } as any; + return conf[key] as string; + }, + })), + } as any) as Config, + } as AuthProviderFactoryOptions; const provider = createOidcProvider(options) as OAuthAdapter; - console.log(provider); expect(provider.start).toBeDefined(); + await new Promise(resolve => process.nextTick(resolve)); // advance a tick to give nock a chance to intercept the request + expect(scope.isDone()).toBeTruthy(); }); }); From 78177f24b7770e81ef414b64b29069606cac3ea8 Mon Sep 17 00:00:00 2001 From: Brian Leathem Date: Mon, 23 Nov 2020 14:54:24 -0800 Subject: [PATCH 82/94] Fixed a lint failure on the oidc provider test --- plugins/auth-backend/src/providers/oidc/provider.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts index 1851d1a473..3732e7b1a7 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.test.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -22,7 +22,7 @@ import { createOidcProvider, OidcAuthProvider } from './provider'; import { JWT, JWK } from 'jose'; import { AuthProviderFactoryOptions } from '../types'; import { Config } from '@backstage/config'; -import { OAuthAdapter, OAuthStartRequest } from '../../lib/oauth'; +import { OAuthAdapter } from '../../lib/oauth'; const issuerMetadata = { issuer: 'https://oidc.test', From cec3da2c901998a71074936a62ed9383dd314bf6 Mon Sep 17 00:00:00 2001 From: Brian Leathem Date: Mon, 23 Nov 2020 15:23:21 -0800 Subject: [PATCH 83/94] Used ts private instead of _ naming convention --- .../auth-backend/src/providers/oidc/provider.test.ts | 2 +- plugins/auth-backend/src/providers/oidc/provider.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts index 3732e7b1a7..f2bd8dcb90 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.test.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -54,7 +54,7 @@ describe('OidcAuthProvider', () => { .get('/.well-known/openid-configuration') .reply(200, issuerMetadata); const provider = new OidcAuthProvider(clientMetadata); - const { strategy } = ((await provider._implementation) as any) as { + const { strategy } = ((await (provider as any).implementation) as any) as { strategy: { _client: ClientMetadata; _issuer: IssuerMetadata; diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 765fd8627e..4f2054c5f6 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -54,14 +54,14 @@ export type OidcAuthProviderOptions = OAuthProviderOptions & { }; export class OidcAuthProvider implements OAuthHandlers { - readonly _implementation: Promise; + private readonly implementation: Promise; constructor(options: OidcAuthProviderOptions) { - this._implementation = this.setupStrategy(options); + this.implementation = this.setupStrategy(options); } async start(req: OAuthStartRequest): Promise { - const { strategy } = await this._implementation; + const { strategy } = await this.implementation; return await executeRedirectStrategy(req, strategy, { accessType: 'offline', prompt: 'none', @@ -73,7 +73,7 @@ export class OidcAuthProvider implements OAuthHandlers { async handler( req: express.Request, ): Promise<{ response: OAuthResponse; refreshToken: string }> { - const { strategy } = await this._implementation; + const { strategy } = await this.implementation; const { response, privateInfo } = await executeFrameHandlerStrategy< OAuthResponse, PrivateInfo @@ -86,7 +86,7 @@ export class OidcAuthProvider implements OAuthHandlers { } async refresh(req: OAuthRefreshRequest): Promise { - const { client } = await this._implementation; + const { client } = await this.implementation; const tokenset = await client.refresh(req.refreshToken); if (!tokenset.access_token) { throw new Error('Refresh failed'); From 9c9d12e8993bd196165578abb083c883d29e3d96 Mon Sep 17 00:00:00 2001 From: Brian Leathem Date: Tue, 24 Nov 2020 11:30:25 -0800 Subject: [PATCH 84/94] Added back the id_token when calling refresh --- plugins/auth-backend/src/providers/oidc/provider.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 4f2054c5f6..fbf38c69c4 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -97,7 +97,8 @@ export class OidcAuthProvider implements OAuthHandlers { providerInfo: { accessToken: tokenset.access_token, refreshToken: tokenset.refresh_token, - expiresInSeconds: tokenset.expires_at, + expiresInSeconds: tokenset.expires_in, + idToken: tokenset.id_token, scope: tokenset.scope || '', }, profile, From b467106c1ffe7f60c2376764ca66429446605537 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Wed, 25 Nov 2020 09:49:35 +0100 Subject: [PATCH 85/94] Reformat EntityPage --- packages/app/src/components/catalog/EntityPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index d30de6abd1..068019c622 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -47,7 +47,7 @@ import { EmbeddedRouter as LighthouseRouter, isPluginApplicableToEntity as isLighthouseAvailable, LastLighthouseAuditCard, -} from '@backstage/plugin-lighthouse/'; +} from '@backstage/plugin-lighthouse'; import { Router as SentryRouter } from '@backstage/plugin-sentry'; import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs'; import { Button, Grid } from '@material-ui/core'; From 8fd5e64351c2f25a821dadfd0816958fbe6ceb53 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 25 Nov 2020 09:52:19 +0100 Subject: [PATCH 86/94] TechDocs: Tell users when index.md is missing (better error message) (#3429) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * TechDocs: Logger already prints the name of plugin * TechDocs: Display a custom error message if provided * TechDocs: throw custom error message if index.md is not present * Language improvements. Thanks @freben Co-authored-by: Fredrik Adelöw Co-authored-by: Fredrik Adelöw --- .../src/techdocs/stages/publish/local.ts | 6 ++---- plugins/techdocs/src/api.ts | 8 +++++++- plugins/techdocs/src/reader/components/Reader.tsx | 2 +- .../reader/components/TechDocsNotFound.test.tsx | 14 ++++++++++++++ .../src/reader/components/TechDocsNotFound.tsx | 8 ++++++-- 5 files changed, 30 insertions(+), 8 deletions(-) diff --git a/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts b/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts index 07cef2f4d2..464c4b90a0 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts @@ -53,9 +53,7 @@ export class LocalPublish implements PublisherBase { ); if (!fs.existsSync(publishDir)) { - this.logger.info( - `[TechDocs]: Could not find ${publishDir}, creates the directory.`, - ); + this.logger.info(`Could not find ${publishDir}, creating the directory.`); fs.mkdirSync(publishDir, { recursive: true }); } @@ -63,7 +61,7 @@ export class LocalPublish implements PublisherBase { fs.copy(directory, publishDir, err => { if (err) { this.logger.debug( - `[TechDocs]: Failed to copy docs from ${directory} to ${publishDir}`, + `Failed to copy docs from ${directory} to ${publishDir}`, ); reject(err); } diff --git a/plugins/techdocs/src/api.ts b/plugins/techdocs/src/api.ts index 194ad14d30..7281ebdf13 100644 --- a/plugins/techdocs/src/api.ts +++ b/plugins/techdocs/src/api.ts @@ -95,7 +95,13 @@ export class TechDocsStorageApi implements TechDocsStorage { ); if (request.status === 404) { - throw new Error('Page not found'); + let errorMessage = 'Page not found. '; + // path is empty for the home page of an entity's docs site + if (!path) { + errorMessage += + 'This could be because there is no index.md file in the root of the docs directory of this repository.'; + } + throw new Error(errorMessage); } return request.text(); diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 785c6f1c6b..fe3a34564c 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -153,7 +153,7 @@ export const Reader = ({ entityId, onReady }: Props) => { ]); if (error) { - return ; + return ; } return ( diff --git a/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx b/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx index 7b0ec80619..d3c98fec78 100644 --- a/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx @@ -27,3 +27,17 @@ describe('', () => { expect(rendered.getByTestId('go-back-link')).toBeDefined(); }); }); + +describe('', () => { + it('should render with status code, custom error message and go back link', () => { + const rendered = render( + wrapInTestApp( + , + ), + ); + rendered.getByText(/This is a custom error message/i); + rendered.getByText(/404/i); + rendered.getByText(/Looks like someone dropped the mic!/i); + expect(rendered.getByTestId('go-back-link')).toBeDefined(); + }); +}); diff --git a/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx b/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx index c774000bd5..744242e343 100644 --- a/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx @@ -17,11 +17,15 @@ import React from 'react'; import { ErrorPage } from '@backstage/core'; -export const TechDocsNotFound = () => { +type Props = { + errorMessage?: string; +}; + +export const TechDocsNotFound = ({ errorMessage }: Props) => { return ( ); From fae3fafcfa3edcea32c18a53097cd46908161c8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 25 Nov 2020 09:57:16 +0100 Subject: [PATCH 87/94] silence some new ts squigglies in the editor (#3435) --- packages/cli/src/lib/bundler/server.ts | 2 +- packages/cli/src/lib/run.ts | 2 +- .../src/apis/implementations/OAuthRequestApi/MockOAuthApi.ts | 4 ++-- .../src/apis/implementations/StorageApi/WebStorage.test.ts | 4 ++-- packages/e2e-test/src/lib/helpers.ts | 4 ++-- .../src/testUtils/apis/StorageApi/MockStorageApi.test.ts | 4 ++-- plugins/auth-backend/src/providers/microsoft/provider.ts | 4 ++-- plugins/catalog-backend/src/util/runPeriodically.ts | 2 +- .../src/scaffolder/stages/templater/helpers.ts | 5 +++-- .../techdocs-backend/src/techdocs/stages/generate/helpers.ts | 4 ++-- 10 files changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index d198f5507c..a5cdd1300c 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -60,7 +60,7 @@ export async function serveBundle(options: ServeOptions) { proxy: pkg.proxy, }); - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { server.listen(port, url.hostname, (err?: Error) => { if (err) { reject(err); diff --git a/packages/cli/src/lib/run.ts b/packages/cli/src/lib/run.ts index 69d3d92640..571c75a39d 100644 --- a/packages/cli/src/lib/run.ts +++ b/packages/cli/src/lib/run.ts @@ -102,7 +102,7 @@ export async function waitForExit( return; } - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { child.once('error', error => reject(error)); child.once('exit', code => { if (code) { diff --git a/packages/core-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.ts index 5d539dca27..523f713f45 100644 --- a/packages/core-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.ts +++ b/packages/core-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.ts @@ -31,7 +31,7 @@ export default class MockOAuthApi implements OAuthRequestApi { async triggerAll() { await Promise.resolve(); // Wait a tick to allow new requests to get forwarded - return new Promise(resolve => { + return new Promise(resolve => { const subscription = this.authRequest$().subscribe(requests => { subscription.unsubscribe(); Promise.all(requests.map(request => request.trigger())).then(() => @@ -44,7 +44,7 @@ export default class MockOAuthApi implements OAuthRequestApi { async rejectAll() { await Promise.resolve(); // Wait a tick to allow new requests to get forwarded - return new Promise(resolve => { + return new Promise(resolve => { const subscription = this.authRequest$().subscribe(requests => { subscription.unsubscribe(); requests.map(request => request.reject()); diff --git a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts index 971b023629..81140ffcc9 100644 --- a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts +++ b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts @@ -63,7 +63,7 @@ describe('WebStorage Storage API', () => { const selectedKeyNextHandler = jest.fn(); const mockData = { hello: 'im a great new value' }; - await new Promise(resolve => { + await new Promise(resolve => { storage.observe$('correctKey').subscribe({ next: (...args) => { selectedKeyNextHandler(...args); @@ -93,7 +93,7 @@ describe('WebStorage Storage API', () => { storage.set('correctKey', mockData); - await new Promise(resolve => { + await new Promise(resolve => { storage.observe$('correctKey').subscribe({ next: (...args) => { selectedKeyNextHandler(...args); diff --git a/packages/e2e-test/src/lib/helpers.ts b/packages/e2e-test/src/lib/helpers.ts index aded184f5c..0b05eaf4b4 100644 --- a/packages/e2e-test/src/lib/helpers.ts +++ b/packages/e2e-test/src/lib/helpers.ts @@ -93,7 +93,7 @@ export function exitWithError(err: Error & { code?: unknown }) { */ export function waitFor(fn: () => boolean, maxSeconds: number = 120) { let count = 0; - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const handle = setInterval(() => { if (count++ > maxSeconds * 10) { reject(new Error('Timed out while waiting for condition')); @@ -112,7 +112,7 @@ export async function waitForExit(child: ChildProcess) { if (child.exitCode !== null) { throw new Error(`Child already exited with code ${child.exitCode}`); } - await new Promise((resolve, reject) => + await new Promise((resolve, reject) => child.once('exit', code => { if (code) { reject(new Error(`Child exited with code ${code}`)); diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts index 668d973830..7c92c71460 100644 --- a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts +++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts @@ -58,7 +58,7 @@ describe('WebStorage Storage API', () => { const selectedKeyNextHandler = jest.fn(); const mockData = { hello: 'im a great new value' }; - await new Promise(resolve => { + await new Promise(resolve => { storage.observe$('correctKey').subscribe({ next: (...args) => { selectedKeyNextHandler(...args); @@ -88,7 +88,7 @@ describe('WebStorage Storage API', () => { storage.set('correctKey', mockData); - await new Promise(resolve => { + await new Promise(resolve => { storage.observe$('correctKey').subscribe({ next: (...args) => { selectedKeyNextHandler(...args); diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index baa66f0662..a09d5871d2 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -163,7 +163,7 @@ export class MicrosoftAuthProvider implements OAuthHandlers { }); } - private getUserPhoto(accessToken: string): Promise { + private getUserPhoto(accessToken: string): Promise { return new Promise(resolve => { got .get('https://graph.microsoft.com/v1.0/me/photos/48x48/$value', { @@ -184,7 +184,7 @@ export class MicrosoftAuthProvider implements OAuthHandlers { `Could not retrieve user profile photo from Microsoft Graph API: ${error}`, ); // User profile photo is optional, ignore errors and resolve undefined - resolve(); + resolve(undefined); }); }); } diff --git a/plugins/catalog-backend/src/util/runPeriodically.ts b/plugins/catalog-backend/src/util/runPeriodically.ts index 2f65d0e6f3..c6d478fcbe 100644 --- a/plugins/catalog-backend/src/util/runPeriodically.ts +++ b/plugins/catalog-backend/src/util/runPeriodically.ts @@ -27,7 +27,7 @@ export function runPeriodically(fn: () => any, delayMs: number): () => void { let cancel: () => void; let cancelled = false; - const cancellationPromise = new Promise(resolve => { + const cancellationPromise = new Promise(resolve => { cancel = () => { resolve(); cancelled = true; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts index e79f2cb066..645133a4ae 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts @@ -66,7 +66,7 @@ export const runCommand = async ({ args, logStream = new PassThrough(), }: RunCommandOptions) => { - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { const process = spawn(command, args); process.stdout.on('data', stream => { @@ -109,7 +109,7 @@ export const runDockerContainer = async ({ dockerClient, createOptions = {}, }: RunDockerContainerOptions) => { - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { dockerClient.pull(imageName, {}, (err, stream) => { if (err) return reject(err); stream.pipe(logStream, { end: false }); @@ -120,6 +120,7 @@ export const runDockerContainer = async ({ }); const userOptions: UserOptions = {}; + // @ts-ignore if (process.getuid && process.getgid) { // Files that are created inside the Docker container will be owned by // root on the host system on non Mac systems, because of reasons. Mainly the fact that diff --git a/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts b/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts index d68bb74aa6..d0ffa72f1c 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts @@ -63,7 +63,7 @@ export async function runDockerContainer({ ); } - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { dockerClient.pull(imageName, {}, (err, stream) => { if (err) return reject(err); stream.pipe(logStream, { end: false }); @@ -119,7 +119,7 @@ export const runCommand = async ({ options, logStream = new PassThrough(), }: RunCommandOptions) => { - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { const process = spawn(command, args, options); process.stdout.on('data', stream => { From f538e2c560d4a4d9172cf643cb90235f3ae01453 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 25 Nov 2020 10:33:45 +0100 Subject: [PATCH 88/94] cli: update versions:bump to install new deps for accepted version ranges --- .changeset/long-birds-rush.md | 5 ++ .../cli/src/commands/versions/bump.test.ts | 31 ++++++---- packages/cli/src/commands/versions/bump.ts | 59 +++++++++++++------ packages/cli/src/commands/versions/lint.ts | 12 +++- packages/cli/src/lib/versioning/Lockfile.ts | 13 ++++ 5 files changed, 86 insertions(+), 34 deletions(-) create mode 100644 .changeset/long-birds-rush.md diff --git a/.changeset/long-birds-rush.md b/.changeset/long-birds-rush.md new file mode 100644 index 0000000000..db4da34789 --- /dev/null +++ b/.changeset/long-birds-rush.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Make versions:bump install new versions of dependencies that were within the specified range diff --git a/packages/cli/src/commands/versions/bump.test.ts b/packages/cli/src/commands/versions/bump.test.ts index 687708b074..f4d69c52be 100644 --- a/packages/cli/src/commands/versions/bump.test.ts +++ b/packages/cli/src/commands/versions/bump.test.ts @@ -24,7 +24,7 @@ import bump from './bump'; import { withLogCollector } from '@backstage/test-utils'; const REGISTRY_VERSIONS: { [name: string]: string } = { - '@backstage/core': '1.0.7', + '@backstage/core': '1.0.6', '@backstage/theme': '2.0.0', }; @@ -36,30 +36,36 @@ const HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. const lockfileMock = `${HEADER} "@backstage/core@^1.0.5": version "1.0.6" - resolved "https://my-registry/a-1.0.01.tgz#abc123" - integrity sha512-xyz + dependencies: + "@backstage/core-api" "^1.0.6" "@backstage/core@^1.0.3": version "1.0.3" - resolved "https://my-registry/a-1.0.01.tgz#abc123" - integrity sha512-xyz + dependencies: + "@backstage/core-api" "^1.0.3" "@backstage/theme@^1.0.0": version "1.0.0" - resolved "https://my-registry/a-1.0.01.tgz#abc123" - integrity sha512-xyz + +"@backstage/core-api@^1.0.6": + version "1.0.6" + +"@backstage/core-api@^1.0.3": + version "1.0.3" `; +// This resulting lockfile isn't a real world example, since it doesn't include the package bumps const lockfileMockResult = `${HEADER} -"@backstage/core@^1.0.3", "@backstage/core@^1.0.5": +"@backstage/core-api@^1.0.3", "@backstage/core-api@^1.0.6": version "1.0.6" - resolved "https://my-registry/a-1.0.01.tgz#abc123" - integrity sha512-xyz + +"@backstage/core@^1.0.5": + version "1.0.6" + dependencies: + "@backstage/core-api" "^1.0.6" "@backstage/theme@^1.0.0": version "1.0.0" - resolved "https://my-registry/a-1.0.01.tgz#abc123" - integrity sha512-xyz `; describe('bump', () => { @@ -116,6 +122,7 @@ describe('bump', () => { 'Checking for updates of @backstage/theme', 'Checking for updates of @backstage/core', 'Some packages are outdated, updating', + 'Removing lockfile entry for @backstage/core@^1.0.3 to bump to 1.0.6', 'Bumping @backstage/theme in b to ^2.0.0', "Running 'yarn install' to install new versions", 'Removing duplicate dependencies from yarn.lock', diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index 00944c5db9..1883d87bac 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -24,6 +24,7 @@ import { fetchPackageInfo, Lockfile, } from '../../lib/versioning'; +import { includedFilter, forbiddenDuplicatesFilter } from './lint'; const DEP_TYPES = [ 'dependencies', @@ -39,12 +40,16 @@ type PkgVersionInfo = { }; export default async () => { + const lockfilePath = paths.resolveTargetRoot('yarn.lock'); + // First we discover all Backstage dependencies within our own repo const dependencyMap = await mapDependencies(paths.targetDir); - // Next check with the package registry what the latest version of all of those dependencies are - const targetVersions = new Map(); - await workerThreads(16, dependencyMap.keys(), async name => { + // Next check with the package registry to see which dependency ranges we need to bump + const versionBumps = new Map(); + // Track package versions that we want to remove from yarn.lock in order to trigger a bump + const unlocked = Array<{ name: string; range: string; latest: string }>(); + await workerThreads(16, dependencyMap.entries(), async ([name, pkgs]) => { console.log(`Checking for updates of ${name}`); const info = await fetchPackageInfo(name); const latest = info['dist-tags'].latest; @@ -52,38 +57,49 @@ export default async () => { throw new Error(`No latest version found for ${name}`); } - targetVersions.set(name, latest); - }); - - // Then figure out which local packages need to have their dependencies bumped - const versionBumps = new Map(); - for (const [name, pkgs] of dependencyMap) { - const targetVersion = targetVersions.get(name)!; for (const pkg of pkgs) { - if (semver.satisfies(targetVersion, pkg.range)) { + if (semver.satisfies(latest, pkg.range)) { + if (semver.minVersion(pkg.range)?.version !== latest) { + unlocked.push({ name, range: pkg.range, latest }); + } continue; } - versionBumps.set( pkg.name, (versionBumps.get(pkg.name) ?? []).concat({ name, location: pkg.location, - range: `^${targetVersion}`, // TODO(Rugvip): Option to use something else than ^? + range: `^${latest}`, // TODO(Rugvip): Option to use something else than ^? }), ); } - } + }); console.log(); // Write all discovered version bumps to package.json in this repo - if (versionBumps.size === 0) { + if (versionBumps.size === 0 && unlocked.length === 0) { console.log('All Backstage packages are up to date!'); } else { console.log('Some packages are outdated, updating'); console.log(); + if (unlocked.length > 0) { + const lockfile = await Lockfile.load(lockfilePath); + for (const { name, range, latest } of unlocked) { + // Don't bother removing lockfile entries if they're already on the correct version + const existingEntry = lockfile.get(name)?.find(e => e.range === range); + if (existingEntry?.version === latest) { + continue; + } + console.log( + `Removing lockfile entry for ${name}@${range} to bump to ${latest}`, + ); + lockfile.remove(name, range); + } + await lockfile.save(); + } + await workerThreads(16, versionBumps.entries(), async ([name, deps]) => { const pkgPath = resolvePath(deps[0].location, 'package.json'); const pkgJson = await fs.readJson(pkgPath); @@ -110,9 +126,9 @@ export default async () => { console.log(); // Finally we make sure the new lockfile doesn't have any duplicates - const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); + const lockfile = await Lockfile.load(lockfilePath); const result = lockfile.analyze({ - filter: name => dependencyMap.has(name), + filter: includedFilter, }); if (result.newVersions.length > 0) { @@ -130,9 +146,14 @@ export default async () => { console.log(); } - if (result.newRanges.length > 0) { + const forbiddenNewRanges = result.newRanges.filter(({ name }) => + forbiddenDuplicatesFilter(name), + ); + if (forbiddenNewRanges.length > 0) { throw new Error( - `Version bump failed for ${result.newRanges.map(i => i.name).join(', ')}`, + `Version bump failed for ${forbiddenNewRanges + .map(i => i.name) + .join(', ')}`, ); } }; diff --git a/packages/cli/src/commands/versions/lint.ts b/packages/cli/src/commands/versions/lint.ts index e2c36ba094..aa0bcd6f6a 100644 --- a/packages/cli/src/commands/versions/lint.ts +++ b/packages/cli/src/commands/versions/lint.ts @@ -22,6 +22,9 @@ import partition from 'lodash/partition'; // Packages that we try to avoid duplicates for const INCLUDED = [/^@backstage\//]; +export const includedFilter = (name: string) => + INCLUDED.some(pattern => pattern.test(name)); + // Packages that are not allowed to have any duplicates const FORBID_DUPLICATES = [ /^@backstage\/core$/, @@ -29,6 +32,9 @@ const FORBID_DUPLICATES = [ /^@backstage\/plugin-/, ]; +export const forbiddenDuplicatesFilter = (name: string) => + FORBID_DUPLICATES.some(pattern => pattern.test(name)); + export default async (cmd: Command) => { const fix = Boolean(cmd.fix); @@ -36,7 +42,7 @@ export default async (cmd: Command) => { const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); const result = lockfile.analyze({ - filter: name => INCLUDED.some(pattern => pattern.test(name)), + filter: includedFilter, }); logArray( @@ -53,7 +59,7 @@ export default async (cmd: Command) => { newVersionsForbidden, newVersionsAllowed, ] = partition(result.newVersions, ({ name }) => - FORBID_DUPLICATES.some(pattern => pattern.test(name)), + forbiddenDuplicatesFilter(name), ); if (newVersionsForbidden.length && !fix) { success = false; @@ -75,7 +81,7 @@ export default async (cmd: Command) => { const [newRangesForbidden, newRangesAllowed] = partition( result.newRanges, - ({ name }) => FORBID_DUPLICATES.some(pattern => pattern.test(name)), + ({ name }) => forbiddenDuplicatesFilter(name), ); if (newRangesForbidden.length) { success = false; diff --git a/packages/cli/src/lib/versioning/Lockfile.ts b/packages/cli/src/lib/versioning/Lockfile.ts index 893beca6e2..f9bc789d95 100644 --- a/packages/cli/src/lib/versioning/Lockfile.ts +++ b/packages/cli/src/lib/versioning/Lockfile.ts @@ -202,6 +202,19 @@ export class Lockfile { return result; } + remove(name: string, range: string): boolean { + const query = `${name}@${range}`; + const existed = Boolean(this.data[query]); + delete this.data[query]; + + const newEntries = this.packages.get(name)?.filter(e => e.range !== range); + if (newEntries) { + this.packages.set(name, newEntries); + } + + return existed; + } + /** Modifies the lockfile by bumping packages to the suggested versions */ replaceVersions(results: AnalyzeResultNewVersion[]) { for (const { name, range, oldVersion, newVersion } of results) { From 2daf18e8095c86ed71b0f059e9b895f3cdf7d6ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 25 Nov 2020 10:51:22 +0100 Subject: [PATCH 89/94] catalog: emit all common relations based on specs (#3408) --- .changeset/pink-goats-sort.md | 6 + packages/catalog-model/src/entity/ref.ts | 16 +- packages/catalog-model/src/kinds/relations.ts | 2 + .../20201123205611_relations_table_uniq.js | 93 +++++++++ plugins/catalog-backend/package.json | 2 +- .../src/database/CommonDatabase.ts | 17 +- .../BuiltinKindsEntityProcessor.test.ts | 180 ++++++++++++++++++ .../processors/BuiltinKindsEntityProcessor.ts | 128 ++++++++++++- .../processors/OwnerRelationProcessor.ts | 74 ------- .../src/ingestion/processors/index.ts | 3 +- .../src/service/CatalogBuilder.test.ts | 2 +- .../src/service/CatalogBuilder.ts | 6 +- 12 files changed, 437 insertions(+), 92 deletions(-) create mode 100644 .changeset/pink-goats-sort.md create mode 100644 plugins/catalog-backend/migrations/20201123205611_relations_table_uniq.js delete mode 100644 plugins/catalog-backend/src/ingestion/processors/OwnerRelationProcessor.ts diff --git a/.changeset/pink-goats-sort.md b/.changeset/pink-goats-sort.md new file mode 100644 index 0000000000..828cccba84 --- /dev/null +++ b/.changeset/pink-goats-sort.md @@ -0,0 +1,6 @@ +--- +'@backstage/catalog-model': patch +'@backstage/plugin-catalog-backend': patch +--- + +Start emitting all known relation types from the core entity kinds, based on their spec data. diff --git a/packages/catalog-model/src/entity/ref.ts b/packages/catalog-model/src/entity/ref.ts index 2cacfce53a..9f6f175d8d 100644 --- a/packages/catalog-model/src/entity/ref.ts +++ b/packages/catalog-model/src/entity/ref.ts @@ -84,6 +84,14 @@ export function parseEntityName( * @param context The context of defaults that the parsing happens within * @returns The compound form of the reference */ +export function parseEntityRef( + ref: EntityRef, + context?: { defaultKind: string; defaultNamespace: string }, +): { + kind: string; + namespace: string; + name: string; +}; export function parseEntityRef( ref: EntityRef, context?: { defaultKind: string }, @@ -100,14 +108,6 @@ export function parseEntityRef( namespace: string; name: string; }; -export function parseEntityRef( - ref: EntityRef, - context?: { defaultKind: string; defaultNamespace: string }, -): { - kind: string; - namespace: string; - name: string; -}; export function parseEntityRef( ref: EntityRef, context: EntityRefContext = {}, diff --git a/packages/catalog-model/src/kinds/relations.ts b/packages/catalog-model/src/kinds/relations.ts index 846313ec8c..3d5d629b9e 100644 --- a/packages/catalog-model/src/kinds/relations.ts +++ b/packages/catalog-model/src/kinds/relations.ts @@ -33,7 +33,9 @@ export const RELATION_OWNER_OF = 'ownerOf'; * A relation with an API entity, typically from a component or system */ export const RELATION_CONSUMES_API = 'consumesApi'; +export const RELATION_API_CONSUMED_BY = 'apiConsumedBy'; export const RELATION_PROVIDES_API = 'providesApi'; +export const RELATION_API_PROVIDED_BY = 'apiProvidedBy'; /** * A relation denoting a dependency on another entity. diff --git a/plugins/catalog-backend/migrations/20201123205611_relations_table_uniq.js b/plugins/catalog-backend/migrations/20201123205611_relations_table_uniq.js new file mode 100644 index 0000000000..a8cf62b4c4 --- /dev/null +++ b/plugins/catalog-backend/migrations/20201123205611_relations_table_uniq.js @@ -0,0 +1,93 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +/** + * @param {import('knex')} knex + */ +exports.up = async function up(knex) { + if (knex.client.config.client === 'sqlite3') { + // sqlite doesn't support dropPrimary so we recreate it properly instead + await knex.schema.dropTable('entities_relations'); + await knex.schema.createTable('entities_relations', table => { + table.comment('All relations between entities in the catalog'); + table + .uuid('originating_entity_id') + .references('id') + .inTable('entities') + .onDelete('CASCADE') + .notNullable() + .comment('The entity that provided the relation'); + table + .string('source_full_name') + .notNullable() + .comment('The full name of the source entity of the relation'); + table + .string('type') + .notNullable() + .comment('The type of the relation between the entities'); + table + .string('target_full_name') + .notNullable() + .comment('The full name of the target entity of the relation'); + table.index('source_full_name', 'source_full_name_idx'); + }); + } else { + await knex.schema.alterTable('entities_relations', table => { + table.dropPrimary(); + table.index('source_full_name', 'source_full_name_idx'); + }); + } +}; + +/** + * @param {import('knex')} knex + */ +exports.down = async function down(knex) { + if (knex.client.config.client === 'sqlite3') { + await knex.schema.dropTable('entities_relations'); + await knex.schema.createTable('entities_relations', table => { + table.comment('All relations between entities in the catalog'); + table + .uuid('originating_entity_id') + .references('id') + .inTable('entities') + .onDelete('CASCADE') + .notNullable() + .comment('The entity that provided the relation'); + table + .string('source_full_name') + .notNullable() + .comment('The full name of the source entity of the relation'); + table + .string('type') + .notNullable() + .comment('The type of the relation between the entities'); + table + .string('target_full_name') + .notNullable() + .comment('The full name of the target entity of the relation'); + + table.primary(['source_full_name', 'type', 'target_full_name']); + }); + } else { + await knex.schema.alterTable('entities_relations', table => { + table.dropIndex([], 'source_full_name_idx'); + table.primary(['source_full_name', 'type', 'target_full_name']); + }); + } +}; diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 51a9978f30..26f7180d43 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -26,6 +26,7 @@ "@backstage/config": "^0.1.1", "@octokit/graphql": "^4.5.6", "@types/express": "^4.17.6", + "@types/ldapjs": "^1.0.9", "codeowners-utils": "^1.0.2", "core-js": "^3.6.5", "cross-fetch": "^3.0.6", @@ -51,7 +52,6 @@ "@backstage/test-utils": "^0.1.3", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", - "@types/ldapjs": "^1.0.9", "@types/lodash": "^4.14.151", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", diff --git a/plugins/catalog-backend/src/database/CommonDatabase.ts b/plugins/catalog-backend/src/database/CommonDatabase.ts index f86ab9e598..287756b0a8 100644 --- a/plugins/catalog-backend/src/database/CommonDatabase.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.ts @@ -345,7 +345,11 @@ export class CommonDatabase implements Database { ); // TODO(blam): translate constraint failures to sane NotFoundError instead - await tx.batchInsert('entities_relations', relationsRows, BATCH_SIZE); + await tx.batchInsert( + 'entities_relations', + deduplicateRelations(relationsRows), + BATCH_SIZE, + ); } async addLocation( @@ -506,7 +510,7 @@ export class CommonDatabase implements Database { .orderBy(['type', 'target_full_name']) .select(); - entity.relations = relations.map(r => ({ + entity.relations = deduplicateRelations(relations).map(r => ({ target: parseEntityName(r.target_full_name), type: r.type, })); @@ -517,3 +521,12 @@ export class CommonDatabase implements Database { }; } } + +function deduplicateRelations( + rows: DbEntitiesRelationsRow[], +): DbEntitiesRelationsRow[] { + return lodash.uniqBy( + rows, + r => `${r.source_full_name}:${r.target_full_name}:${r.type}`, + ); +} diff --git a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts index e1328e827a..94d35cc109 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts @@ -14,6 +14,12 @@ * limitations under the License. */ +import { + ApiEntity, + ComponentEntity, + GroupEntity, + UserEntity, +} from '@backstage/catalog-model'; import { BuiltinKindsEntityProcessor } from './BuiltinKindsEntityProcessor'; describe('BuiltinKindsEntityProcessor', () => { @@ -44,4 +50,178 @@ describe('BuiltinKindsEntityProcessor', () => { }, }); }); + + describe('postProcessEntity', () => { + const processor = new BuiltinKindsEntityProcessor(); + const location = { type: 'a', target: 'b' }; + const emit = jest.fn(); + + afterEach(() => jest.resetAllMocks()); + + it('generates relations for component entities', async () => { + const entity: ComponentEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { name: 'n' }, + spec: { + type: 'service', + owner: 'o', + lifecycle: 'l', + implementsApis: ['a'], + }, + }; + + await processor.postProcessEntity(entity, location, emit); + + expect(emit).toBeCalledTimes(4); + expect(emit).toBeCalledWith({ + type: 'relation', + relation: { + source: { kind: 'Group', namespace: 'default', name: 'o' }, + type: 'ownerOf', + target: { kind: 'Component', namespace: 'default', name: 'n' }, + }, + }); + expect(emit).toBeCalledWith({ + type: 'relation', + relation: { + source: { kind: 'Component', namespace: 'default', name: 'n' }, + type: 'ownedBy', + target: { kind: 'Group', namespace: 'default', name: 'o' }, + }, + }); + expect(emit).toBeCalledWith({ + type: 'relation', + relation: { + source: { kind: 'API', namespace: 'default', name: 'a' }, + type: 'apiProvidedBy', + target: { kind: 'Component', namespace: 'default', name: 'n' }, + }, + }); + expect(emit).toBeCalledWith({ + type: 'relation', + relation: { + source: { kind: 'Component', namespace: 'default', name: 'n' }, + type: 'providesApi', + target: { kind: 'API', namespace: 'default', name: 'a' }, + }, + }); + }); + + it('generates relations for api entities', async () => { + const entity: ApiEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'API', + metadata: { name: 'n' }, + spec: { + type: 'service', + owner: 'o', + lifecycle: 'l', + definition: 'd', + }, + }; + + await processor.postProcessEntity(entity, location, emit); + + expect(emit).toBeCalledTimes(2); + expect(emit).toBeCalledWith({ + type: 'relation', + relation: { + source: { kind: 'Group', namespace: 'default', name: 'o' }, + type: 'ownerOf', + target: { kind: 'API', namespace: 'default', name: 'n' }, + }, + }); + expect(emit).toBeCalledWith({ + type: 'relation', + relation: { + source: { kind: 'API', namespace: 'default', name: 'n' }, + type: 'ownedBy', + target: { kind: 'Group', namespace: 'default', name: 'o' }, + }, + }); + }); + + it('generates relations for user entities', async () => { + const entity: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { name: 'n' }, + spec: { + memberOf: ['g'], + }, + }; + + await processor.postProcessEntity(entity, location, emit); + + expect(emit).toBeCalledTimes(2); + expect(emit).toBeCalledWith({ + type: 'relation', + relation: { + source: { kind: 'User', namespace: 'default', name: 'n' }, + type: 'memberOf', + target: { kind: 'Group', namespace: 'default', name: 'g' }, + }, + }); + expect(emit).toBeCalledWith({ + type: 'relation', + relation: { + source: { kind: 'Group', namespace: 'default', name: 'g' }, + type: 'hasMember', + target: { kind: 'User', namespace: 'default', name: 'n' }, + }, + }); + }); + + it('generates relations for group entities', async () => { + const entity: GroupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'n' }, + spec: { + type: 't', + parent: 'p', + ancestors: [], + children: ['c'], + descendants: [], + }, + }; + + await processor.postProcessEntity(entity, location, emit); + + expect(emit).toBeCalledTimes(4); + expect(emit).toBeCalledWith({ + type: 'relation', + relation: { + source: { kind: 'Group', namespace: 'default', name: 'n' }, + type: 'childOf', + target: { kind: 'Group', namespace: 'default', name: 'p' }, + }, + }); + expect(emit).toBeCalledWith({ + type: 'relation', + relation: { + source: { kind: 'Group', namespace: 'default', name: 'p' }, + type: 'parentOf', + target: { kind: 'Group', namespace: 'default', name: 'n' }, + }, + }); + expect(emit).toBeCalledWith({ + type: 'relation', + relation: { + source: { kind: 'Group', namespace: 'default', name: 'c' }, + type: 'childOf', + target: { kind: 'Group', namespace: 'default', name: 'n' }, + }, + }); + expect(emit).toBeCalledWith({ + type: 'relation', + relation: { + source: { kind: 'Group', namespace: 'default', name: 'n' }, + type: 'parentOf', + target: { kind: 'Group', namespace: 'default', name: 'c' }, + }, + }); + }); + }); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts index 472dffeb16..a4e62e5b8c 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts @@ -15,15 +15,31 @@ */ import { + ApiEntity, apiEntityV1alpha1Validator, + ComponentEntity, componentEntityV1alpha1Validator, Entity, + getEntityName, + GroupEntity, groupEntityV1alpha1Validator, locationEntityV1alpha1Validator, + LocationSpec, + parseEntityRef, + RELATION_API_PROVIDED_BY, + RELATION_CHILD_OF, + RELATION_HAS_MEMBER, + RELATION_MEMBER_OF, + RELATION_OWNED_BY, + RELATION_OWNER_OF, + RELATION_PARENT_OF, + RELATION_PROVIDES_API, templateEntityV1alpha1Validator, + UserEntity, userEntityV1alpha1Validator, } from '@backstage/catalog-model'; -import { CatalogProcessor } from './types'; +import * as result from './results'; +import { CatalogProcessor, CatalogProcessorEmit } from './types'; export class BuiltinKindsEntityProcessor implements CatalogProcessor { private readonly validators = [ @@ -64,4 +80,114 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { return false; } + + async postProcessEntity( + entity: Entity, + _location: LocationSpec, + emit: CatalogProcessorEmit, + ): Promise { + const selfRef = getEntityName(entity); + + /* + * Utilities + */ + + function doEmit( + targets: string | string[] | undefined, + context: { defaultKind: string; defaultNamespace: string }, + outgoingRelation: string, + incomingRelation: string, + ): void { + if (!targets) { + return; + } + for (const target of [targets].flat()) { + const targetRef = parseEntityRef(target, context); + emit( + result.relation({ + source: selfRef, + type: outgoingRelation, + target: targetRef, + }), + ); + emit( + result.relation({ + source: targetRef, + type: incomingRelation, + target: selfRef, + }), + ); + } + } + + /* + * Emit relations for the Component kind + */ + + if (entity.kind === 'Component') { + const component = entity as ComponentEntity; + doEmit( + component.spec.owner, + { defaultKind: 'Group', defaultNamespace: selfRef.namespace }, + RELATION_OWNED_BY, + RELATION_OWNER_OF, + ); + doEmit( + component.spec.implementsApis, + { defaultKind: 'API', defaultNamespace: selfRef.namespace }, + RELATION_PROVIDES_API, + RELATION_API_PROVIDED_BY, + ); + } + + /* + * Emit relations for the API kind + */ + + if (entity.kind === 'API') { + const api = entity as ApiEntity; + doEmit( + api.spec.owner, + { defaultKind: 'Group', defaultNamespace: selfRef.namespace }, + RELATION_OWNED_BY, + RELATION_OWNER_OF, + ); + } + + /* + * Emit relations for the User kind + */ + + if (entity.kind === 'User') { + const user = entity as UserEntity; + doEmit( + user.spec.memberOf, + { defaultKind: 'Group', defaultNamespace: selfRef.namespace }, + RELATION_MEMBER_OF, + RELATION_HAS_MEMBER, + ); + } + + /* + * Emit relations for the Group kind + */ + + if (entity.kind === 'Group') { + const group = entity as GroupEntity; + doEmit( + group.spec.parent, + { defaultKind: 'Group', defaultNamespace: selfRef.namespace }, + RELATION_CHILD_OF, + RELATION_PARENT_OF, + ); + doEmit( + group.spec.children, + { defaultKind: 'Group', defaultNamespace: selfRef.namespace }, + RELATION_PARENT_OF, + RELATION_CHILD_OF, + ); + } + + return entity; + } } diff --git a/plugins/catalog-backend/src/ingestion/processors/OwnerRelationProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/OwnerRelationProcessor.ts deleted file mode 100644 index 4c67589789..0000000000 --- a/plugins/catalog-backend/src/ingestion/processors/OwnerRelationProcessor.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - Entity, - ENTITY_DEFAULT_NAMESPACE, - LocationSpec, - parseEntityRef, - ApiEntityV1alpha1, - ComponentEntityV1alpha1, - RELATION_OWNED_BY, - RELATION_OWNER_OF, - getEntityName, -} from '@backstage/catalog-model'; -import { CatalogProcessor, CatalogProcessorEmit } from './types'; -import * as result from './results'; - -const includedKinds = new Set(['api', 'component']); - -export class OwnerRelationProcessor implements CatalogProcessor { - async postProcessEntity( - entity: Entity, - _location: LocationSpec, - emit: CatalogProcessorEmit, - ): Promise { - if (!includedKinds.has(entity.kind.toLowerCase())) { - return entity; - } - const apiOrComponentEntity = entity as - | ApiEntityV1alpha1 - | ComponentEntityV1alpha1; - - const owner = apiOrComponentEntity.spec?.owner; - if (owner) { - const namespace = entity.metadata.namespace ?? ENTITY_DEFAULT_NAMESPACE; - - const selfRef = getEntityName(entity); - const ownerRef = parseEntityRef(owner, { - defaultKind: 'group', - defaultNamespace: namespace, - }); - - emit( - result.relation({ - source: selfRef, - type: RELATION_OWNED_BY, - target: ownerRef, - }), - ); - emit( - result.relation({ - source: ownerRef, - type: RELATION_OWNER_OF, - target: selfRef, - }), - ); - } - - return entity; - } -} diff --git a/plugins/catalog-backend/src/ingestion/processors/index.ts b/plugins/catalog-backend/src/ingestion/processors/index.ts index 118f977125..49d4e03b64 100644 --- a/plugins/catalog-backend/src/ingestion/processors/index.ts +++ b/plugins/catalog-backend/src/ingestion/processors/index.ts @@ -22,10 +22,11 @@ export * from './types'; export { parseEntityYaml } from './util/parse'; export { AnnotateLocationEntityProcessor } from './AnnotateLocationEntityProcessor'; +export { BuiltinKindsEntityProcessor } from './BuiltinKindsEntityProcessor'; export { CodeOwnersProcessor } from './CodeOwnersProcessor'; export { FileReaderProcessor } from './FileReaderProcessor'; export { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor'; -export { OwnerRelationProcessor } from './OwnerRelationProcessor'; +export { LdapOrgReaderProcessor } from './LdapOrgReaderProcessor'; export { LocationRefProcessor } from './LocationEntityProcessor'; export { MicrosoftGraphOrgReaderProcessor } from './MicrosoftGraphOrgReaderProcessor'; export { PlaceholderProcessor } from './PlaceholderProcessor'; diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.test.ts b/plugins/catalog-backend/src/service/CatalogBuilder.test.ts index ee7c6f2246..58897a10a7 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.test.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.test.ts @@ -144,7 +144,7 @@ describe('CatalogBuilder', () => { owner: 'o', lifecycle: 'l', }, - relations: [], + relations: expect.anything(), }, ]); }); diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index 3f95dcd0d3..4be0f1799b 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -37,15 +37,16 @@ import { import { DatabaseManager } from '../database'; import { AnnotateLocationEntityProcessor, + BuiltinKindsEntityProcessor, CatalogProcessor, CodeOwnersProcessor, FileReaderProcessor, GithubOrgReaderProcessor, HigherOrderOperation, HigherOrderOperations, + LdapOrgReaderProcessor, LocationReaders, LocationRefProcessor, - OwnerRelationProcessor, MicrosoftGraphOrgReaderProcessor, PlaceholderProcessor, PlaceholderResolver, @@ -53,8 +54,6 @@ import { UrlReaderProcessor, } from '../ingestion'; import { CatalogRulesEnforcer } from '../ingestion/CatalogRules'; -import { BuiltinKindsEntityProcessor } from '../ingestion/processors/BuiltinKindsEntityProcessor'; -import { LdapOrgReaderProcessor } from '../ingestion/processors/LdapOrgReaderProcessor'; import { jsonPlaceholderResolver, textPlaceholderResolver, @@ -283,7 +282,6 @@ export class CatalogBuilder { new UrlReaderProcessor({ reader, logger }), new CodeOwnersProcessor({ reader }), new LocationRefProcessor(), - new OwnerRelationProcessor(), new AnnotateLocationEntityProcessor(), ); } From 3aa7efb3ff18078f8e236763d45225e48721ef8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 25 Nov 2020 10:51:35 +0100 Subject: [PATCH 90/94] backend-common: add support for passing false as a CSP field value, to drop it from the defaults (#3437) --- .changeset/few-kings-agree.md | 5 ++ packages/backend-common/config.d.ts | 11 +++- .../service/lib/ServiceBuilderImpl.test.ts | 36 +++++++++++++ .../src/service/lib/ServiceBuilderImpl.ts | 45 ++++++++++------ .../src/service/lib/config.test.ts | 51 +++++++++++++++++++ .../backend-common/src/service/lib/config.ts | 17 +++++-- 6 files changed, 143 insertions(+), 22 deletions(-) create mode 100644 .changeset/few-kings-agree.md create mode 100644 packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts create mode 100644 packages/backend-common/src/service/lib/config.test.ts diff --git a/.changeset/few-kings-agree.md b/.changeset/few-kings-agree.md new file mode 100644 index 0000000000..0019611503 --- /dev/null +++ b/.changeset/few-kings-agree.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Added support for passing false as a CSP field value, to drop it from the defaults in the backend diff --git a/packages/backend-common/config.d.ts b/packages/backend-common/config.d.ts index 7a134ac506..c4ec84ab62 100644 --- a/packages/backend-common/config.d.ts +++ b/packages/backend-common/config.d.ts @@ -79,8 +79,15 @@ export interface Config { optionsSuccessStatus?: number; }; - /** */ - csp?: object; + /** + * Content Security Policy options. + * + * The keys are the plain policy ID, e.g. "upgrade-insecure-requests". The + * values are on the format that the helmet library expects them, as an + * array of strings. There is also the special value false, which means to + * remove the default value that Backstage puts in place for that policy. + */ + csp?: { [policyId: string]: string[] | false }; }; /** Configuration for integrations towards various external repository provider systems */ diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts new file mode 100644 index 0000000000..cffb0a88de --- /dev/null +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { applyCspDirectives } from './ServiceBuilderImpl'; + +describe('ServiceBuilderImpl', () => { + describe('applyCspDirectives', () => { + it('copies actual values', () => { + const result = applyCspDirectives({ key: ['value'] }); + expect(result).toEqual( + expect.objectContaining({ + 'default-src': ["'self'"], + key: ['value'], + }), + ); + }); + + it('removes false value keys', () => { + const result = applyCspDirectives({ 'upgrade-insecure-requests': false }); + expect(result!['upgrade-insecure-requests']).toBeUndefined(); + }); + }); +}); diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index 710de6f1d7..d8b9ffa2d9 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -18,7 +18,7 @@ import { Config } from '@backstage/config'; import compression from 'compression'; import cors from 'cors'; import express, { Router } from 'express'; -import helmet from 'helmet'; +import helmet, { HelmetOptions } from 'helmet'; import * as http from 'http'; import stoppable from 'stoppable'; import { Logger } from 'winston'; @@ -56,7 +56,7 @@ const DEFAULT_CSP = { 'script-src': ["'self'"], 'script-src-attr': ["'none'"], 'style-src': ["'self'", 'https:', "'unsafe-inline'"], - 'upgrade-insecure-requests': [], + 'upgrade-insecure-requests': [] as string[], }; export class ServiceBuilderImpl implements ServiceBuilder { @@ -64,7 +64,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { private host: string | undefined; private logger: Logger | undefined; private corsOptions: cors.CorsOptions | undefined; - private cspOptions: CspOptions | undefined; + private cspOptions: Record | undefined; private httpsSettings: HttpsSettings | undefined; private enableMetrics: boolean = true; private routers: [string, Router][]; @@ -154,20 +154,11 @@ export class ServiceBuilderImpl implements ServiceBuilder { host, logger, corsOptions, - cspOptions, httpsSettings, + helmetOptions, } = this.getOptions(); - app.use( - helmet({ - contentSecurityPolicy: { - directives: { - ...DEFAULT_CSP, - ...cspOptions, - }, - }, - }), - ); + app.use(helmet(helmetOptions)); if (corsOptions) { app.use(cors(corsOptions)); } @@ -214,16 +205,38 @@ export class ServiceBuilderImpl implements ServiceBuilder { host: string; logger: Logger; corsOptions?: cors.CorsOptions; - cspOptions?: CspOptions; httpsSettings?: HttpsSettings; + helmetOptions: HelmetOptions; } { return { port: this.port ?? DEFAULT_PORT, host: this.host ?? DEFAULT_HOST, logger: this.logger ?? getRootLogger(), corsOptions: this.corsOptions, - cspOptions: this.cspOptions, httpsSettings: this.httpsSettings, + helmetOptions: { + contentSecurityPolicy: { + directives: applyCspDirectives(this.cspOptions), + }, + }, }; } } + +export function applyCspDirectives( + directives: Record | undefined, +): CspOptions | undefined { + const result: CspOptions = { ...DEFAULT_CSP }; + + if (directives) { + for (const [key, value] of Object.entries(directives)) { + if (value === false) { + delete result[key]; + } else { + result[key] = value; + } + } + } + + return result; +} diff --git a/packages/backend-common/src/service/lib/config.test.ts b/packages/backend-common/src/service/lib/config.test.ts new file mode 100644 index 0000000000..8a35f147ca --- /dev/null +++ b/packages/backend-common/src/service/lib/config.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigReader } from '@backstage/config'; +import { readCspOptions } from './config'; + +describe('config', () => { + describe('readCspOptions', () => { + it('reads valid values', () => { + const config = ConfigReader.fromConfigs([ + { context: '', data: { csp: { key: ['value'] } } }, + ]); + expect(readCspOptions(config)).toEqual( + expect.objectContaining({ + key: ['value'], + }), + ); + }); + + it('accepts false', () => { + const config = ConfigReader.fromConfigs([ + { context: '', data: { csp: { key: false } } }, + ]); + expect(readCspOptions(config)).toEqual( + expect.objectContaining({ + key: false, + }), + ); + }); + + it('rejects invalid value types', () => { + const config = ConfigReader.fromConfigs([ + { context: '', data: { csp: { key: [4] } } }, + ]); + expect(() => readCspOptions(config)).toThrow(/wanted string-array/); + }); + }); +}); diff --git a/packages/backend-common/src/service/lib/config.ts b/packages/backend-common/src/service/lib/config.ts index bfd966b499..541170b704 100644 --- a/packages/backend-common/src/service/lib/config.ts +++ b/packages/backend-common/src/service/lib/config.ts @@ -134,24 +134,33 @@ export function readCorsOptions(config: Config): CorsOptions | undefined { * Attempts to read a CSP options object from the root of a config object. * * @param config The root of a backend config object - * @returns A CSP options object, or undefined if not specified + * @returns A CSP options object, or undefined if not specified. Values can be + * false as well, which means to remove the default behavior for that + * key. * * @example * ```yaml * backend: * csp: * connect-src: ["'self'", 'http:', 'https:'] + * upgrade-insecure-requests: false * ``` */ -export function readCspOptions(config: Config): CspOptions | undefined { +export function readCspOptions( + config: Config, +): Record | undefined { const cc = config.getOptionalConfig('csp'); if (!cc) { return undefined; } - const result: CspOptions = {}; + const result: Record = {}; for (const key of cc.keys()) { - result[key] = cc.getStringArray(key); + if (cc.get(key) === false) { + result[key] = false; + } else { + result[key] = cc.getStringArray(key); + } } return result; From 76ff21c2d5c02e34357143a350820b8eb77075a0 Mon Sep 17 00:00:00 2001 From: Askar Date: Wed, 25 Nov 2020 11:06:39 +0100 Subject: [PATCH 91/94] fix(search): compile possible filter options from available entities (#3370) * load filters from entities * fix(search): compile possible filter options from available entities * rename filter state and add text in case no filter can be applied --- .../search/src/components/Filters/Filters.tsx | 116 ++++++++++-------- .../components/SearchResult/SearchResult.tsx | 45 +++++-- 2 files changed, 97 insertions(+), 64 deletions(-) diff --git a/plugins/search/src/components/Filters/Filters.tsx b/plugins/search/src/components/Filters/Filters.tsx index 19d4e53d72..7bd344e12e 100644 --- a/plugins/search/src/components/Filters/Filters.tsx +++ b/plugins/search/src/components/Filters/Filters.tsx @@ -49,8 +49,14 @@ export type FiltersState = { checked: Array; }; +export type FilterOptions = { + kind: Array; + lifecycle: Array; +}; + type FiltersProps = { filters: FiltersState; + filterOptions: FilterOptions; resetFilters: () => void; updateSelected: (filter: string) => void; updateChecked: (filter: string) => void; @@ -58,16 +64,13 @@ type FiltersProps = { export const Filters = ({ filters, + filterOptions, resetFilters, updateSelected, updateChecked, }: FiltersProps) => { const classes = useStyles(); - // TODO: move mocked filters out of filters component to make it more generic - const filter1 = ['All', 'API', 'Component', 'Location', 'Template']; - const filter2 = ['deprecated', 'recommended', 'experimental', 'production']; - return ( - - Kind - - - - Lifecycle - - {filter2.map(filter => ( - updateChecked(filter)} - > - + + Filters cannot be applied to available results + + + )} + {filterOptions.kind.length > 0 && ( + + Kind + + + )} + {filterOptions.lifecycle.length > 0 && ( + + Lifecycle + + {filterOptions.lifecycle.map(filter => ( + updateChecked(filter)} + > + + + + ))} + + + )} ); }; diff --git a/plugins/search/src/components/SearchResult/SearchResult.tsx b/plugins/search/src/components/SearchResult/SearchResult.tsx index f1e160e792..9fd54ac06c 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.tsx @@ -126,7 +126,7 @@ export const SearchResult = ({ searchQuery }: SearchResultProps) => { const catalogApi = useApi(catalogApiRef); const [showFilters, toggleFilters] = useState(false); - const [filters, setFilters] = useState({ + const [selectedFilters, setSelectedFilters] = useState({ selected: 'All', checked: [], }); @@ -146,17 +146,18 @@ export const SearchResult = ({ searchQuery }: SearchResultProps) => { // apply filters // filter on selected - if (filters.selected !== 'All') { + if (selectedFilters.selected !== 'All') { withFilters = results.filter((result: Result) => - filters.selected.includes(result.kind), + selectedFilters.selected.includes(result.kind), ); } // filter on checked - if (filters.checked.length > 0) { + if (selectedFilters.checked.length > 0) { withFilters = withFilters.filter( (result: Result) => - result.lifecycle && filters.checked.includes(result.lifecycle), + result.lifecycle && + selectedFilters.checked.includes(result.lifecycle), ); } @@ -174,7 +175,7 @@ export const SearchResult = ({ searchQuery }: SearchResultProps) => { setFilteredResults(withFilters); } - }, [filters, searchQuery, results]); + }, [selectedFilters, searchQuery, results]); if (loading) { return ; } @@ -190,41 +191,58 @@ export const SearchResult = ({ searchQuery }: SearchResultProps) => { } const resetFilters = () => { - setFilters({ + setSelectedFilters({ selected: 'All', checked: [], }); }; const updateSelected = (filter: string) => { - setFilters(prevState => ({ + setSelectedFilters(prevState => ({ ...prevState, selected: filter, })); }; const updateChecked = (filter: string) => { - if (filters.checked.includes(filter)) { - setFilters(prevState => ({ + if (selectedFilters.checked.includes(filter)) { + setSelectedFilters(prevState => ({ ...prevState, checked: prevState.checked.filter(item => item !== filter), })); return; } - setFilters(prevState => ({ + setSelectedFilters(prevState => ({ ...prevState, checked: [...prevState.checked, filter], })); }; + const filterOptions = results.reduce( + (acc, curr) => { + if (curr.kind && acc.kind.indexOf(curr.kind) < 0) { + acc.kind.push(curr.kind); + } + if (curr.lifecycle && acc.lifecycle.indexOf(curr.lifecycle) < 0) { + acc.lifecycle.push(curr.lifecycle); + } + return acc; + }, + { + kind: [] as Array, + lifecycle: [] as Array, + }, + ); + return ( <> {showFilters && ( { searchQuery={searchQuery} numberOfResults={filteredResults.length} numberOfSelectedFilters={ - (filters.selected !== 'All' ? 1 : 0) + filters.checked.length + (selectedFilters.selected !== 'All' ? 1 : 0) + + selectedFilters.checked.length } handleToggleFilters={() => toggleFilters(!showFilters)} /> From 9b9e86f8afd3ec593aca08f0599a832c7b6748f7 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 25 Nov 2020 14:30:52 +0100 Subject: [PATCH 92/94] export oidcAuthRef --- .changeset/honest-gifts-push.md | 5 +++++ .github/styles/vocab.txt | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/honest-gifts-push.md diff --git a/.changeset/honest-gifts-push.md b/.changeset/honest-gifts-push.md new file mode 100644 index 0000000000..ad2df98a6e --- /dev/null +++ b/.changeset/honest-gifts-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-api': patch +--- + +export oidc provider diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index c31e19e35c..a0841fe7d0 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -131,6 +131,7 @@ npm nvm oauth Oauth +oidc Okta Oldsberg onboarding From a1f6e07d325e840eefc6f2c2b0ac252282f9467a Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 25 Nov 2020 14:32:49 +0100 Subject: [PATCH 93/94] created release log --- .changeset/honest-gifts-push.md | 5 ----- packages/core-api/CHANGELOG.md | 6 ++++++ packages/core-api/package.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 .changeset/honest-gifts-push.md diff --git a/.changeset/honest-gifts-push.md b/.changeset/honest-gifts-push.md deleted file mode 100644 index ad2df98a6e..0000000000 --- a/.changeset/honest-gifts-push.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-api': patch ---- - -export oidc provider diff --git a/packages/core-api/CHANGELOG.md b/packages/core-api/CHANGELOG.md index b936715c70..63926daf24 100644 --- a/packages/core-api/CHANGELOG.md +++ b/packages/core-api/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/core-api +## 0.2.2 + +### Patch Changes + +- 9b9e86f8a: export oidc provider + ## 0.2.1 ### Patch Changes diff --git a/packages/core-api/package.json b/packages/core-api/package.json index 212d4c9046..c39dca3b7a 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-api", "description": "Internal Core API used by Backstage plugins and apps", - "version": "0.2.1", + "version": "0.2.2", "private": false, "publishConfig": { "access": "public", From ff1301d28f1d5567ecf65ee21b67f45cd0ccba78 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Wed, 25 Nov 2020 16:52:29 +0100 Subject: [PATCH 94/94] Warn if app-backend can't start-up because missing dir --- .changeset/brave-rats-obey.md | 5 +++++ plugins/app-backend/src/service/router.ts | 12 +++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .changeset/brave-rats-obey.md diff --git a/.changeset/brave-rats-obey.md b/.changeset/brave-rats-obey.md new file mode 100644 index 0000000000..a09d922763 --- /dev/null +++ b/.changeset/brave-rats-obey.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app-backend': patch +--- + +Warn if the app-backend can't start-up because the static directory that should be served is unavailable. diff --git a/plugins/app-backend/src/service/router.ts b/plugins/app-backend/src/service/router.ts index b435ec15f1..65cbddaae6 100644 --- a/plugins/app-backend/src/service/router.ts +++ b/plugins/app-backend/src/service/router.ts @@ -21,6 +21,7 @@ import { Logger } from 'winston'; import { notFoundHandler, resolvePackagePath } from '@backstage/backend-common'; import { Config } from '@backstage/config'; import { injectConfig, readConfigs } from '../lib/config'; +import fs from 'fs-extra'; export interface RouterOptions { config: Config; @@ -35,9 +36,18 @@ export async function createRouter( const { config, logger, appPackageName, staticFallbackHandler } = options; const appDistDir = resolvePackagePath(appPackageName, 'dist'); - logger.info(`Serving static app content from ${appDistDir}`); const staticDir = resolvePath(appDistDir, 'static'); + if (!(await fs.pathExists(staticDir))) { + logger.warn( + `Can't serve static app content from ${staticDir}, directory doesn't exist`, + ); + + return Router(); + } + + logger.info(`Serving static app content from ${appDistDir}`); + const appConfigs = await readConfigs({ config, appDistDir,