From def640c0f54e38907287c042a55417e8729e8ece Mon Sep 17 00:00:00 2001 From: yongyiduan Date: Wed, 2 Nov 2022 10:39:09 +0800 Subject: [PATCH] v1.0.1 --- .gitignore | 17 + README.md | 58 +- img/command.png | Bin 0 -> 51188 bytes pom.xml | 107 ++++ .../tencent/bk/devops/atom/ScriptRunAtom.kt | 538 ++++++++++++++++++ .../bk/devops/atom/ScriptRunAtomTest.kt | 232 ++++++++ .../tencent/bk/devops/atom/api/QualityApi.kt | 43 ++ .../bk/devops/atom/common/Constants.kt | 42 ++ .../bk/devops/atom/common/ErrorCode.kt | 19 + .../bk/devops/atom/common/RunEnvHelper.kt | 156 +++++ .../atom/common/utils/ReplacementUtils.kt | 105 ++++ .../bk/devops/atom/enums/CharsetType.kt | 37 ++ .../tencent/bk/devops/atom/enums/OSType.kt | 43 ++ .../bk/devops/atom/pojo/AdditionalOptions.kt | 89 +++ .../tencent/bk/devops/atom/pojo/AgentEnv.kt | 34 ++ .../tencent/bk/devops/atom/pojo/BuildEnv.kt | 35 ++ .../bk/devops/atom/pojo/ScriptRunAtomParam.kt | 22 + .../atom/pojo/request/IndicatorCreate.kt | 65 +++ .../devops/atom/utils/CommandLineExecutor.kt | 194 +++++++ .../bk/devops/atom/utils/CommandLineUtils.kt | 286 ++++++++++ .../tencent/bk/devops/atom/utils/CommonEnv.kt | 68 +++ .../bk/devops/atom/utils/CommonUtil.kt | 45 ++ .../tencent/bk/devops/atom/utils/Constants.kt | 46 ++ .../bk/devops/atom/utils/ExecutorUtil.kt | 53 ++ .../atom/utils/OauthCredentialLineParser.kt | 50 ++ .../bk/devops/atom/utils/ScriptEnvUtils.kt | 178 ++++++ .../devops/atom/utils/SensitiveLineParser.kt | 51 ++ .../tencent/bk/devops/atom/utils/UUIDUtil.kt | 50 ++ .../bk/devops/atom/utils/script/BashUtil.kt | 282 +++++++++ .../devops/atom/utils/script/BatScriptUtil.kt | 203 +++++++ .../atom/utils/script/PowerShellUtil.kt | 162 ++++++ .../bk/devops/atom/utils/script/PwshUtil.kt | 175 ++++++ .../bk/devops/atom/utils/script/PythonUtil.kt | 265 +++++++++ .../bk/devops/atom/utils/script/ShUtil.kt | 273 +++++++++ .../com.tencent.bk.devops.atom.spi.TaskAtom | 1 + src/test/resources/.sdk.json | 9 + src/test/resources/input.json | 3 + task.json | 86 +++ 38 files changed, 4121 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 img/command.png create mode 100644 pom.xml create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/ScriptRunAtom.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/ScriptRunAtomTest.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/api/QualityApi.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/common/Constants.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/common/ErrorCode.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/common/RunEnvHelper.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/common/utils/ReplacementUtils.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/enums/CharsetType.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/enums/OSType.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/pojo/AdditionalOptions.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/pojo/AgentEnv.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/pojo/BuildEnv.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/pojo/ScriptRunAtomParam.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/pojo/request/IndicatorCreate.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/utils/CommandLineExecutor.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/utils/CommandLineUtils.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/utils/CommonEnv.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/utils/CommonUtil.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/utils/Constants.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/utils/ExecutorUtil.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/utils/OauthCredentialLineParser.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/utils/ScriptEnvUtils.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/utils/SensitiveLineParser.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/utils/UUIDUtil.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/utils/script/BashUtil.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/utils/script/BatScriptUtil.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/utils/script/PowerShellUtil.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/utils/script/PwshUtil.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/utils/script/PythonUtil.kt create mode 100644 src/main/kotlin/com/tencent/bk/devops/atom/utils/script/ShUtil.kt create mode 100644 src/main/resources/META-INF/services/com.tencent.bk.devops.atom.spi.TaskAtom create mode 100644 src/test/resources/.sdk.json create mode 100644 src/test/resources/input.json create mode 100644 task.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80e02dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +.idea/ +.gradle/ +build/ +target/ +out/ +release/ +reports/ +*.iml +.DS_Store +*.log +gradle/ +gradlew +gradlew.bat +version.txt +build.yml +.codecc +task.json diff --git a/README.md b/README.md index 8c5f1b0..7462671 100644 --- a/README.md +++ b/README.md @@ -1 +1,57 @@ -# run \ No newline at end of file +#### 插件功能 +在构建机上执行脚本: +- 默认情况使用Bash进行脚本执行 +- 可选解析器如图所示![command.png](./img/command.png) + +#### 适用场景 +执行编译脚本 + +支持通过如下方式设置当前步骤输出变量: +``` +echo "::set-output name=::" +``` +如: +``` +echo "::set-output name=release_type::dev" +``` +在下游步骤入参中: +- 通过 ${{ jobs..steps..outputs.release_type }} 引用此变量值 +- 为当前Job上配置的 Job ID +- 为当前 Task 上配置的 Step ID + +支持通过如下方式设置/修改流水线变量: +``` +echo "::set-variable name=::" +``` +如: +``` +echo "::set-variable name=a::1" +``` +在下游步骤入参中,通过 ${{ variables.a }} 方式引用此变量 + +#### 使用限制和受限解决方案[可选] +设置输出参数或者流水线变量时,**在当前步骤不会生效,在下游步骤才生效** + +#### 常见的失败原因和解决方案 +1. 脚本执行退出码非0时,当前步骤执行结果为失败,请检查脚本逻辑,或确认执行环境是否满足需求 + +#### 多行文本使用set-output/set-variable/set-gate-value +bash示例: +使用format_multiple_lines 替代echo输出 +``` +content=$(ls -l ..) +echo "$content" +format_multiple_lines "::set-output name=content_a::$content" +resultStr="\n" +resultStr="$resultStr\n$PATH" +resultStr="$resultStr\n$PATH" +resultStr="$resultStr\n$PATH" +echo resultStr=$resultStr +format_multiple_lines "::set-output name=content2_a::$resultStr" +``` + +python 示例: +``` +multiple_lines = "line one \n line two \n line three" +print("::set-output name=lines::{0}".format(format_multiple_lines(multiple_lines))) +``` \ No newline at end of file diff --git a/img/command.png b/img/command.png new file mode 100644 index 0000000000000000000000000000000000000000..78c21795675b6f28d4455cfae02c1e7ed01c5e15 GIT binary patch literal 51188 zcmdSAbySpX|L%GWY91u3}8yY@i58&Gk*B1&hSmgtB8^8~@t)x|@v9PLQ@zADsfS+-l6!l!OuqfFu zUpJn;VBN>U>JEP-trU&(0oqT2Z#2bK;V_ zd%Vl!d`U&ChNP91RlUI4iG`Jw2=3a&v*xj#N;s7Xw9X4fA^z9Z+_`XtPKJZ8_dMD( zuf_)kyEn2FPM=UhcPLwH1T3s3vRyB#YiiPi4lncPHr>;1kFhJd|Ld}i7?F(K4d6jf z^LksE_6(Fu{nuMP!u_HexBahcoV>_~e(sjuS)IYr03spN$bVp`cVNJQbl^1Jy=3MM z>C1G+sefH%51yY&G~t}fwlIAWn=hUzbND~->ec(U_3N3z55$|VGG>x9Gv`<{_BeL5 z>ql6N!P3AT|G5k_Gj_e)h+Z#DI1Gv7X}Y^#iq-Rri#ILf4z6jdl@ZQ5(#e|eKdat+`*r#S$O91+Wov<%+(hU zZaHLb+LLnSbn&oDpQ;74Q59uRDTc`0i1^KL{%>SG=inh}OS zpX=4a#Gf}wxvJW?EhPLu`xbg-h&>TFs(@H{ZS6)rH7bd9y>KL+kOlEurY|cP=eEkIf4+HKvpGME#-zyLjAr(D(iKP7=z( z06jB3X!q*)Wga6|D7F+O)MIkBcE*-$_ySE+Q)s&2Q4Q>jGR5CJ7aMFM^wy1}(Af8& z(-L?!2gt?LAf@T~f_r-3r<7%;>EuqnLG7SSIwEsmXqTqy3-p)1q8}xv%jO@ zhby`Y1TQUY+5A$HW)GVxl>Pea%XXFbgpo%-NzbDNi;6L_=c1oLPA&4W^A~$?e+mtP zPcb_CB6Dsdqzc)IU!P-@p}=^&vk0D3AAx?=lW{?y4ed@AIq3~HI@RUemCrcTg#LAO^nYy=->?A>z`;CxWCQM zdCVKZ^uG~ZUQo5|2d~D6?^;97*YjvhR1cHk=z`J#tEh(a%@V6_!t-&w%0 zpWVhHLuG1OhXzyEgH9?D2H*W}Dj@C)Kg3x(!`06W|0`6@(rwcNtT3%hscY1rRl8+# z@>T^LIv|?oTa_%bgm;A_NNascx_d`$Ws* z!Pltecr^!y<{j8-4|fF*nE`vV$h_}i)LshXd%9h950{GZ+yqV& z`K|~2V(y{XS|W4+4-ak&p0XAK$xhxo5yY1@lwF*2pDFVK4nmKh{qZ|{Bex;Bemwq` z%7`%YzOq>16N|@v$4}9+`06o^>6}q;OzE zLs7jZdj%Q)2q=iss!W7s;RV{;NXSZ`|kJPE5I7 zE^|mdSi;>w0Y{D}*HOih-ikPc#+*21DeHHgVEBRUR#`pY0v)hP{k-+F!;hhYyT3yp zWxEYOvp_F?T7k_t%xu?97-QC}WtF~DKi?*{lAzhM$@^+i z<>r}_wqav|Rto7ZXl=V@u)M8l(8l!kE%uS=qA?(_`UOmtxZP3W_)}e7w2re9CzIwY z)F)kBPj53KzS-9f$@iOv-q#MwY1yj~v?Xnyo>+>NGVahjX^OEAfY37~xFZZ0?0qa; zcN>;s(ph4Z&gRS_c`av{&sWBdkKtL86Gy9jlZhX>dTOkf2d26R^srp*?!i<8Kx$s;kAm>M*WRT`6 z;$&-{@QV8$EFY)?Nz8xOk!*=>h8b<5+fkxlxof1*3q-xfA4y#NO%Wi8auktQ3S#Cx@yBsQ2?-T5|QII@9r}s>?@6MuW=iVOlw7Gij z*2O8G0jxCW{v{zTy!Kuy?`z%Qj80o=(X+M3=1HXg^@)4C=R9|g3)3CaE{Tag{GP1% zK(rSI!>wQ+NE@}4k?MQ4Ubo+D`E-tuv`~I(NvGt8$E0bfmhqm$lA8ayrq@_2`#H7H z4!*6)(M)@g$#J9od&}J=wgJl_?W^41ck3XMrx#`km0nL82O$>;PEJQGM?~A*%IUGt z<`@&ywTgjgSLwz-H5y6h&x3Fb@*_(P0}9B3MFA`K*DN6KxRh^opa%V~Tu~7|R7^xX z=<=!8dHt_UrX5~SLR+R|HGO1j9)G~0bl&@UujM(teP4=jFlPCDQuf8?Dc~iVXDi=` zW?ojF3NZLD`IkK$&q4^^H|plEI1?_6`Om1M;z1+jPW!<#r7e^UrZ@YcpEU&5A3Kld zi$6!+F`GN+W|-fu?&a$zQ$3D2!_-}>i2>r z7iVEw1PCdh%Jj5@=#{8z*ab`dm~w_;Hbjz>&jZ@hv<@VSmp#RT$YU>E%WTMF{S&X7 zw#vB?l;8{*{fnrs?6l3*aJeHN|XG~7DCgHCrtb) z{A(qH)X!bDXWPw)p=`SkpLbrp+mC&`SxoAVJ~IRkHuNCUBmuqJwp!l2%eAD#OJ_dr z_Mhzp{M7=L={B;LpC2)m<+D3w=zn?qCm~`o;|evmAR8k3p>tAPw>t?(wy%rpjD39~ zg$0m1jY*j3y_P>L{%im3mx8H`IT_63d6A#kA=sY3yw-#b92zaepsT4?0}ugXkdSXf z^x-6Jg@3m9=I2i1slE6mAaB?ouII%L7@GE_wO5{KZ7ogR3xLp)kO>N1zzeud{Q_}iU5I9@;knXs!KBD|?_ z3Z!Tr1Q|2mgaalshjV02qIVqq)BQjn7DS22bT>l%%iCkBHX`|&tOxGOn~$?L)$&hw zU!KmJ$Qn*lIG~U#{ZEM`A1#@)ojnC^Y}7o~eCJKe%5@ z$4KjB)uL|C_oSFA46bA1_ttA&b~4Ib$Iw}BBhqGpx~9zJA;@f7 zlwkbVzv~n5e=7GW3!^rO>iNz4SS$aIQ{Yr?lkG$;ACT71i#3(yL(ByX>D&3Z%Gy~TN$MMYp%@8{`(}t zodz$X9qA@YJrBzkApylRoz9;!|9#E*X8zT57D_d&L6F52{-tZ5hI|27mgN*?;#mLRM0% z(IUUB-GNl`f^YtE*TX7iU}adDpLfk4lZ+mIvOBlVD{0>GSowGJuxcfBF;&{C4xw8z zq#cwzHLMxaHlTpV*QVD0@-At_w29~A`nF}H*Ouw_+S^N?i?Y%Vgy}&(ObACFg99LY ziQv$CzQPP!F3%fHmrm#X{cC{PBiXkMXbk&tIWpAIcw>V=&X_QC`gW%uhrw6C)pDVj z*KLD4N^t3)e)yl6gzb~{bg&6-lhpZ8Y{YN5HWCLRfAkUqChNlWfFqoKi%>Mj3z$}{ z21=J(Pk-6QNFcvk0v)MimM`)d9{idCeto$Y>7;boYs_iV(Jg)11+i2Ca zC4ml|G@{U`Px>b?_^9@pbUc#fzx^8@A&|R^X@j_Tr*5U;^p}r_V;gD} z*qQNcRG(FZVz_Q9YQLQ*eJMt~yi}rqkDUn(L4IdpGy0Rs*QBn!R@b}Zt^x@k7lX!u z{MFev6bw{Z0+u9RMZ-Cuy-l;q@+zbYOg8rZJoTn1Gpz!8Tw)&G~<)ukIHlP?84SgWet!W54U!HHeP(ZuNT|Yhiw`%N2fqy&ysD0(s&X zl1H|=2Mwv%3p!@mu+F&F?fQABgWTG2 z^CtOjQh%M$kaU z;*Ynx9cNqKSlDu_z1>`-#^@M18HU=!>Lgo0hZyhH(YSRjKjP;+PT7uR)#|=aA~w1**=gy0KKGf zf^UL*e*WsDURlZo`GX_*C=NOtke%+>=C%4I(aD}C)|!75=ThGIzAD{v;t>;_WxDh} zt4hrE1^n7)qx(Cz*T9MbbWn6D%Eizx^#sO-k9ePrUgqdjLo4djh82Ysb4iVn`0?H3 z-uOUZ9yk>8utUrQb5e?Z&AonT9|q!oGaeV)t8qY(1WQ9XQmLvRKj#iZh{?nW&8QO&|t%UO@hU72V+&gQdhb@mH zt}K#`z}%KMxwoI0;OmrV0|MP|F$5Tpy{=gEl|iy-x!c$U@dA0jRWpuwktJG#M?`{> zM>sQ%a6i|c2Stze;MY*GLR)h$Ss$j@j7Xo+_b{(l7WVy?lO`T{(O~_RcJrW6YWiT? zDGh_`;#PZ$P~n*#PNE0}8K+;A0iwh(|NTIQJ~};$#f;c}18#$#Pw|ge)N8(lZ3~va zVmDc?=)JpMj?#HtX+^Itdojed$%kkbwEVArUJATGb>r3$2eUIr1*n9H>FMHKG8cAS z5Fa4eIj*Vv6e1-+evxEs;X6q0R0dD0kRw;Q(W{Adi~)k@iDhFQVrUCfWdjJ~CK4;3 zUG&DH%X9dmf?+#cwuH0xgXJ(^)n-*Uy)F2c=aSe>T~#+*TOk_{q5%KiNd7$Qre^`|~^*xeOh z3kWEldL?^8-JV+}owfCz<*ux_i@0ww8-xeF-`XMwZQM286Pp3aXHgH;r7f}+LFSVrY+dSLAM4bzPe1#5f_b1W83hj83oXJ+>UnZ{ea#HZO-29_JOO%V`)J z4DOV%HxbwhaS;U%X3aXnFHfRkW5d0mfM6IA7tU!w_S#kQGau{898k^WYAxjiytK2b z8~O!d-i()^C_CCiKg3M3K*nd6kOUU%BhLT=dP8ccwNO6|;6Ep_w znW#QqylXV|X6EUQorB0|1>IKVR6;W7C*>r~MQfF2-6a0XmIb7@Ne&`dGx9;0HDvPdZ*^ z;o?;p$+*I35V;JwXitNMRI991KTzUM4iGEW{;zk!A)ptuC4#_=zQGMMW2A%(uRl(Lc0D>G+Lqe;ya1Ht!1k zdvy9sG8|_=YGLo+8lG1B@Juq^$KfhZ7Yif->PT}FayDu6^*TJR@b^0r_6ZPZe@$QY z!7}-%ZlO# z>y=XI`<-9z`NO$3-y?%SiUyj^#H7t;(@ij%0=1GDmO3Yr)9QB|qg_i1@Gu6`J&elo zX2M2iWh29N;6e8im^!Hc?ncYQB*-167jGuo^9^;zZ%jGBRA#e9C9Bg-?FE)QLtXSiGUxCfBngr|Eb zYuRvN#6t=<4~44*aZhAcN+}isfdYBSuw}gL4er9282@->v8rcmzl8=w%?H|IeB*F5 za6X0pXbsB7Xi70s?7uPg5$;n2WlW-@O2^^dAAmFP=`3-7=AMwBHdADbXq^{T0ovLL&TTCdd^~ln3Z8b)U>aFLrvb+9EV_8A|fQS?+p= z1%?G_Ji!^3pMQ@PcBc4?SfS>XQHf6C5TGWBS*7EA2j?Mwx%Be(+L%cDRa=QIvU@Q* zK7rb0KS6PmHoqWf2NSh28k=)58S!3%s+kVX6C;|23P6Z9gLReLzIp-0Vjmz%f;lSWrdtQgMwE zomZWY4py#mErq_2Ar;XLw}>Pv6MiJO9Tj|9QScAx{|R5gF#xq4h1wZ7e`g9!sNCR2 zHL%T?IAx%_%ZO~8J6x>f>}Z$GQ}&gA9dP)(;Wki5bsC6 z!wx+9;nOm_J9U_d{qN;{8*2jj(qY3Fgv)-qef(W#iUmE`mXg6C=o=O~Q8Fb}d(y;T z@$kAAj_xh>5Bdl++>Qegg8y=gNW=CgR#}fmJMK?abSMw86PvY9cav-2lHkoK;J*|N z^XedDPzRkrR`jQPPTNjftGyEvGwiJs4|259K}O4(+^a0WL8n)+7XsOqzPCu^o){vm zXjqIuE|eL%hn0hTmowY2y7$C)8}gdeN@Pb{?%S zIV9aYqC)TJb)IlnRIGD7KQ3H+yRtn)GEO0YqNy~_@`4dKV5Kx}6wo)<6UILFUU|%T z$}I%n+#xL%(z-3e@+Ktn?9$t&ol-M*l8rQN65S`IncRRub z4IC@)gbG@oZ*ZX@S7%E#GwZU_prK@_c1p#v73ES$q zm9yy}lPR?wG~>&pyz`32lmY20r)A|gWtj|P76v+vC5BHdXt%&g7%{RY;T5_TKo=tH zYP(Vnh7>Mja-^3{$BbrfHxsD-I*Yqsa%Gbn4SeWzF0|6iMV1t2Hhwcw#~YaZE2N;k zTR#BV2X%tF-p9&SCwr!^$KPo%jVnz1$rXZY8fZx9LHAydd_z9?p@yjaO5P)PUMbwS z?OnG9$oN#VhG*7Ti5GKwRW^)0%&JYx7C84W$~ zMU+i>@lbU}&PQz)?HKZI_vHx4_FvgdbXE%il`DxfDjX1yUw-&Ui+)AEkW7?@G}Zjp zo)3r;_2+^w{jiR6Bl*(Ah7)2lkX0iX;mlV;l;uXuk=tAQTrJ8&=EoutXXKr&;HI-w zB9KLoqroKU73*zx>4{q0@*Xw(c(@%^N0p~hzvF9l!}ej4JI@8T)4-s>%3PymbFnu4 zR#N_h_!x~%Bk1Q|SalCE--MGrYhnDnl3K{tj9roKG%h?T1Z45?utRWaS3tzZ2t!kh zU@v9<9Wj5&<-k&p2W0~`NA;BNVZSmpiWe*V4Jz=Xu(RU&y~RzLbVh8W#W!RhP8lj?TmT}r#m$7GYCtlKb}VPb({VaV0wMt?-A~%?HvGe zfS!^7J;2du%7OLG`_BF_Ht&N$aQ!i#KXxOETKlNF+M|BX-4+xurcK%*wEcLXyP7z8mD(x6;{I{;@Q?fQc)`4PY*?Q*Skg)egW9XgAt*)JNCH#0`)2liMI z2g#-6MVJTPZqq-f7%bk1jGN~>EAWGK7^KGm(El?v%d4_x>~7hB zbIg%aJl9AQ*G5B^$*+6hW@4p4_mRfA5yn;P|z zk%91k+c=qX#pMJYbOU<;m(gD{2oZO6V>3EAn*;EmbinX+U0v^?=)h1=HNbE}@nIbp zbRICPKOgipq#D9yJ&IB7@X;_ViP_q*E$Vsao$ZEKdb4K!n!2~(^ zkc!4@ENGa~h&=Colx|F{sEb6^uOI%ZXIg&Cwr^Xssp`GN7P#}5}R8LTa9%)_j3=mi7ej+4xMYcnV*0AW5DSiEBlXN z%kbXZ1Kj>;I=`c#et@qKcP)Ga*atr_K0-x=7HKucR9FSvg>OypA35EB9j@(d~)~X!eV~D@>6oK#l2ujr10kEq(*0z#%&1W9O-6*He61J zNsV)^&ih4c*F%yzm#EQULCzM99~&_T2L>Av-FcGBwN5eTj=e-z)qbD?wpA+sI`6W? z$eheMV88(26n~mdwR0!KnZ)vgj(-=Zvj0(_a!U3ii$~ZYWi;G7T8r;Uoko%8Z(u*XJnypw-UCC%$Uw%N$!Z=5A( zI4<^%iSZ=yI8HXtONif=m_B2i-mJ5|#6Rpt!6f#jx)b-LSLTEzmrDP9+O$^^b$@p< zfAO3~!SmUd+g4?{%rg=hUaP4qf%ZXl0LbwOkPU1PhZGq4&(5f2v+ui4e)0k)9;=vA z*_)c=BjniwOgzzn)Dr>+Z&EmMkx$duRzEllS3HVkc7H}qCkz)GD6ci@zBUSy7}8dI z{7XBey+^djybWc?5-P)uB2Iy}pYo;k^ErA17ieq_4W+pJSEL1&s%En?ny9@zKgbfvd?)7P4RuJg;u>VD%{znD?s(OGuIc(OY*9#AhaSfDxeTgxh zi(pquK@bKd{f8p}+QYxVU1}kzanFV1M5bTu_Wfol0tKyIP1{Rs7q8@gnk{8tqpc1B z+_OXsm^~t9fVL*Ro8HH6b-TOepo_L25HGZQR+<47w{NJ?5Rf)k(EI@BG9|gwv>giO zN-$?wh)LHm_9~bH{z)34e>wh<^a<_cJt)! z9V$b0WVBN{3UC7_|FE@y1~NzI^z}wRAe~COTCxFBjIrzT2L&+3-Z=!QP5raW>)%`Z zprTRub}&-z<^7|fc!0erIBQq!vD}4UUs*=-!Z;U4m_q4>|L{Hb^>gExW7!`<>o8f0 zvB)tBq?i8@r%fI6R93Vg zdlo&5+XV-3hfBczfc4xT()<_&WT-Si#LCP6@$b8|B%XEstACx2x*fuY*fYbHrXqO{ z2PI;FUy^r1li+{M=ccz9^O>3Me>b1?JO4hNX@G82G1@6`!mtqP+cQgfIP1P=}h` zIbiRl`ibU-ngm@S@c0wVzdFVogmnI?#Y8_1bms5TF2d6hyyua+b zG;0ZmLg}FR^*npQ*ZcrXV4|l%YeE()ag9DKApFka*8|++C&qooCnP$C0V&c6(*QQs z`{g$}?cA4oyp57}+R(zL zu|+1eB6iYNJ@6*vqY7lgd&Dsmg<=t@BooM${ZJ zjFSsJEBb0bRLTy+wFB4&>;Y7-&I53(FYpF66#>SY6H&3v5@k~%SC+Yqb!MFr4^25rF%)@Mch4oqRCD4!f6G2%s`8M#_nbkRbytW+j46z`k1oZ`cJm znj)CozJa$txY-B@Mdt2zva}yrkp#LeFbv%}kW&{qxYp$Urd;d$!u5S4qr5oOOOV+lA4;(f@Y1 zrr4fvdE5CWsR@tNISiwO!T7?>0n(fQtpo^7Xk%g{RGRZTu?kVN{S2q#Q+xjAOK_+t zq_lMar`&)48%|_&JT@KNRm{E0RUP$#s7D!tGlhYf&cXVptFYD{B3 zMyQ2@;Rzm00C{w+#?-NKCCMT(PM5O!8zxN_^zBbrvg93wzq@a1*CjY^GZQlF@DNl2 zD)oLBEcu;1Bmh3UXYQXsvBI}b_QTPBi>@ch^(=h!90p$Y_$e8;ZnL&oj~iNM03uo}dIR`g1+z_R zCxGVzre*c-q&H+xj0s^DI*EJo{(IdI_H^>*7(|)u&G4`fJmmX~x0v{+Im{>ySO_~p ziR*Nd^g0t6!hX6dk{#G;>0ecn#M_o#-NXe6+8e52JoqiU^xU~&=nn=&v@EDx=Ryqd z;gx2d?!Oj)k4gE@8w^=YX$6IuiYkXgjh^jtdPNj8T9C z4>y{I_+f1@UFP4C`h%fraW72k(hnII6LoaAkj9CA=iYcq-U|*amPw(zKJ~vo#YoOP zUTlMn=*6pyO$L&ah9m*jpX(GmJWs}6(SmNvBxY}hL6RY{N8dxLzM<85WipI{=4Xq{ zCJdF0)Q#i6j7)ouj2kPkO;=3ciSf?06Lx=F2ZTwLS-fCW?TcMwe@B)=oZSwTV0~=*MyMap-qYe0328!OAT)aqC)9MSA-8=V}WYD5J zcyMJrySiS#sWhw|*H2z-QL`Sn(JW_}HX@2fK5GeMc)T(miZV+I;=fJKE`c6g7JhEn z5Wc{o)zHAKA|i&21SN4es95R=|5!f45YA$STPb#(69ptbUeS-s8Nnmce}W-o&aD@c zOU-m4cRRdfdl#6OkU7kEZ>IT9IJDKMGhTE^xvR`Par*(o4N5lA8^7Dbd7GL2)gGxJ zt_TdzkC89JG2t2TS&ZyxzK-9xY3cwNBB?CwEYBI03<2DLP3?VRNIpNt4{7Ajxg2U^ zyEim|l4`q=<3?&p-Vz?K)sH~|!a#zO09I{Rb9pi&Qk3DMq$h{l1Wr<@r9yreD@!7q zUJal~6HYfRV0WZ@#X^F3iXY2dv>oOqd-3ABSTf{q6kw6!@^z5t`XTh+D8M!|`h`cS z68Z89FrDyw<^1B%D$1*!&=?B+h76<`4j}&0`l@+atQc)QA1isHD~5fK2v=d{rZs{TdD$a8V~=O*Hy#ED9u9F1)W7TT~=wULnE z`HkB%wzg7%>dm@#;1cK0y}C0LE5fHNfnOa}+H+jArGy6_(PHMB{@{y`#zFIm#CXS71Peg26cA0_anF{grCq zA+DtPz@K%_i>W(zoOKopa(oqU$-L^f#Qw1J4n5B9+7YeZ0clTQo2Z-8miJPZAcbxy zJk3o>lLg%3X$)Ntp=g44F%xd(7|+Zf6=X>7NYp%Z@H8irI) zpEI7|*zahWS}|#?Om$jP$lI~lTV%9rT@qGD^SwzSn$-9G{^aT!fHIRFcE*^AGZNH+ zK|TJ@i|9QyCccD%r+Xp+pwf-#QADQI>sypi4uvv%e3`2sOTLD7hj;|ad?8{cm94N7 zAF4s6p`-H}MZrVw>u=~xbz#7|A>)G|Jg!|V3!)F9(fO(Mo720f{W|9HbV z8vyCZdUH~7p3R{Z!@{CmG%YEKFxa7b)9j-kkVOJ1xUgU>=j?P(a~eTi-7UL&rXRLE z?H?Ca?Ic<(MwMthW7vqwb)fvIdxJVy>ny}&dJmpuC34^@Dsgvv8l@eL)K`jd`FPhPTav@y*=~sBGRBX5(#isc7!9!s zs8WgeA7&ykCP93W9l==PT%uxQF^|1akJ-kb?GG2f!5Xs89!T|U6(k#{HRj$g-(Uvz zdsGSM`1z$;?(;l9c^9B2BiJvO^P*AHUN8n!Z*G|#`|8sj;fVmm0Cy{edrW+2VSehq zCsrqztkiJOl!)8>INY0?shi0D4K>ar9*ouJ=CB&X(_GqsJ52g`T3f1A-_i$9+cBO9 zIf6X_LopdO;oR#S5V8jgsFrRZ_83RYsix^G1MPbAEBf1lw~JPr3};qD(W8j%&5%xB zo6ALCb?+Y|oSat~R~kkzw&A>o>SBkzz-x<-Q?1Ud!%D*qHhSL^J zN2@=NOeF-4q}$T;O5wAUfc^+rdb555Duj2Kk}!7%@FsN1BN^Pt@;{&SJbYC{`pPD^ z{5CP}yV~G}yM%6Fg4VePLK>b+VVnsn2-X@XjSy1mS(H?hoG1n+p z`#0G{cWWZ&c;$+>O#QBXa|rY96IR<}a%r2u_z^Q2Mi4WdD{ZD_SXVsuYSvd#;Fa0e zO#$tx+PUAAHsMaX(e2&l-xIXAHslN454)4}V$0>zob= z(&Mnw+odHMfahs?ZeNdNU%7>g@9jl;e(2PfZvMpkQ`;t&&q)$-2TH=Q$R&z7l?Dco zcSy#}1PbQ+W=847B)tx!iE&S|_OC}Z)e>#%%I+|Wc$rynGB4&dtt2`iodG}mA-TQ)JDj>|$EO!vHc~&O8I{Q3 z=dJieH#{S64pGhYRH|BggR zwKg=Tx}V6U{Q>=Q8S3C43eJ!p#_AXcf;~ zl^@}_V=2e1ILq+z79H93p5LBN4fofSS|;LsDY7z80y6vCl-kISM4y~=rn!do>*;=q z?!{NOg@M4+c+&ACUC4@1=yrBJIR`#<8 zw&#C+p`}5Uz#;o&T6HGqK_TzFr@Wbu_PI@k-vmPL$d*iuRp8B@CL^?ig56O6lBj0{ z7>qSzy2m_8{i@aTAhK^JXzw(Z+Z=y6D{tETf-DU9o1;OP{Ke6nRl`rB*&w$|&{Y-q z3`?M?QH=`PWXXHoEw!<`noj@r4Z4MU8dk?(-+WGT_edc76_6JJM{jpXzuQ zf7Cg?*^Q*1SH~+NKvLVkT>e)h^Zl))sv8N^!lx8!St?6hB=xQ9X!Sk)<_gXy8}VBB zZ0PEF{vInnf?c(x%K3bwXb|O@YnQSkiWzRF>L2Ld8EA)}38+Fpb8LeAUFbNZ@}tq8 z`IfxO*9`4W1y#N|qNqA_#3^Sgd)Vu37dr@YDT~z8elS3D{kSftEEBJN4V^L$1?QFg zc^HaO(d$jpQ0C1g7qJH!`g$J*0l5aI50P0|mFRZDNv!FqR;CY;HcU`Rc4@y{6@foE zeGnBr33cFj4~f~s{~cP{IRA&Ho(bR01PP6Q+I1aMyIbelPZkg?ps^P21Q zDPWAGc2~z*-0v+Q*cJbLD^UpbO7I;eW0>hN(`bknXP+6@aKA)`3v+Cr(RZBgA5^Gb z3kx7&9Z1j5NA4?sMdAI%k5Va4!BVJFBICtQ;ba$}F?)p?ErQhb$dYPAhJ$ZuE(0Vm zrwD3xG!Xk@nqZ}pR<=g;hkW`F!Q13?_9+~I$aST1M1GaX`IH6zyI+Dx;TiD;&7Yr;*WCK}beARfuP2 zHY|C@gLjZrSC@pwk*_g87K4#hiV3k|=)pCMwWnIY2K%)FPyge=#ds0JhKL3e<3ganY?zr$pZOOYv4!`QjYH>dMVqbB zOg-88WvLI?;AU#0OqU11vo0ofWC!nIpD)uN;7sdnD~z5JE6{O|S@k*`_k%W1F%OC} zAeWfvPP1!Wr^So?n8qC|zhQT-S}RL@FCZG=?!{rEr7Mu+yqeraW$rXv9@6!mq|a1b zF<5HWiQYjmNgx(05ZJ-lhla|IvKAC073P9yQ+t_}3CiSQ1!`dt&S;qpgd_C31b%_Z zF>SBjT1cZVeR~)x+QQ%|3bFgfXHo+7)G0qxfU*l`zR6<>ptyQO$!5syiQrL^vw6$V z=ci+}4}Lk2*$k#}469WuJiZzg`3e1@Odt62v_mev4Um(2B|iE%AQ+AEW8Jv$5i6<> zG`1o=M-n7lGU{bIIN$Uh8`tcBUk;3hpR>%7Va(~8C7y+X9L29wYuXq|Tz!w9m3aJ*7rQfAH&Cv-k3JO#`jHa+5aTq;NHp~58(0YP& z{>ddm;Eu0!@7+?52*F7mt#nN1w{aC5pj~cC?IdiFevQn$?~~1(+XGpY4VpL1c6?9a z&_XjpaPMS*8-+p7LjOX~7A)7RX+DVTZgJlTf8=CdNbxF=ovM0fnv0#$4jG-D_O<9y zx5O&#AZSJUJb^u2bXg4M=s*teJ3C?vA1NKM4|>0Vf_{4DeUEk~muIzGt0jw(eVtX#61Wm3ehbO-1-?2wQ760%_r zBa2%O23zG?9G}t^z=yRpYc*lLc^=*Hmm<9BbXp@*nC+mb@xNt9X+USI@5I!&(?^p+{auZ5b;ewkxh3$_Cu z-HWTx9q8!pG49C@oyZ%-;mmgacFQ`lcsfuC`OH6KJ^>gIYCLls_vFr2u-V(vx`T># z-ZGZC^ojfe-2mOR7tz363I0th*xfQKlm;*qy5MBI+QiNoa<(bB&xtMx;pQ|03=!!=jGX{$Ej~8|fHIK)OSb z4iOMhx<)_{7={J`36TZ?kq!kUWnjpmLmCGOkp`7UN*aj){%hQOKl^$1dCqlSoa;L0 zjlM(q&6@RF>%Kqt=bOW%I!!e5Zs(xo2wgRdD;;A?>0J2vS=J`ecm1A!tk{GZHfg`v z8;-hgXI>M+PrYBz3!W2ry<9vL z@6M&MbfaRQUN=CbzV`D3YN4oGT4VMlcT}I}?Ar_0=Ou_dqqCzHav|*XP z2drY+L_VK_6)=x~H^1+N`88lTj<-0#AACc6F6#}n-ViT7b$EH6a-;FbC=s5svKC}i zn~?VpDehY>Uy+Jik}G*-Epb#*wS~K~DS`n+@SDG{-F0M`I8|M*N@>&6`$0ptMeO!~ ze_edi_;(vF|J_40U)T2xdwoaRVp1v4@Hs3->As>;$$!&VW7K*;CEIPvJM->*W%?$k= zVOK$Q=JBxsebv`VsYrgynepwhInyr5j-Kwo1dj#?qjadw?aPg}3q)6em{~FBR%dbY zvNF3c{4h<=cjDCaJ9O>6evGfTtIDGljCxU@D6cz>lkiyp{ z?urB{)n;ugabd)MbT`#A>N;k~8K)WeFV0`6GoFqGW}ANAL!2e?pPqqYi{gr}YRV{l((I=%B;ZG21G~FYDo&e2G8muYW4O_xcx~ zzzJ(H6X-E3y}GH4TOOk2q@?&vG+4f|Pt=g>eubVz4}Q_~dTin<`FAGRV&O&@gjA~H^JvIC5TKCs$1LwEr-Q~iEwlj(gTt^=yNv%Iz+HstJ&fbe^ z)cfAmY`2nW^f0h@s{GBk>D_U9!ylnx!;$o#Yx$NE;q8uHJ|6Xv@sV#P**QLhS`sGt zRpf2h*I|V${bu}MD^JtL+Nn4U6im|c73N}@M-UOtoAU!9Uj+)^{+=!goC1|L$LT7I zB{2t;iDjuwh64G6Af35YnRDLVWw~=O1}bRnww0R(8mZklHejUlUx1_4mKJrfRU)Yk z?X?W%YUO}o$3dlv=hD_)6;bs&cOOcF-Oa!6;IBYqPxL^^T(~`y#*^xVQjkK}xEqn| zh8btF^MwXtIH@?n4L>e<+SH0D4y|ZxD0?Nf9<3#187Sg4y8fEdO^2yu(l6a zhw{bSOx(z)eG|%RZ;@BOiX;h7`3`0e^1EQlP&W%Anh86i18+vpYqYHK5sQtODwg;RsfCzY(c)6hMrY&qE7c3qtKt)t z;5S|isRT3r|3v9}D~`szuG)UF#-XI36n@~`)NxL7^-AK8W2H@N(y7p5%^{o%*= zt8l3;@t?K%6ADMtmaZ%Z9lej6sJPjMy=Y_AHi$@x^^JQfn6~jhW1)8mm6@~SA2*|o z28R5zI>LCx5-cysU_48eD;g)U%Zc5$PS{xuAJ-|BWQN}6c} zyy6R^52REpfI0a3akfb@t#jR!H;BrX)34&yVqNI$bP6x(t}TivTaeEFo9`Wbd2in? zN{uh!P&Eb`7}blUUT*QWO6=E0gcgTT|2kR&Fd)O84}>ROhq_7Ix+Nt z^Xle{mv`6&S@twTFmw2v6B=gnCh2$WR_2fZjg`-C0p^8Tllym|Gos}Dl!^ArmXNU7dMid%9zIy~}0H80g` zuGks~L*`$TAN_*G-94I=Yu;d$5~(CMbyS816o36tv|zp=jY zfRdh)(Lts3Y70O!?aqIkJsUG0GeB)a?5z|z(jOw*J_IQ@N3GDv0)aE z!cq(-;R53dXqPVwuUe=7muJQ>N&C{kmw<+#20IIFayRuWY%#JqvgH+=+1O!%JG>Rx z4F)uM-btLfNq4t{ViI^t^+MRE^g+gmnzi}~-#m{c-B_`VBX|#+bK!-DEo3=<7?aR1dFT2?26~nXKF~v` zj2Y3bnEk+Z7)_W(j{XCi25Lh7&sVcmYke%Zqr==Kl`6cQXL$uXyL#QkpzKxPLFNc} z$mwmes40da9Uui{w_coV)PKvZ*#L4XiM%>k=wvOV3vY;R!>$@FxN5BPq6o$eFWB(BcV>< zvevx9h|6J^87cE?wnBnn(1y4Za~L) zkd26B9V0!%`LWrGjhTgqO{HN|W6Q2hqj?J~hEkzg$D(9Y{&Yr&I}b5QT1Ju5;YBse za;|}Z$+o!p1L-RTZg=%h@Dey;ytQf0rXN{=361}T&5;H=M3&{ z>|u7r?B}VW2at`?)PahLH0U!=ewm8L%Kb91mcOKatOzhY&!gdBM63S-&Fzf#pj=p)VjuGJ%7!rHhWo7%S)e3kFRA{fXApPU@ws(9V#Txvor) zfO?@D0zZ~6X7@dN-fr62RkW`@-5E<@UF=rK;2JB;7}DB!=2sxT(m;w?s>+CGqNO{e z+qUuAmfHLQ{A#f)6{=f4W%9ck8TezmX3Q3A?g5z)RKL*SM6yur%$T#L@EWq<9Sqj{ z878P;F{969-g_8&?Y>!D%)aFO^W0sqa!u}vSn7oB^j}HDpHRB?(Zu6#&GQLe(=aKC zv~dh6VdLy;3m~w$`IDe7T>*TJ27vzIXDQ%Q25|71hnoRLd>=)AQQ~c4cto{_+K$WW7WQF z;H6Q^V#Wu9!H}B6WmI6$$j%kKuPif@SRKs?)OR^~8RyD}4e&XLEqYw+T;Y)SRP0)| zig(hXt^z8B9ISaXS}+R$?%k}qx7YIDO2v7w?v%p_d7?Xd1ZV{?M-ji_jvWa((AcZP zYy?GcCx|B!aj&NF>{Mv^Cf+h#__B%Ajm3q!)6J85`*=*8jf2_Ux@B1?Ib$A8+&Utr z-^%S=w3qoYX?A@w($x5Cx7-+7;!!^}(6((P#VJW?)`vJL@dlwJc;Bbtv2bFvQ$0yJ zWtTF9l%rxmq zy^=ncMQP(PaLLMf9N65u1Sq$DXd4iJTCe=-094;;PML7|;aTDe@Y@M{xdQepHF@II zSS!IEF^!?EkxXES(_4Jm(aO1Y^4jOXhA}YB8+4Mr2xL7jvUvH)_}9M;vkr_reY1nW zPdO1JIixuldmT$@SnEhqO2`9fyTNSN4#MT!bi}zgN9& ze@ZfJKY%O%wN4o{N(pRJ(=c&)`3I{&s?+&49P4sDE@1umXYvn!+vL@w{9`xFP-YZb z{P4|Ce>j20DEi@131+lMlO^#kA4GI;Gm^h9KJ7u=gwrdVV_5aaEX!h+ez$8@Z!fL9 z9}$y#-u3NVmK%-3nI}aHuTGET%haxQIgEY7VZ-|7j5OwNe(4kmWANB9;AI2?j4I zxqG$WVpT-*c3zb~uax{8uBER^1>ewe0$aX)tYH?qu1FoGBz?B1h>(wh4VR4kml%*h zHm(dGbUEeiH~L}c7x^cEF*fK5n?)wAgiXJg^;YfUw#ihxVdo{xA3NpF^+E8SjPNdf zJ!@v{LZWC)m=%8@!!?F)3$(m$P4BQkHn8%_<1SjgKcyg~smxyaGj8T$fmApcR?*6A zU3#kpl($p)k*@#_`4i|~&C1v+sCv5Tpyrt0V8~{`sC6&du3xY&iyC?QL{-`F$G$q= zBJm|>mVFaV!7b;{`S=P;5fNIz-!u3nJ$Z}n(p=;HJ#j1db3mlZW_tfPj^e0ahzg)y zyBUWR`qMC~HUKq}E+sdD#=GjiFE%^vVgXI5Wc*BmKcnov7%UI>h!#7pW*RplI$)7jL*D-?A$8V>k&jt$KM<^ zVWyT`8I4nWF)G?odTxgjTw9!!`Mef3E&4EkpA@5;!P}%%6ILXexTAj|8Wb%&9)-@M zWhFuMW<@A1)gLHKJol=KH*MriotAX}Dqk;@f*N_d-+sc28Lthv($QZF?2mT4?LH#j z^Qk!DN-v9k{M8+k(n^mJ=$GbDkce@y=@thw-EqERqSUTq%KS;TkP0v z;(jIKAOmh~{R84wZYq65(Z>=HPmix&wvp+~qcioP&`p9D(b8&pqb<&u1=2LE=}B8C z?dj6zn^tR*?ybX!ikrWxBKUg`f@s~i2*j0=UgpD8Ep79?sa-}rIs4GLR%a1cttzU* z_d{ENYwEv=onU)4I+*I{h`WzbcY_V3mYH-%!Y=C>sYVs-uj-s)o3&F0x4TpV#tezv z1UWwbWn2f8L!NZ>c>xq`kx%McUa6wvjJVWMJQ$U5O&?xup1`_`wfrkF>Lx6-?b|s= z1yUiuK0P&`e|(g$GDF(OimJM;^4(2v`Mysmdr?(E-0|hnjLY`Z)UE(f)K2obWwd!^ z2HX|!S0{t_K6SpeLNrbLfSHg?3-74{$smjN@tUp3ho{y7jvJ}d-a?MwPI9kLP(L7P zB2AWD-fh_qC{GHTc68FP*ho$u=^?RB+d)L6RI)rNa)O1%1lTx>pDG-E!*I9O3ldd4 zdLIZ~B(&P22-{dxkw#ki-y{j+Z#yWjNhJ6~d@xT%S}jBBsM`gBtQYI_3%mGN5T%@- za)QRdJUPJe_S%q~v2Fs~Euxm-o^YAt{_U8TcS5cpF0glXY{lQD$>sgkWc9J;rT|AewuQilnGl((j7hnIH7l z-!rj}P`iA>o$tzI=eF0ry2wZE^!C{IA!RprnPW7$TH2?j2{X!Az~z1mx7X6cSiS08 zYLv=-?Xc+A@xyDvqTkK1XV}@HZAloJGw)aS%Bn-_j`Ge@;TugOl2jO%6K7WypTSj$s@RSoAtT?9&N|#Ywz$N+y#+ZZepc0u2eerdO@hroFs3S^RWs(-5y)#5gEfqDF38x~6$EVZl z`dTu{iz{~P^;U%I;(E$-m&q1)q0Y&imw}}j4VAmKnQDGsi#hEP!*xU3x2^O@!?Kmu zmr{ecTFQ;weUWVh!B}oM%af7^fm||5JQ9dxAB2ON;%48qLozgs zcEroPwzP2&?k(?~KC|`n^5V~<>|Ldty*D%Od*1VYQRN~?;5KCD7!+R0^Fu#p)h*_6 zl5a&@RyPs!%El|SA6i@|PWBQ?oQ`fn9y)$u3ny%AD1XL;+d~z%`|@6G*)Hc1KI*7r z+jPe8@}rxQ4g9x)QXq|xvf%lH0bvpDM2*8mOu_c>WsTmRhWszH(LxGY&5$4Rx#e^1 zH>?~guWoGuw32W2dH|s>&kq^eL`&q>dXjuXXAH^hBEPHLmO=n%!?%W`Az#Y+Kwf6JyLG5r7Ri`{d zF3$#GdV`YM2--w1UZZ&m10EiKFhrs@rqKo7cPHEegtnFDCa_~o+S0FMfIAiSxzKqK z+<~Q$TpaSBC+^Q3FJ%YcBwt|kuW}_}r1zpOV>*%3yWSE=*PUJ~x0K9ow+BSozrt(1 zsgn}kb)7O7{Dyho+DUZ@=lZ~=yn7}8X!vTH|Iz_}Aa~1g>bVAGBH)+_g-gY(QF>WP z>y2qr%ba!O3s&wzZH}ENsUq(ioi`z}K~aVb*X%H5cD?lTQtBh7B9mHe4J8HK%yoR}tKN-O*?Jp=x2;I4%S&l9D}w+%b?3 zG1q?fuWd_opb|5~yxIV&>DXm+um+Y5Jmku?>xs54gN2!MGB_^OmiL^Pe@Ps78L%o} z%fq|FjWHiG`U;!Vy_O3cm;c$Tb!F+q14`{p6H4v( z$wdOr9wfW5SZdnh3UtMeLx57zRggUSnBV>OUI*B899qf);O|c+Mss*KRx(hT;|C>4F#iO9-Gpu?9}}#uIB*#)$izE6$?2P>n@9s z7%s4kWl*J?myYvbq^{72O$=?P&lQYUEo<3n8vj%B^U-Bq*j1E4^B%%g1GM?vvRc=% zGymr)r;ny@EZ#)uGdw$dQ-`+|3&t~AyM{uBj^D(KcvxzBY}Ua7r{_%fM4?6-MjRoh zkl2$X^5ym!-{WN_lB8VL=C9ksJax-ab>bB~wUzVN0zz}$ERU%AHC#)zSQU@-h@aiS zD|klFsM}DxC4I=yJEOEreqVRS*8lcyho$jgY3QO6peFkjFh9V zVf}snRqIAgC{U=ZeGJR~>J`fGIDtym+B(fzAqmV2`cw`CGW83qE@qj4`mFYOGP?l? z?aY_3Hxn=0V;QN{bOK=^oO246`0zj_uvp5OUGRb|@LWiJP%;BgfvJ$8Ag=1&CtfdT zwboZ!GU{>cebbyy;TQKk>a(KpP@YX=4kw+_u`Z3PY#oNcwKwW)JsrNR0=F@q>XDmR zh+3+UNJy<11cDMLOef${o||JvB6KpJM(!V>uqC!$5`&zqWcIY)xT}}GxLlbg_@L7BQxso z94PNutnU@JXP(%YXY6#iLoiudtorNeYr5m*41Z|O-scmJI3JRrP$gfF2NPavP2nd4 zZfg3Lou}6a`QZ%k@9Mt4<~_E8tA4X5(e@~JS=E|F+sH5Zjb5UTWm9@_3!b+=u)6gl zBl|?A!ysU6aM96MKhT<$(>(7eLC=5Ft^a4Oh*;&9!zMv8rGPOE1!q0ya)dnjjnVxP2 zHL7mHfad@Po_YDHBvQc8!A0G9T_Q*GDJ5gYqGC~SjW>0UtAx+Mvu+nba>mh!?^$UV z|8r&7oB4J;J;YLyM;B|lL5*GS{`^t<)o)#v^ZnN~5v+KS6$igf?j<`*zDDu)t{T+x zHS3w-e8g7!1A!(zBg|Fbj!V#OH$aX>^PJ-Cb)g~LlOgLV2D(d&!0aomEHJ;j-e73D zY@mM!V{dteEL*ZwrW(2Hd*LI94rwhQ1Tc z)}`XF-RJu*eXqQwL%U(gZ_6zU*H4uV9eOS@E92xZuzg;>4p-w+M37yh@hmkDI4TknM$D&@qzt10ijlzfCYt5) z`va};M3>kAuMm>K@W9L6Au^;%h5Ft;GHLev_fw za%0IDVxb9yJfT|?+!l}?RSbmg7KUug5^6HfvEJh?_G1y2-AZZx#DJCuP!;M$-Wzd^ z|6F5wFDZAEX9+qadQH0TL$lAU3i!uyJW*Ovr;<&$>shXQtZgVl;$G`4AZWF$EH`DD z!_Q@3ts@Ritz)5NEdQYM?Py^}lyb^D@ZP6G_?B>96*4&rEnN4`>hEGBYI7t%j)g0e zbM3kHolCqHt3^OC*?Zr4+lB!?Zt^0sdo_h=XpcP6OD0Vng0ka%L*yKya z&*fVA;gu`bQ#CFrj;=W#Vl78$QMe-%#iWT2arI2R$Mn=a1&eu`7Idv!M9t)!>k+Pk zM#($b)5@19@R}U@y-bk7TaCrM-*h`#{x{6KTXC;PRhT#gi+PKFERZuyu~WXmhv+0qp*z;u7oGm22dqTC993a}uz@)`I3d}GGs$Gt&*Z;eW= z7TLG`hhWa)#Yf1(0HSsgSKU>h=i@2bmSM(Y(1k9tsEl=bq^P#&;_7jKifDr%&ct(_ zY&2s6Zl!85re;A%tQ31NGMYD9wEhvTx~M$RGZwybr?wLmnG9|Jb3Oj z&FHYwylC$cgBwZezVI|!XQ`eeI&VBeWAyT2tJ7NWL#_1v6!hPC6WGmz&c8t=9`$%_ zd@x)$WXu`TBP+uCxc?$-#DxEOoSVg$kP)VMRI+bPveI&J5Hi-wz+>u2EVRs%S73xT z=$Ye5uqkUOx`6im;Mm@0*{M0+H;}#LZzCy3Qh43vGVbr|>++cQB)d$AV-`y(QUpnq z*Nn1I5?wy7aH-L-EW0ob#y-xqI9u)8o@6ijcw6>$;=xPuvm*3I%uatS9kpJW_V9Xy z+{YWHUlUv=Lvf{!)YYCS?+lY_!bL_DY4L@lc;(if(a`Nybi`#obN*%1SpPu!0VSy` z{94XhHmD7ZQF1^M`9pIy)8yDcjBh?oYD^`yuG1E%@apL8X+7>19t`iy*T&jEQ;qV| zC*R<0CVGEbSN{HHY*>eWE0?>bOVd`z_)&%ptJk7QlUttLf(}O_@Jzk8IkQ8NzVPpH z)g*q!73g3?kn5IA1+k5>&IsMg^6p5u_h$dbBAa;!k4}E9jZ^1K$cQnl4&`^7+^7Bj%&2wwx7Dn+%WJfsz z%d|ZFPkGQe>?CvMg?hk^izNb^htOL$QhUw`&hM9zXLyf79kX7Vlkt5s{>?Y!^PSBz zh-%|zFVn~FTb@Y6R21eVGO9N@!s?JOs8U<~bCBTn)L@c4bK7UL1BK{*tLMS~B>WLl z))(%+4})X_O1|l#EAZyKn$gZpM>Jp$Dx^OBV;Ks?BL{-gXkPAKzmCC@NJdjhx<7Yf zL~@tCOv{>|#D9~U(~uBZX^)9#Wbqo+tJb|Rkqsuu;wklga)YrL=9cjpYngW`oX%P= zx8unh4}cp`)IxZ02u!*<8=CP8TkkzEvnn9odU-S)&ibmRj20I_VkTyH%e|@h`$)Mi zVA5}sG+HYQ?p6^QLVMIJRIe=B*fnfY)KbLu-3h<}fDLF3GFLwO)x)C47HwVnYQ;xK zfRovf=xfkJd>Q$JLYdLFQOnOR2&1x$fnJ!Ditp>HtG9|$Pc;-_XTKQFsb7^~sPO|P zo`WJu)j~rLFQW4wr1J1TNM%8%si*J}!e*+^d|sGeoL>8=eq$tP_FnS&8nQY%B%F84 z5T;72+;E<#x&c06mzi45R|adP`sD@je&<7%m?0ChMd8Czg9S?D5G3X5-%mP*|98?* z^FOvAht7`eg$VC=y`M+|C4R%?3M`kc6{-s#GG$uJzC1lo3yQtF+xQw&jImB@@L!Z= zd$17GYwa`DR=iw8g)zOI>0 zJ!Wk)7|N?ZS?L8Z%hR0l-#W)--_2`sjgI#C_j{iHh5P^!S)i%$o|oj>JZr+E8NoDf z2?m?wQ{_1&jfu*%C^jzzbSSHNxm_#JntVeSpWEITsd^)1rSF_{lGw4OqA%H*dr@Sv z^OQISY4!-9L+Q@-P~#gN!q8b;TPnonYX;T7>P66-UE#zyD$HVbMIUQ)2)|$@H$7J% zKrWujhe0P0CItUZTN&fSh&eoM`vwyHTAK~gbephp1S2(&E@=n7etc}pM1Az$7Dirc zF&8Axd^EV+89I91d30>d?YfDPo;Oe=9Xj^>JNh)@#qTk0OjX3<1aL)GJ>x)xJ3QC7FhfYYPWqt??4Z0~t-h&+Neq>(t;zvUd@-Q05! z1z@88@6wrL$83d>(b3?`#Mb0s(c0cINjiRQ|ASd7jVFN;F-k5I?> z4W{XdMqTDk9Mf@u+aM-k0OScg#vtQvD?K$l4RtCO6cmLdoTM;0r?tdoVuj%+0ECj; z|GB;hIQt{0Kl!F@Y@7X^rR<+nkEb_FTpT-u`{Eqe z)-uM0E_gFD?jC&3Y1U!fQ}}4`qa@z`c>?z2KLE@><1kb1^q+cqCtvEIP8&0huNR(E z1WZhcWTta1@>Of*w$fxu_mmQC`3@SAOIAwX1_Cs%|CVm{B)3C2@vG9So?_}>ClBgx zI(C09j7w;>Ox&pQP5V3SW1_o-xNo+u0=kc}lEvj={E%6~60+sXT9z#nWdpyLI|a~wQ2 z{R~izu`G}%H2|!XrWhpf>kNP_qaV%xU@~a6mp@#|_sG*;ZwTM>1=p=iY^JUWmc7CX zQ1f>$&bG64I1vlj9I8E2&z1QIi1Ro!=-gyib$Ji$UMS2>ok}t;`Cn<=e`E$x$t~-N zu@1K9#Re71vz&pYJJ8sWRzXy3329jReVwI9FxGotgB{)uc%O5p)8 zgJ&}G@?F5c@`AtcbyJY6s4(RS1xn|yBlc9n(-~5|l*ddc$3%70> zadvOIK!;~Zgs^|9$=4bRH@51PwKehRJ-{=@(ma40mk4D-GQ2#7hob*nEZCC(z2i!$zPMeBU=t zicKbE-}sJtgEdp6kDk3sy~?LN?Z2tC+JP`Hr3>qZx^fw5GLMo0cEuYz8`3zNl+AXz(U0*RH--luEiH-e8r zF)veg&#levaLj0k*(3_BVJqcWUlQ}zTMB-dU7R;U+9|4@H&Xy<{cZ9{jm|>Cpi;oT)Tt z7U(WZPOKkAi_jVsjB#%(zLyz9TZGPLoYe)L)yZ04o#-+MQg(=6R`nH$iUaddEE5_^ z*Mm?|4Q$^>m2~RSwz?QeriYCLTB}|UChXb**fY_GqLE6r6zvc%KQIeGTBDh4HVA`C zP0~V{BKIWkT$_4>TcWg;X45bWz!T?E^#L|jw+B~~G2`py0~hYH>?It?9tlp45z9y( zXWN8%#?VSr3}0qiylEhQ@TJgJZLF!q{vG1{oV_U9adkoSF*QMp66p(Nr1TW2_011Z zRpsZH$eH~0(gC~aocH%s2D}dyky=em`mLhh^QNIp>nb1KAX_0=+Xwf9w}zDeD}?Y^Te@fG)N^IUjAwV;Bw zkng~Iku>9dzTa5>a0Q#>$Ej`aj+j`fzOxX?iC)UoloSbtg1%M;3kWPfLRUEqdG)-U4Z$6Hx!WMd&6j0JKlAtRFa zU69UW5kS|RJQDG|jidCb$(uR2nP z<@$tAVr-GM$QmMaSZ(ee4xCAGve`5cfX^v13to!MNrESR%NIc5QbSN0sXuyibni|i za*I}G2>lg%O|YH0a+GXQ>tym-r4$x8p(>uNTl)<;PgX*#YKK;KVSn0on(fA$%8AgK zYGf<)ee`!RSG1^^F%0W|gZ7Grge7hB`ZC};eCpv2A$1a;%jYW^9B-ktCaD!Fda{3 z2z?U7656s>m>E46zEfB+RZXSDB*6e#Npe0JI-yT#V>B1Hr()?QvMi7Xe%K5wC>BX# zs@7XsKg7FSC}Q&?as%52e4*|rR(|eB6TeWF9AIY3*+sXAj51qPzp66fzU_GV5|XSl zEpt;dHf^42xCsz>tUJ_zH zp3m-5bcFyKX&D*GM}5Sk*1Y5wR^$N}>Pg*4?DbFZ0=ip}{MzILr3oAU(zK@#!>(rI zi3an~J?f!Ys^2+{^oA};F$?w{m9yLi&-Nf2t`Fbcf}q5Py(+)ZK5ETdDKP92g_gc4}u_l z^6v6csWr=027%R=ulK0-jbdWd{n1+Tzq>EdF^2G-#+By+bh&$98}vF^^j+@P)w1*j{tcPE+-I$pKt>53mbIKSFgt|TY#FZpWD2@{sfL9v?;OSH* z$7vq#A%ut9NJ(H+ZTkQpg1$!${^7|N>u37&bZs(+w|0HXvh^}I#9DhV?%Tj8&aQTU zLzO>n^f=xes|N@AG7i;WmWtHoI>xZAce|HZQDEXT`HpM8dqPP=N`I&skRQJr6_dDU zP!Lk?q-lTK9>cwcE~71E3qUu05}uLgy<53y&?+kkKg@ThM+vONI0^Q2Z<}J!TB!#K z<>n7QN!~{KRBc7IIz6=gN$Z05Ldoa#*%u;0Ki_yLp}+E=bx5akAjNqj+kclM^F$a9>E+prSUm3vJVB-X}FW35#O z^ONeThtwMxKT%T1e|@9`V<^AJ4Y!&z(zGn9h7Q4$!^3r(w_b+T=}N_4s?Jq@EIw~t zX8LxwKo zBS+~c-*Bb-WoZH=3V!VOyaT1mfCcrnI!}0O_$6!&rq$GyGCeVJ;@8kc z$DJE#dBBO)oMycU#>RUV2uGLO!uAG<>LP#Na-=?%sxS10=8E+NP&h1W{aPaKRefhe ziT+4iNblKdc!WVN$1G08DSC^2ym-8uAU~ySHk%|JIAx8a0I!u5U`{4P4v-(%hLjX# z%;_)w>$X<4CtVn_Q;{nZzW!d?MeUO^uBFA0*8`KM?3!?{xTzg*@oAZD`V9_816sl@ z`f-{f(J$s+{l500ti+On!sIJK7Yf|5O82hd;=K0%{*SyG?13ujm@D#gAOUsoBMLeW zlU7g#d=4DVW#}uJk>r({*J2cS`+aoG+Wzr%h9jf{Jp{%xF?)13q-{??MIzZo@f`=F zxQiZ(Zq;BC(q23yCxGqIc10(d$BHp?(fONav6B0OKV`gMI|g+=G~ZHI9mTEh3-X z1lowG=c6R9(eV@Wd~KC@>7M90D7#uJ#w+II{wXMLI3RllQHk+3D4lp-AJEJ0GY7TJ4 zHay9Hu!)KM_J~)!Xt9A z($FnpAfK;;EF**24zZ@7Ge^YxXk`k0xQNE+lX~?YnkMy_X6WM`cLvEBHicj2{fyjf z&|*ESQgVvhO%>t?oMbOFXknQ#3~TXCmAMr}kMf7v^cEjU zlN3b3C@U5NB-n00Zj+m37url8s{@2U5gPx8^}F#?UzW~F-jJHT71sF#Xfo;Rk-zT* zOM$-;0&qKX=~V<19`cwSXPWpLoibGj8$Xr!tcXY&sIOnx?q6uu2*3aEmKD7BNRM{a z*&i@}SpZz!-?xD2a4fI1zjALqd(k^WXOc>ovpQ#^?=ut$qBzu$#{wAb#WiT-Mx?d^ z-kX?un(Q`FNZ8qlYX2eVijARei#!#B^~!Nb5D7u8;E|zz3SU82X7cZk!Uqq3EI%+9|S${=n%GnsdHwzH*TM5J7s? zyQoDMrPIk)udC{$e5@APzT1>SwuvYMJ)7TN~l9+Ix(CwU2JjA;K<@xewXOiAl| zDYv%$Y1yX9jo1eYvg~R`{T-Y}0F!vt>LduU9*jDjfOH7KBi-XtM-23GBgg>Sk58-a zO0J7JUKNIXM-y}NW`g+k3aqVZs8oO8&pZm_ZDrh4Kr*@hQtTlH1UHUtT9)lL)&{Cb zF@p1!bJ>q#@)d=KA4{oil;{+(LE8tn70Cs`+2E_4F{;0os*|maD6=Y$1e$bfq+q6$FXXEy^2~zaoS@XP-Wnd}wicdrlh>e`F=m`);nZ0p8~0UM zjlG!Iv5rXkfq<8guG9=Yirn~H?yNp?-wRbWK|i{pyStvYUa<9p(kvr>OLf#}V%>7} z-Z!?HPd~9kdm~>{R;+4~RVMa<<2Ocwx-L4>95gz3Auj<5*V9l*)JCAx{HinbasSfc z^mXNL1HhEH=&>A2m}u-Q=cD#=`H!C(2l3OMNB|93NpDkr$Mj@61@52XBldh+Vw3&h z-m<^!5?t@$ui@Z5IpS2lqE3Lv>PHb2sAG-*GYQHaox-okKbZNGS`7c7C?!b6^d3`M zo)F;pDhhO#Tl(h0S;3T%hb0atNf)=OWS8Jb2h8*MF2Ltw?6uOGPg#w)>w<&0PxT1$ zX79yME4ckiR<>MDA4;^T76A|6kmCv6ByGSQ?|h@h4{!v$Quj9=lamNcrR1lROP=!a zWaFlIA?G%+3}1fYk;a`H>qgd4N~6P-F_wpCj-G;9Qoq`Gg_lKJ)$c(+LjTm95BJ=jyL0m*GqmMadT)%lC85V%+6Si8k&w`p=~Z|4Q6coSlC?op5O9)9o|n za>aA5($L}cB+jw-%6rlW3&q8~BiA{Qn9!-|*Ml))gB%M&YOzzJ~r1q0N9Rdy;Y#nrYU;s>EG1wUXIjA-ZRN-GhqgTMP}oX%rw zodoNlgyb8%2v!?HQ1LGdF%Y8+?p*x&7;o z_;67|ei!eVNgDWW4 zy(xUlB|Kf)(>g9F3rM={ocPh@J1ADrE^g3jMju=~PnaXjg}C*apmjbCi5{{({fSOd zD8k4o^PL1Kv203SHOZZqGE~sx8)I~fc%xg|bV_g6gU^wJ?N&E&Qn>usX4^1Jtbr_u zpbd)yRR3`A%(r1qxudank4#WR_BSlXmhbLYjKEtT1%Ln!^H>^Rr_>SJYCSl8q{WKn z!3}Z^n4Nye`yJJwZf&%PopOji%Wu@=zgd#Ax zEAE85wC)gnvKVt>y+fnesIbrNPn|!qT5%U+UEQa+0*sYZKAaE(%aQ54)l>qf?;bJQ zT%73OWZ*tn<6fDyx}dI#=t(qWzu#*Kp$U%5hX)`7T0g|h+|n1sQPX`&3Kxq1oku-# z_Y+T)ea>fHO)-r~$i%cS@4*VK2|j0*tibWN}-b- z51AsSG3Hr1)(y@ibI60P-uX%1lc~_;W=p&OCY&pGJpa_7pt%}l_cRC66gae$*LDHN z%4DYB=XqsLtPt~f1LyUIoMgI?nz}y1o8Ua5-u(-vO7~H2Vf#sy&qPB@d`FZTZ~7wq z#ybcp!=V;uX+MQi!1J}sNKjUrFp4vL`;r{?$&09A8 zDHNx`pChtzL*Q`ai=$u0(tD7B`i~Hlxh_Ixuy5;4Z{z78egK##nzQ*Oz?8TB@O9LkcS-x{}7}Wa2RiDh{-U`bWWx)`x1mY;4tPd=i zZG1309P9p!QCAsktI0Fq$olk(vK|0ZZxY|ZQ)l1&K?sx4#)pBNoyOwvXm;!%8=82? zRTmSQAl=i@rZ-pUQ>hyW?|k!~|1nzOYYa@6GL~wp=0Zq!?&zWcPCSwh zLHm|g#;k}w&L*%Z(a+a>5fB(e1hVy~J4H)&f^<$vShZfVP~Qo95@a68`;yhiwi5Fo z8(nsa!HJoQ#^@p*+Vkwr08NLPLP7*0b~b{Ln+*)ARjVX9cBYiW1Q)r;O-}WIcKms1 zG7NtD{CGvqI>cGEw@F+rklTK<5g>^UyUy}m5qCkPshb@u>K%2R)>0%#WKsWY?-TWE?K`xzNDscC8%a5l_wCqVa4^dvu8Z(~Sm8XNpXAJZ++ z&dy%=amOwwxJ;rg!d%;@3}Dtfn5r1d*FcXnxL%q^bAWKUgR>d9z9Vcs&y{c&WG**? zEy}|3{Z6GTq<62=%%Vu@3@{*N+gcvy=w4Zp!Yfv2vJrw@IpPaQh-Kt@z{jG*Kl3L1 zC5AtME@*21%dc*Fs;1HmPLp7&5HmRAhM|v~FemQO{R&Gq2a^Abyk^X9+RZDIN_Mef%VEC!;Lnf1W|WzQJmb`(K%K zBk^?@8|N>IcI?wB91nW$p#RJ@Rv@U2mHt)KcUW!NRchoT0Am4Y)r^DgSL41~zxZO~eRP9IAj<2j;m*9R{{yQRVt;-)Q>LSK z&5>UAUx9-~tUKu{#g>_$fs71t<}KV;YR39k2ON9vwEer|5wYs!{`-allhxg)A$3lt4@;uv^{&~rTaRtOK6)r9>FVinfHiK_G)8Fp@aXpDly8F*E zcZdso@hz&;;#l%@HMYC|*O#Q({o}_KT;0|B*#C8Kr{iBkB(m%OHMG+2Rmne|l>f~; zGjL7-e>gAVZ!fn$F(4to_bJvkCRsd05j_mDnkJl@cNhA=TsH~G57#cM$}d2U2v7t2 zLEG`O7ij9gL+c#__$k6NUZ>v7!j0V7F=Dx0_UCYYhXw4f2AxZ)^U z!2@{4qFdrqd!w}}P#WE3)zpR6#-GBoV*qx7O#{G{KB3ylJCi_S{qoy+13V22Z43-@ z6h{DWC4k-#C~LZNsM;zE1sAYm_?9b37hzT?P=pBONb8)xOjL--XM4?7({m>}v6p>tPOE*xrPJ{dTmKfbM zQXZGs@)y*GuESZorPT-~qeRmJT+rP{)F3%&qFABN>~IkWYyw6x1ol_w;%;0Wiw>E} zj|Ah~@k_SX0c=%QLrLz2(c&z`jg*SKD@a-`PB8>05eo!D#1wEc^a4yW9f^bKi_o9 zCUtDU5A2tQPJNt7!{2#!8!Y}#Xptf$h}>$D-Y0;Z1MWQLnv*Y|a}HMSJ=4`}QU{(B zWQ{j~{P#rULK_SN=eOJ4+)0Tkz?(N^TY8ub78I`ynti?NJpsYOskCk;0&+T`)*3^_ zG?Smb>&X*qmW$>P)M1DZ5Ms|6&l)S8EgGP{V693`Q~nw9{f+Z`UTmD#KV$U5Zx*s# zKm++3(w5~-Z{VB~XGT=(gfn1)ZGUKyn3q_*`l;aaIF-`yoU?XjRwOccI^W;RyHa&P zUjP2o{J!#?)73>l5_3UKfe^}O{cqD)lSiLNjQzGpZ2S$;wt`u)NKj--1?2LAT{J_p z_VUZ-{h3ISjL#AZ9~T&})lvAj-bg*CjLSbTpRK-_&l#(_MfRht1v!zq0|@Y$(hY8c zRvto&GlbbP?QcqA24lQV(1oa>3t}URTpT~X0?Fgp1*m<^0B8S^!Sw+ z9Q=K&z^c1(t>;Tn)2j%^xL<_$cFAVZ#-Hpo72xG>C}zA0_%)I8JkG`)dCov*cA~iY zEz2#9MC`h5#`DYeb5a(@pjk$F664yznhMsM<(eEq_)>4E0hXGyF3Z1sc|ogXa77TRZVY&8;x~`dq3{W^Lic!v<2+6kH2$m zr(8}H4csXQo*)luq#Du6vucou)z`5?-8mM*V5+|{V0PG1kxtNo6QFvAQhzMo-iE?> z9GR@Bvat^X_>VlF>w2wonB=ec!YZi;g=M|4!6!%3w`Cnh#K6wwLKL(jos-3oz(`J6 z$#3^3$Fgd#j#JFaBB5+>H-E(psSdwWtH6-ndUe|7_tQkoOY&O(;;r;QvFLB z=<>D3h4^UH840>J;&4G~j|*Shb)7)VS2Y;l$ivKZh%I!_BRyo-IpCa6ls*xpE?C{h zC4z(<;&a7rkwB0wa|KU`r#Dt|~XX_!6JWg>plf(+)7S^eP_7a!R zd21xjH_9#Ef}tobZY>L$5|4GN7B8neb$%kEHIjG9Qc{OSrMNTawNkhsUd2_{#l+Y~ zn6qdHgL2Zdj6fB<-ktZ4QPnw5h7DFd><6MI7uzPOEU$45Rj^q&_VKu-rnUZlPI@cq z0;Q4`Q2r{Z>8bP)!r1Da_&y^dyZq*MiV44xlJC6TSrP_hq3 zl)z(hD%dKA+v^nV`#+wUMTuSFn@&O!D9W`5eeC`K-{^;{UAK{nSnJt-tWP)f@lCdz z1nJuKStQiLqKswao9RB{PNNL0LJGOWG#+t z6LyZ)?`e#>B=YH+!$_%}HcS&M>jm+){v4@8{8vL_$5Y~Q`JRE=k;(8)vQ~CPW{YdA zWvi4Dw~~O;#rf2x_%kK^!*Ez|o8xydUR#oUK!;HQ#lfg0-JY4udf)rU6T#uL7$bV( zM*ugpqhSnL6j?ePEKxMwTtfDPFJRSObt5Nu&4@opYfe(BvWPycDcVaFjM)5y8t>l^ zL~3T`2fZ7*!*B&^J=TBlzrI- zIGO*NMxnj{evnPJ>OCLRNz*DAs{H5V?FI)NXYr-s4TkZN<6QB4W#rHMY^G~@c&xDl zV=9$v*E&V4jr1^7O59xWdhnHX!>cXced&0R{v_-U2e76NDkKR_hiQJWO+c$T4EUz1 zZr#T+)FZmgN=g*5I4Ye+ra1?1iyoR&9ZjsKUzbq0I~0M|eyQew6OWUbY7H4Bx16hw z>heREB4o~+cF1Y_-J)L#IFin2bN^~V@3TWuMVENLr0Kcv1gX(gJg|yp3_V&;T~Vf- zv@)K^_~+9PRHtFm7Sr{{D_ttsGuL^rF?e8OEsAIeV+&(1=CxK9%uz+=>q}g`c8(jI z-B0&6XU%=ccfaxGN+l-APt@SEQBSRT5fNNv^OKO=6;)i#SV`ZYrT#wJ*73moRS9gB zAAkhg%1}hzCx38MtI8l4l75LjFbcU!x^;H>XKIhO5%*l;J0_)3Fss%`14Q0pUI`ih zn6Zcq%ZydwE@9$^*^kY8_tRowBM_aZ><1jSgyjlbb-i{`4zDU-KZV>+L^yLB6C!g= zY<L68l^Fois|fNF8Zh@Rt;O6O1k&Smz=L znZMpp04)JFy2GcJgGL;PGlHCMdtSMz8TavmqK3fyO!w9J0|&>56tj|V(LVT-_A}>N zDba(T6nQVlGLB~D4|A(w^ZQUz@NKE-;0;|o@1Vl%Sc3*IDvq=I741zE!-UJd;l16j z+}I~|afzQh-WG@8DQJ-5D#L8Z@-h*2B4vHJ&$|JeBom3oHo}-GmZ5C`Fpzt4rAkY< zXgSv_MKQ0}hm5dj%iyhg{e0{vW$q^%oFkYU>5AGsPQRY%i3PF2m9x`i)+-t?g{Fzh zhsmsC9YP>gaeq8aE?JHJ)l-wLd2eRk3DWsjax&Ni1K+>b17PX)9<%~z^W+k10nKo% z_fz5|KrY%;7K*ru+{2Bu3j6y$rPFR&{UY)5$`HtjmEeAb{H|eUsCD+nY02#$4@c2=8Si%E5@NVgeO!Zod;Fyh}EKn;75xUA)1#aW}l%_y4p z76P#X_Avq$%V1YEf8Lu($)j7otapWUJ6`Ww1nTNtStWur5cA!;4ivIl78*b<^(HUN zmV1G3#?5s6qQ@_c-@djo+yp?ZRWrf|v#}At zbqa2_g=Z-c{(80mF)y`ftx8JM@$)7N+}Dzm&4+@9)5aI7kWhH?dJ1Q zU_4KxlvZUnN|oLwPm{0tHQ*x5T^CYc=+(Ns0-VJL;v6X8wXW@f-D@?ba16QoenVfkI# zh~k;pT0f4?NYQ8eLP4i@UP!^eCpdTSLZLLS&s1CcE#|~N6;7a5%rHxWqlB>6H(~y} z@{t>toXBH6%B;4+hcBY;Rb7lb(Z_FvkE4L=`!C(i1a0l-SrUtaEzw!A%=a$ZZvTfV zsRFnD1?+XwZfyR=B<{?jT4N37!WaTG{K)u5C zSl+ugW=Uq*o!`5sL{uu_7c9pqH#dsC2Mc#1gq8CWvCEv79FH^V`a)P3w(0Q*GT z5hIo)mcsS;5GG6|uM%5uVmR(hKW#y{Cg&4LfYb~NpJF9doNh}a=;@U%a=;#6NQHaW z9;G|kwt^O9p`b+K+K>Od2L4eo`&4;V=w0vfpopcShR#9lRaCr_vd(;)h^fBaBzCL7*63%>+~ z?JYVXcYB_WItZlJH}AX%*&703(U~}i*f)IkfV-$8SC+tL080us@VDw!Z2AcW-V#v# z;=7;e4$qoRdfM;w$ci5J&()?66*BreD5tb1uTSsPJdEd^+Q}*wkZ~!J9Tw-Y{7aQN z1iQyq6l8bXx1M6h29?nwMs35W_U|XhT;BpLH2I&w_n+|V7sAT~zRMLI^vv%@P`-?( z4n2ghUFC@}qKLmopSs0=+KFqbE#$3ty6yvsjJO4`$-Z2kB>#KK_J2-+`R7qai6;DA zdipOv;{R)7K%)QeUp8=~|GTQ?6dSI`>~PR`rz#VH+{$I|V_|~*X>x2^XFe9Z)aQQQ zv5330J(r**0X5pV1Jb4BF)Q^`?C!x&|EXf2m;t22CV*l}LfuB2^#gvTQ2*cko=1}& zyGuo75>SkLsxxvk6`;}$SjZjBY%hR31CbT4w7J8k(Z$q}j=H@SNOtOj+8!1<&&N7J z^%J*cNJJ8@`x~DEN6HXXDG72` zmS!IP4g!kOT%@s$t3cQ8vjmP?@I8qi90*e?<%3zqw&Wl?AAn(gLH9muZ|YyH&TJM! zq&v{uC>+0l)-ty!M_|!2OT4tl*3zKR0GxhlVi}f0e+@zgvUAeS;9$&u9}E&Wd}-z7 zfi!9xQw&tBHw>*7@LP~2kn)aePRS!TKxthWH0XC)`g^A4VA2;0ly*FK14bF#CXCm+;I z1|>#}CNU@`$m6GBJxl3;$80NMb_|OKQcfSvm_)$iugWJ)pV!Fj{UsLZFoQfOqWUfC&(`ik3yxnVW#ln+jd-+ec7@&=r6I z)>+Tho2q)Jb}diVR#6in9&tZdIXUTCy;EnczHmVDU!& zj=XxqIBF4)wgqjJ8HvMxi;AY<8{2InNLQ5n>`(Bg@&#Y^S8G)jQ9W!e;;9iZnr~HI zG*)^lDC(cCDpap>5y!coC=Kb_3EMGLwhLbs1ZU4rpPu^by(uDEtp^ik{rCHpoz_QC zk$<dB`=ARM-65Wo{fokf!J}s5?<#9qZk>`_= zzoj{vEw$>9QVFLQ!q9I0DD=Q(r53EMCw6X3DYL7Xh2DJFCqCBued<`_I5Pv|j3Sbd zsiTs~o3=QMq@oVNl(k+)bC}}od*3%#xVLk)@+qb+D0cFsIt}`Q@`W?x=dZl}3>kFE zj?7_o&pe9qslK~o1ZMXtvA7vyH7QI>%h`-0a5j~!KuKKyeI+~Itn?xa zkLw;|L>(aM^lU?6>$_%SZi4d9XT$tKL znlXL2CYnlorYKF?EG{H4UqJ$MPBYX>x(CUXvAG-7{8s*ETr|O8S+Ehr}~^&hr8N5R5(M&8?0D9Yq*&BlRSxk~md?Gm$Jvb5l3y zB0V=t$zlxfnF~yhmkL)R{5vcB@Rc^x}62 z?epC^M~a{ME6^1|Utd7*WAuD&+<2F|-bzuaSYC1UMylIKLP00KS3!Fh*|&xi{5UDt zU0bQ#m#Fk>G#O>;PAAUR?XT=Ae7^Hwo#5$4U-MgPRDjn_qWN?k*4Ao(s zXSPvft5t94&=6E=m(zBt7zOEBS=ATb&Uvj-ui|r^yEDfwqq>kHr;fk+qz3YW=kT(E zN0Hio9_G3pvxG*H#fqc$5l`cbwXP|Q{)r7AjD11kkxfwp+;d>*uCZ}f410~@-ori5 zK_!eBarc~>fZ)h0dorqUBQVPUui-X;`Of(={oQ(8DOXDt_5)? zsxR5xIyo7W zgNq28QwWcDUz(2g%8m@tEYJH5)0OJrFlJSIW~LFNl9s=%1TF)BQ-_rZm|Mua`I4MG zg;HUQ%rN}Jsaw}s6a05BD4TzkEc310i#+(g)t!fCn!8eePJ)T+Y;dgIE60gnF;}`; zc&uxD+hq8JLxvWfzc9qy3+2zvKe5ZogD)&DCjs4$CKEIQGgAG#P5d%gyAleejIc$= z^9Blxa|}73-}z!sKCA#E%X>E9q^p75R-UFIh-g9NSZxV3-k?m#ieOZiA$FBvw777s zg-*WgTM#;4sJ4D`bod2V(<%}apZk!{HODF+39$>%1?%?c^(V}n1Ba1@Qare&ba3OL z@#SJbc^BrYcXni+rH#Rd8_Hq2<1Ue;N^#XYJ-M3w;Q9SM(H#QY9|W@6A&T?Ko3Fgw zzGMmcn{W#6w2uZ!aR@p^KRvN)aTzeC0)ycEs#HwlUxJAqg9JdqL9){5?TobW3 zfAk)p7@G!9p~88J?5a>3!0S*u_`k0HGf36^2rTz;Cq4gwtx!-KEX61(mA6p&9zTAsv(N3UJLrMzZdA@Q-M zPS+z?Q2W8L-+A(+#ZJx4qh>(X;v2jKc ztFStzB*c!extUynR2F7li5*aP-qAYDJlf54W2wFnG${F+#r+Ftu2MaXO}s_6Hw|Tx z$c&rIe;e1Em0ys~*$De34sC&ylTA>MUnh2z`y?nL&gVWH-6Q*>6u^E>0%A^0cq*J{ zDba+7R#T9YWa~WxXN#-ZUGt51;=ZhDQptG)31mFij@H?aba;Gq-VJe=^E|0Qhx+|U zUgJ7feJga_Xd{^BwjEe%myPNvi!(W;(h_#eC{e^yyw^HXy(w7~1XZLr{DlxH_eM(1 z?m}v}4O*bZ*RP3Z>Ew2RESJ})6MVj?!Kn(?>B&)1Xgf9WlN*EP{k_;pa>^ggEG!%0 zS30F7`>m(5YzKFGCA`OR-mvYqk9OlVXz|tESw|O(A+qmYVJP?C3jihFX8hFX{bwqB zSp1VXnP+2+aw~Z}hU$#>j!p4w5TM%qHH?0B+d5c{Y_mbCINOUrpc^4y&ck&6K^xsx4I1u(voW7H}_ zD@=)M^f`(~cV4GQgR!*`IJN(l(Wap}E9uE*QV!HZBc@}KGtf%Z@$@F;o0+j`MdW7E z*aiCXt9-Zhxzk*QwM@m|TeyMPTv#8AiM0K+hefcS--PeSQ&g^$ROkKE4*(pIcill( z51Yp*r7~f+!QVnWur97OW4oc^>zioO;fC3;_Tr0+MxC8#O#&oMU8TDLvK5w5W4gJ? z+#7iwaemf1C-&P%tNey|?3ja@4{qoDS!q}=R;NNXkOjNDPM68bsY~^Dd-xSOTPpks zF$5hIMr@<4a`(8zgt2@A!P2*VA`roJWyBVFbgnd*Jx#b^rCa=4xRs%O8L0X#r=gT& zKu_Q{L0&Xna+K%{Sg|7Pe^!_*R-avRZn(Zn?avl_%xVA&!Hnq4gHH8#I_pf@|l**!Uhfe1AlVd)v8K@a;m1;{wjc z#+jB$Q9znRzLa+Kh3u}Bl1@x{cu@0mIq`xwB?D3Rx{MBGERki?V@g-!*68<%YNXrG zwWp1jR(WL%NxW|P9?=~tVffuNlaI~h((EUYohxP zOoUUDOdr3Q0Lh{e3PsK2RLQ<8!1v?6H={UT_Yv86HeU}};~=r9QJwg7)kHr;)9))T z9YGYD=r5?pI{V@292u57vq2;R=BNUuz{DFX$j-mEalmyAP`@GxAU^9oDHRWuX4Y*8 z=Xg@!gJ}PEjY<6c5vDm#Q`oD9(tnpui^mQ@L(U#XT%2_V?gud&&_}%sevf(7MioTEbPy zVOwR-BbMq%#8XR|Wh>^3-AHZenB1w-?5%DQD;+H1&8&gaM51!D##NZhT>Vbm?RoZE zErNOZ7mk7nQ}_kR3;uI7%(B4BuE$qU3I>0-*U`#w>LGyYH$3^h<3 zuhGv#orI8Rd;orw-1T8p-$_7mpLzRG%}B;Yi2d$dZp4Sb)zfbocu2Ci{o`63~hhxu{peEB0`vbG~WVOG??0?AZ|M4UKD}WoY>lbJI zGc0r8A57Qz3d$CAErMXr<9Q{vGgZRaHiJS$<7U;o4pMA|$8yS82i;OjsB& z6aVH*==P&I9kV0-ffB?|%JkaDmx`E|fBtlClHSY|wN0z@184M}S>UkaSAuQL%sL2d kjS|%esQ)E%Yx9UmyXXnhgpj2C1o%=@zO7Vx-8}Gr01almLjV8( literal 0 HcmV?d00001 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..04f43e0 --- /dev/null +++ b/pom.xml @@ -0,0 +1,107 @@ + + + 4.0.0 + + com.tencent.devops.ci-plugins + run + 1.0.0 + + + + 1.1.3 + 1.8 + 1.7.0 + ${java.version} + ${java.version} + + UTF-8 + + + + + + com.tencent.devops.ci-plugins + java-plugin-sdk + ${sdk.version} + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + junit + junit + 4.12 + + + org.apache.commons + commons-exec + 1.3 + + + + + ${project.name} + + + org.apache.maven.plugins + maven-assembly-plugin + + + jar-with-dependencies + package + + single + + + + jar-with-dependencies + + + + com.tencent.bk.devops.atom.AtomRunner + + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + compile + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/main/java + + + + + test-compile + + test-compile + + + + ${project.basedir}/src/test/kotlin + ${project.basedir}/src/test/java + + + + + + + + + diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/ScriptRunAtom.kt b/src/main/kotlin/com/tencent/bk/devops/atom/ScriptRunAtom.kt new file mode 100644 index 0000000..ad19abb --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/ScriptRunAtom.kt @@ -0,0 +1,538 @@ +package com.tencent.bk.devops.atom + +import com.tencent.bk.devops.atom.api.QualityApi +import com.tencent.bk.devops.atom.common.CI_TOKEN_CONTEXT +import com.tencent.bk.devops.atom.common.ErrorCode +import com.tencent.bk.devops.atom.common.JOB_OS_CONTEXT +import com.tencent.bk.devops.atom.common.Status +import com.tencent.bk.devops.atom.common.WORKSPACE_CONTEXT +import com.tencent.bk.devops.atom.common.utils.ReplacementUtils +import com.tencent.bk.devops.atom.enums.CharsetType +import com.tencent.bk.devops.atom.enums.OSType +import com.tencent.bk.devops.atom.exception.AtomException +import com.tencent.bk.devops.atom.pojo.AdditionalOptions +import com.tencent.bk.devops.atom.pojo.AgentEnv.getOS +import com.tencent.bk.devops.atom.pojo.ArtifactData +import com.tencent.bk.devops.atom.pojo.AtomResult +import com.tencent.bk.devops.atom.pojo.ReportData +import com.tencent.bk.devops.atom.pojo.ScriptRunAtomParam +import com.tencent.bk.devops.atom.pojo.ShellType +import com.tencent.bk.devops.atom.pojo.StringData +import com.tencent.bk.devops.atom.pojo.request.IndicatorCreate +import com.tencent.bk.devops.atom.pojo.request.QualityDataType +import com.tencent.bk.devops.atom.pojo.request.QualityOperation +import com.tencent.bk.devops.atom.spi.AtomService +import com.tencent.bk.devops.atom.spi.TaskAtom +import com.tencent.bk.devops.atom.utils.CommandLineUtils +import com.tencent.bk.devops.atom.utils.ScriptEnvUtils +import com.tencent.bk.devops.atom.utils.script.BashUtil +import com.tencent.bk.devops.atom.utils.script.BatScriptUtil +import com.tencent.bk.devops.atom.utils.script.PowerShellUtil +import com.tencent.bk.devops.atom.utils.script.PwshUtil +import com.tencent.bk.devops.atom.utils.script.PythonUtil +import com.tencent.bk.devops.atom.utils.script.ShUtil +import com.tencent.bk.devops.plugin.pojo.ErrorType +import org.apache.commons.lang3.StringUtils +import org.slf4j.LoggerFactory +import java.io.File + +/** + * @version 1.0.0 + */ +@AtomService(paramClass = ScriptRunAtomParam::class) +class ScriptRunAtom : TaskAtom { + + private val qualityApi = QualityApi() + + private val QUALITY_BOOLEAN_OPERATIONS = listOf(QualityOperation.EQ) + private val QUALITY_ALL_OPERATIONS = listOf( + QualityOperation.EQ, + QualityOperation.GT, + QualityOperation.GE, + QualityOperation.LT, + QualityOperation.LE + ) + + private val USER_ERROR_MESSAGE = """ +======脚本执行失败,问题排查指引====== +当脚本退出码非0时,执行失败。可以从以下路径进行分析: +1. 根据错误日志排查 +2. 在本地手动执行脚本。如果本地执行也失败,很可能是脚本逻辑问题; +如果本地OK,排查构建环境(比如环境依赖、或者代码变更等) + """ + + override fun execute(atomContext: AtomContext) { + + val param = atomContext.param as ScriptRunAtomParam + val result = atomContext.result + // 检查参数 + checkParam(param, result) + if (result.status != Status.success) { + return + } + val script = param.script + // 字符集选择,应对某些windows构建存在的字符集不匹配的问题 + val charSetType: CharsetType = try { + CharsetType.valueOf(param.charSetType) + } catch (ignore: Throwable) { + CharsetType.DEFAULT + } + // 获取运行时变量 + val runtimeVariables = atomContext.allParameters.map { it.key to it.value.toString() }.toMap() + // 获取系统类型 + val osType = getOS() + // 获取构建id + val buildId = atomContext.param.pipelineBuildId + /*拿到工作目录,后续文件操作将在工作目录进行*/ + val workspace = File(param.bkWorkspace) + /*替换脚本变量,目前变量已在引擎替换,此次预留暂时没有起实际作用*/ + val realCommand = parseTemplate(script, emptyMap(), workspace) + /*获取input中的变量,为了后续塞环境变量时不会意外将其塞入*/ + val paramClassName = param.javaClass.declaredFields.map { it.name }.toList() + + handle(result) { + /*获取脚本类型和系统类型*/ + val additionalOptions = AdditionalOptions(param.shell) + /*检查脚本类型是否适配当前系统*/ + checkOS(additionalOptions.shell, result) + try { + when (additionalOptions.shell) { + /*batch脚本,windows使用*/ + ShellType.CMD -> BatScriptUtil.execute( + script = realCommand, + buildId = buildId, + runtimeVariables = runtimeVariables, + dir = workspace, + charsetType = charSetType, + paramClassName = paramClassName + ) + /*shell脚本,一般linux和macos使用*/ + ShellType.BASH -> BashUtil.execute( + script = realCommand, + buildId = buildId, + runtimeVariables = runtimeVariables, + dir = workspace, + // 市场插件执行时buildEnvs已经写在环境变量中,作为子进程可以直接读取 + buildEnvs = emptyList(), + stepId = param.stepId, + paramClassName = paramClassName + ) + /*python脚本,需要目标构建机安装python3环境*/ + ShellType.PYTHON -> PythonUtil.execute( + script = realCommand, + buildId = buildId, + runtimeVariables = runtimeVariables, + dir = workspace, + buildEnvs = emptyList(), + stepId = param.stepId, + charsetType = charSetType, + paramClassName = paramClassName + ) + /*powershell脚本*/ + ShellType.POWERSHELL_CORE -> PwshUtil.execute( + script = realCommand, + buildId = buildId, + runtimeVariables = runtimeVariables, + dir = workspace, + stepId = param.stepId, + paramClassName = paramClassName + ) + /*powershell desktop脚本*/ + ShellType.POWERSHELL_DESKTOP -> PowerShellUtil.execute( + script = realCommand, + buildId = buildId, + runtimeVariables = runtimeVariables, + dir = workspace, + stepId = param.stepId, + paramClassName = paramClassName + ) + /*执行sh命令的脚本*/ + ShellType.SH -> ShUtil.execute( + script = realCommand, + buildId = buildId, + runtimeVariables = runtimeVariables, + dir = workspace, + // 市场插件执行时buildEnvs已经写在环境变量中,作为子进程可以直接读取 + buildEnvs = emptyList(), + stepId = param.stepId, + paramClassName = paramClassName + ) + else -> {} + } + + result.status = Status.success + result.message = "$osType 脚本执行成功" + } catch (taskError: AtomException) { + /*处理普通异常,这里是脚本逻辑抛出的异常*/ + logger.warn("Fail to run the script task") + logger.debug("TaskExecuteException|${taskError.message}", taskError) + result.status = Status.failure + result.message = "$osType 脚本执行失败" + /*返回失败以及对应的异常类型*/ + throw AtomException( + taskError.message + "\n$USER_ERROR_MESSAGE" + ) + } catch (ignore: Throwable) { + /*处理意外发生的异常,全局捕获*/ + logger.warn("Fail to run the script task") + logger.debug("Throwable|${ignore.message}", ignore) + result.status = Status.failure + result.message = "$osType 脚本执行失败" + /*返回user类型错误,一般用户使用错误会引起这种情况*/ + throw AtomException( + USER_ERROR_MESSAGE + ) + } finally { + // 写入上下文 + ScriptEnvUtils.getContext(buildId, workspace).plus(parseContextFromMultiLine(buildId, workspace)) + .forEach { (key, value) -> + /*根据拆分的格式来区分具体的类型*/ + val split = key.split(",") + when (split.size) { + /*只有一位,只需要输出为上下文变量格式*/ + 1 -> atomContext.result.data[key] = StringData(value) + /*有5位时区分了3种类型情况*/ + 5 -> + // 以逗号为分隔符 左右依次为name type label path reportType + atomContext.result.data[split[0]] = when (split[1]) { + /*指定string的类型输出为上下文*/ + "string" -> StringData(value) + /*指定位artifact的类型输出为构件*/ + "artifact" -> ArtifactData(setOf(value)) + /*指定为report 输出为报告*/ + "report" -> { + /*第三方报告*/ + if (split[4].contains("THIRDPARTY")) { + ReportData(split[2], value) + /*本地报告*/ + } else if (split[3].isNotBlank()) { + ReportData(split[2], split[3], value) + } else { + /*不支持其他report类型,如果有直接抛错*/ + throw AtomException( + "脚本执行失败 set-output report设置有误,请检查" + ) + } + } + /*不支持其他set-output类型,如果有直接抛错*/ + else -> throw AtomException( + "脚本执行失败 set-output设置有误,请检查: $split" + ) + } + /*不支持其他类型,如果有直接抛错*/ + else -> throw AtomException( + "脚本执行失败 set-output或set-variable设置有误,请检查: [${split.size == 1}]$split" + ) + } + } + // 写入环境变量 + ScriptEnvUtils.getEnv(buildId, workspace).forEach { (key, value) -> + atomContext.result.data[key] = StringData(value) + } + // 写入质量红线 + setGatewayValue(atomContext, workspace) + + /*脚本执行结束清理临时文件*/ + ScriptEnvUtils.cleanWhenEnd(buildId, workspace) + } + } + } + + /** + * 单独解析多行内容的输出 + */ + private fun parseContextFromMultiLine(buildId: String, workspace: File): Map { + val res = mutableMapOf() + /*获得多行的输出内容*/ + ScriptEnvUtils.getMultipleLines(buildId, workspace).forEach { + logger.debug("multiLine:$it") + /*解析variable或者output变量*/ + val str = CommandLineUtils.parseVariable(it) ?: CommandLineUtils.parseOutput(it) + if (str != null) { + val split = str.split("=", ignoreCase = false, limit = 2) + /*解码替换回来,得到多行内容*/ + res[split[0].trim()] = escapeData(split[1].trim()) + } + } + return res + } + + /** + * 统一的异常处理模块 + */ + private fun handle( + atomResult: AtomResult, + action: () -> T? + ) { + try { + action() + } catch (triggerE: AtomException) { + atomResult.message = triggerE.message + atomResult.errorCode = ErrorCode.USER_SCRIPT_TASK_FAIL + atomResult.errorType = ErrorType.USER.num + atomResult.status = Status.failure + } catch (e: Throwable) { + // unknown 情况归属为插件问题,需要插件方来处理 + atomResult.message = "Unknown Error: " + e.message + atomResult.errorCode = ErrorCode.USER_TASK_OPERATE_FAIL + atomResult.errorType = ErrorType.PLUGIN.num + atomResult.status = Status.error + } + } + + private fun escapeData(value: String): String { + return value + /*单引号的情况,是batch替换的结果,这里进行兼容*/ + .replace("'%0D'", "\r") + .replace("%0D", "\r") + .replace("'%0A'", "\n") + .replace("%0A", "\n") + .replace("'%25'", "%") + .replace("%25", "%") + } + + /** + * 设置红线指标 + */ + private fun setGatewayValue(atomContext: AtomContext, workspace: File) { + /*去拿红线指标输出文件*/ + val gatewayFile = File(workspace, ScriptEnvUtils.getQualityGatewayEnvFile()) + try { + /*不存在直接推出*/ + if (!gatewayFile.exists()) return + val data = mutableMapOf() + val title = mutableMapOf() + // 创建红线指标 + gatewayFile.readLines().forEach { + val split = it.split(",") + if (split.size > 2) { + /*格式不对直接抛错*/ + throw AtomException( + "much gateway parameter,count:${split.size}" + ) + } + val nameToValue = split.getOrNull(0) + val nameToTitle = split.getOrNull(1) + /*去做二次切分*/ + keyEqualValueInsertMap(nameToValue, data) + keyEqualValueInsertMap(nameToTitle, title) + } + /*创建红线指标*/ + updateIndicatorTitle( + userId = atomContext.param.pipelineStartUserId, + projectId = atomContext.param.projectName, + data = data, + nameMapToTitle = title + ) + // 将自定义指标的值入库 + saveQualityData( + taskId = atomContext.param.pipelineTaskId, + taskName = atomContext.param.taskName, + data = data + ) + + logger.info("save gateway value: $data") + } catch (ignore: Exception) { + /*处理异常*/ + logger.info("save gateway value fail: ${ignore.message}") + logger.error("setGatewayValue|${ignore.message}", ignore) + } finally { + /*执行完后删除文件*/ + gatewayFile.delete() + } + } + + private fun keyEqualValueInsertMap(nameToValue: String?, map: MutableMap) { + /*为空直接退出*/ + if (nameToValue.isNullOrBlank()) return + /*二次切分后确定最终的 key*/ + val key = nameToValue.split("=").getOrNull(0) ?: throw AtomException( + "Illegal gateway key set: $nameToValue" + ) + /*二次切分后确定最终的 value*/ + val value = nameToValue.split("=").getOrNull(1) ?: throw AtomException( + "Illegal gateway key set: $nameToValue" + ) + /*组装进map*/ + map[key] = value.trim() + } + +/* + private fun upsertIndicator( + userId: String, + projectId: String, + data: Map + ) { + val indicatorCreates = data.map { (name, value) -> + val dataType = getQualityDataType(value) + IndicatorCreate( + name = name, + cnName = name, + desc = "", + dataType = dataType, + operation = getQualityOperations(dataType == QualityDataType.BOOLEAN), + threshold = value, + elementType = QUALITY_ELEMENT_TYPE + ) + } + doUpsertIndicator(userId = userId, projectId = projectId, indicatorCreates = indicatorCreates) + }*/ + + private fun updateIndicatorTitle( + userId: String, + projectId: String, + data: Map, + nameMapToTitle: Map + ) { + // dataMapExample: pass_rate to 1.0 + val indicatorCreates = data.map { (name, value) -> + /*获取数据类型*/ + val dataType = getQualityDataType(value) + /*获取标题*/ + val title = getTitleOrDefault(name, nameMapToTitle) + IndicatorCreate( + name = name, + cnName = title, + dataType = dataType + ) + } + /*调接口创建红线指标*/ + doUpsertIndicator(userId = userId, projectId = projectId, indicatorCreates = indicatorCreates) + } + + /*获取标题的逻辑*/ + private fun getTitleOrDefault( + name: String, + nameMapToTitle: Map + ): String { + val title = nameMapToTitle[name] + /*变量中有则返回变量中的name*/ + if (title.isNullOrBlank()) { + return name + } + return title + } + + private fun doUpsertIndicator( + userId: String, + projectId: String, + indicatorCreates: List + ) { + /*通过接口创建红线指标*/ + qualityApi.upsertIndicator( + userId = userId, projectId = projectId, indicatorCreate = indicatorCreates + ).data.let { + if (it == null || !it) { + /*返回异常情况处理*/ + throw AtomException( + "创建 run 红线指标失败" + ) + } + } + } + + fun getQualityDataType(value: String): QualityDataType { + /*转int*/ + value.toIntOrNull().let { + if (it != null) { + return QualityDataType.INT + } + } + /*转float*/ + value.toFloatOrNull().let { + if (it != null) { + return QualityDataType.FLOAT + } + } + /*转boolean*/ + if (value == "true" || value == "false") { + return QualityDataType.BOOLEAN + } + /*其他类型直接抛错*/ + throw AtomException( + "gateWay error qualityDataType: $value,only support INT、FLOAT、BOOLEAN" + ) + } + +/* + private fun getQualityOperations(isBoolean: Boolean) = if (isBoolean) { + QUALITY_BOOLEAN_OPERATIONS + } else { + QUALITY_ALL_OPERATIONS + }*/ + + private fun saveQualityData( + taskId: String, + taskName: String, + data: Map + ) { + /*调接口保存红线数据*/ + qualityApi.saveScriptHisMetadata(taskId = taskId, taskName = taskName, data = data).data.let { + if (it == null || !it) { + /*返回异常处理*/ + throw AtomException( + "保存 run 红线数据失败" + ) + } + } + } + + /** + * 检查参数 + * @param param 请求参数 + * @param result 结果 + */ + private fun checkParam(param: ScriptRunAtomParam, result: AtomResult) { + // 参数检查 + if (StringUtils.isBlank(param.script)) { + result.status = Status.failure // 状态设置为失败 + result.message = "脚本内容不能为空" // 失败信息回传给插件执行框架会打印出结果 + } + } + + private fun parseTemplate(command: String, data: Map, dir: File): String { + /*通用变量替换逻辑*/ + return ReplacementUtils.replace( + command = command, + replacement = object : ReplacementUtils.KeyReplacement { + override fun getReplacement(key: String): String? = if (data[key] != null) { + data[key] + } else { + null + } + }, + /*额外需要替换的变量*/ + contextMap = mapOf( + WORKSPACE_CONTEXT to dir.absolutePath, + CI_TOKEN_CONTEXT to (data[CI_TOKEN_CONTEXT] ?: ""), + JOB_OS_CONTEXT to getOS().name + ) + ) + } + + /* + * 检查系统类型 + */ + private fun checkOS(shellType: ShellType, result: AtomResult) { + val os = getOS() + when { + /*非windows不能使用cmd(batch)*/ + os != OSType.WINDOWS && shellType == ShellType.CMD || + /*非windows不能使用POWERSHELL_DESKTOP*/ + os != OSType.WINDOWS && shellType == ShellType.POWERSHELL_DESKTOP || + /*windows不能使用sh*/ + os == OSType.WINDOWS && shellType == ShellType.SH -> { + result.status = Status.failure + result.message = "$os 脚本执行失败" + /*不满足的情况直接用户抛错*/ + throw AtomException( + "The current system(${os.name}) does not support: ${shellType.shellName}" + ) + } + } + } + + companion object { + private val logger = LoggerFactory.getLogger(ScriptRunAtom::class.java) + } +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/ScriptRunAtomTest.kt b/src/main/kotlin/com/tencent/bk/devops/atom/ScriptRunAtomTest.kt new file mode 100644 index 0000000..2564894 --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/ScriptRunAtomTest.kt @@ -0,0 +1,232 @@ +package com.tencent.bk.devops.atom + +import com.tencent.bk.devops.atom.pojo.ScriptRunAtomParam +import junit.framework.TestCase + +class ScriptRunAtomTest : TestCase() { + + private val atom = ScriptRunAtom() + + fun testRun() { + atom.execute(AtomContext(ScriptRunAtomParam::class.java)) + } +/* + fun testGetQualityDataType() { + val cls = ScriptRunAtom() + assertEquals(QualityDataType.INT, cls.getQualityDataType("0")) + assertEquals(QualityDataType.FLOAT, cls.getQualityDataType("0.123")) + assertEquals(QualityDataType.BOOLEAN, cls.getQualityDataType("true")) + try { + cls.getQualityDataType("xxx") + } catch (e: TaskExecuteException) { + assertEquals(e.errorMsg, "error qualityDataType: xxx") + } catch (e: Exception) { + assert(false) + } + } + + companion object { + private val OUTPUT_NAME = Pattern.compile("name=([^,:=\\s]*)") + private val OUTPUT_TYPE = Pattern.compile("type=([^,:=\\s]*)") + private val OUTPUT_LABEL = Pattern.compile("label=([^,:=\\s]*)") + private val OUTPUT_PATH = Pattern.compile("path=([^,:=\\s]*)") + private val OUTPUT_REPORT_TYPE = Pattern.compile("reportType=([^,:=\\s]*)") + private val OUTPUT_GATE_TITLE = Pattern.compile("title=([^,:=\\s]*)") + } + + private fun getOutputMarcher(macher: Matcher): String? { + return with(macher) { + if (this.find()) { + this.group(1) + } else null + } + } + + fun appendOutputToFile( + tmpLine: String, + workspace: File?, + resultLogFile: String?, + stepId: String? + ) { + val pattenOutput = "::set-output\\s.*" + val prefixOutput = "::set-output " + if (Pattern.matches(pattenOutput, tmpLine)) { + val value = tmpLine.removePrefix(prefixOutput) + + val nameMatcher = getOutputMarcher(OUTPUT_NAME.matcher(value)) ?: "" + val typeMatcher = getOutputMarcher(OUTPUT_TYPE.matcher(value)) ?: "string" // type 默认为string + val labelMatcher = getOutputMarcher(OUTPUT_LABEL.matcher(value)) ?: "" + val pathMatcher = getOutputMarcher(OUTPUT_PATH.matcher(value)) ?: "" + val reportTypeMatcher = getOutputMarcher(OUTPUT_REPORT_TYPE.matcher(value)) ?: "" + + + val keyValue = value.split("::") + if (keyValue.size >= 2) { + // 以逗号为分隔符 左右依次为name type label path reportType + println( + "$nameMatcher," + + "$typeMatcher," + + "$labelMatcher," + + "$pathMatcher," + + "$reportTypeMatcher=${value.removePrefix("${keyValue[0]}::")}\n" + ) + } + } + } + + + private fun appendGateToFile( + tmpLine: String, + list: MutableList + ) { + val pattenOutput = "[\"]?::set-gate-value\\s(.*)" + val prefixOutput = "::set-gate-value " + if (Pattern.matches(pattenOutput, tmpLine)) { + val value = tmpLine.removeSurrounding("\"").removePrefix(prefixOutput) + val name = getOutputMarcher(OUTPUT_NAME.matcher(value)) + val title = getOutputMarcher(OUTPUT_GATE_TITLE.matcher(value)) + val keyValue = value.split("::") + if (keyValue.size >= 2) { + // pass_rate=1,pass_rate=通过率\n + var text = "$name=${value.removePrefix("${keyValue[0]}::")}" + if (!title.isNullOrBlank()) { + text = text.plus(",$name=$title") + } + list.add("$text") + } + } + } + + fun testGateOutPut() { + val testCases = listOf( + "::set-gate-value name=pass_rate::0.9", + "::set-gate-value name=pass_rate,title=测试用例通过率::0.9", + "::set-gate-value name=pass_rate,title=测试全部通过率::0.1", + "::set-gate-value name=errorCount,title=错误总数::100000.00001", + "::set-gate-value name=IntTest,title=整数计数::100", + "::set-gate-value name=isUpload,title=是否上传::true" + ) + val expectResult = listOf( + "pass_rate=0.9", + "pass_rate=0.9,pass_rate=测试用例通过率", + "pass_rate=0.1,pass_rate=测试全部通过率", + "errorCount=100000.00001,errorCount=错误总数", + "IntTest=100,IntTest=整数计数", + "isUpload=true,isUpload=是否上传" + ) + val gateFile = mutableListOf() + testCases.forEach { + appendGateToFile(it, gateFile) + } + checkRealAndExpect(gateFile, expectResult) + val data = mutableMapOf() + val title = mutableMapOf() + gateFile.forEach { + val split = it.split(",") + val nameToValue = split.getOrNull(0) + val nameToTitle = split.getOrNull(1) + keyEqualValueInsertMap(nameToValue, data) + keyEqualValueInsertMap(nameToTitle, title) + } + data.values.forEach { + getQualityDataType(it) + } + // titleCheck + assertEquals("测试全部通过率", title["pass_rate"]) + assertEquals("错误总数", title["errorCount"]) + assertEquals("整数计数", title["IntTest"]) + assertEquals("是否上传", title["isUpload"]) + // dataCheck + assertEquals("0.1", data["pass_rate"]) + assertEquals("true", data["isUpload"]) + assertEquals("100", data["IntTest"]) + } + private fun keyEqualValueInsertMap(nameToValue: String?, map: MutableMap) { + if (nameToValue.isNullOrBlank()) return + val key = nameToValue.split("=").getOrNull(0) ?: throw TaskExecuteException( + errorMsg = "Illegal gateway key set: $nameToValue", + errorType = ErrorType.USER, + errorCode = ErrorCode.USER_INPUT_INVAILD + ) + val value = nameToValue.split("=").getOrNull(1) ?: throw TaskExecuteException( + errorMsg = "Illegal gateway key set: $nameToValue", + errorType = ErrorType.USER, + errorCode = ErrorCode.USER_INPUT_INVAILD + ) + map[key] = value.trim() + } + + + private fun checkRealAndExpect(real: MutableList, expect: List) { + real.forEachIndexed { index, _ -> + assertEquals(expect[index], real[index]) + } + } + + private fun getQualityDataType(value: String): QualityDataType { + value.toIntOrNull().let { + if (it != null) { + return QualityDataType.INT + } + } + value.toFloatOrNull().let { + if (it != null) { + return QualityDataType.FLOAT + } + } + if (value == "true" || value == "false") { + return QualityDataType.BOOLEAN + } + throw TaskExecuteException( + errorMsg = "error qualityDataType: $value", + errorType = ErrorType.USER, + errorCode = ErrorCode.USER_INPUT_INVAILD + ) + } + + fun testOutPut() { + val a = "::set-output name=var_4,type=report,label=测试报告名称,reportType=THIRDPARTY::https://www.xxx.com/" + val b = "::set-output name=var_3,type=report,label=测试报告名称,path=report/::index.html" + val c = "::set-output name=var_2,type=artifact::*.txt" + val d = "::set-output name=var_1::1" + appendOutputToFile(a, null, null, null) + appendOutputToFile(b, null, null, null) + appendOutputToFile(c, null, null, null) + appendOutputToFile(d, null, null, null) + } + + + fun testEnvUtils() { + val command = " echo \${{ ci.workspace }}\n" + + " echo envs.env_a=\${{ envs.env_a }}, env_a=\$env_a\n" + + " echo envs.env_b=\${{ envs.env_b }}, env_b=\$env_b\n" + + " echo envs.env_c=\${{ envs.env_c }}, env_c=\$env_c\n" + + " echo envs.env_d=\${{ envs.env_d }}, env_d=\$env_d\n" + + " echo envs.env_e=\${{ envs.env_e }}, env_e=\$env_e\n" + + " echo envs.a=\${{ envs.a }}, a=\$a\n" + + "\n" + + " echo ::set-output name=a::i am a at step_1" + val data = mapOf("envs.env_a" to "test") +// print(command) + val res = ReplacementUtils.replace( + command = command, + replacement = object : ReplacementUtils.KeyReplacement { + override fun getReplacement(key: String): String? = if (data[key] != null) { + data[key] + } else { + null + } + }, + contextMap = null + ) + assertEquals(res, " echo \\\${{ ci.workspace }}\n" + + " echo envs.env_a=test, env_a=\$env_a\n" + + " echo envs.env_b=\\\${{ envs.env_b }}, env_b=\$env_b\n" + + " echo envs.env_c=\\\${{ envs.env_c }}, env_c=\$env_c\n" + + " echo envs.env_d=\\\${{ envs.env_d }}, env_d=\$env_d\n" + + " echo envs.env_e=\\\${{ envs.env_e }}, env_e=\$env_e\n" + + " echo envs.a=\\\${{ envs.a }}, a=\$a\n" + + "\n" + + " echo ::set-output name=a::i am a at step_1") + }*/ +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/api/QualityApi.kt b/src/main/kotlin/com/tencent/bk/devops/atom/api/QualityApi.kt new file mode 100644 index 0000000..3f694bf --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/api/QualityApi.kt @@ -0,0 +1,43 @@ +package com.tencent.bk.devops.atom.api + +import com.fasterxml.jackson.core.type.TypeReference +import com.tencent.bk.devops.atom.common.QUALITY_ELEMENT_TYPE +import com.tencent.bk.devops.atom.pojo.request.IndicatorCreate +import com.tencent.bk.devops.atom.utils.json.JsonUtil +import com.tencent.bk.devops.plugin.pojo.Result +import okhttp3.RequestBody +import org.slf4j.LoggerFactory + +class QualityApi() : BaseApi() { + + private val logger = LoggerFactory.getLogger(QualityApi::class.java) + + private val urlPrefix = "/quality/api/build" + + fun upsertIndicator( + userId: String, + projectId: String, + indicatorCreate: List + ): Result { + val url = "$urlPrefix/indicator/v3/project/$projectId/upsertIndicator" + val requestBody = RequestBody.create(JSON_CONTENT_TYPE, JsonUtil.toJson(indicatorCreate)) + val request = buildPost(url, requestBody, mutableMapOf("X-DEVOPS-UID" to userId)) + val responseContent = request(request, "创建红线自定义指标失败") + + return JsonUtil.fromJson(responseContent, object : TypeReference>() {}) + } + + fun saveScriptHisMetadata( + taskId: String, + taskName: String, + data: Map + ): Result { + val url = "$urlPrefix/metadata/saveHisMetadata" + + "?elementType=$QUALITY_ELEMENT_TYPE&taskId=$taskId&taskName=$taskName" + val requestBody = RequestBody.create(JSON_CONTENT_TYPE, JsonUtil.toJson(data)) + val request = buildPost(url, requestBody, mutableMapOf()) + val responseContent = request(request, "保存红线数据失败") + + return JsonUtil.fromJson(responseContent, object : TypeReference>() {}) + } +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/common/Constants.kt b/src/main/kotlin/com/tencent/bk/devops/atom/common/Constants.kt new file mode 100644 index 0000000..2d0e7e7 --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/common/Constants.kt @@ -0,0 +1,42 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.common + +const val BUILD_ID = "devops.build.id" + +const val BUILD_TYPE = "build.type" + +const val WORKSPACE_ENV = "WORKSPACE" + +const val WORKSPACE_CONTEXT = "ci.workspace" + +const val CI_TOKEN_CONTEXT = "ci.token" + +const val JOB_OS_CONTEXT = "job.os" + +const val QUALITY_ELEMENT_TYPE = "run" diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/common/ErrorCode.kt b/src/main/kotlin/com/tencent/bk/devops/atom/common/ErrorCode.kt new file mode 100644 index 0000000..2f39b44 --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/common/ErrorCode.kt @@ -0,0 +1,19 @@ +package com.tencent.bk.devops.atom.common + +object ErrorCode { + // 插件执行错误 + const val PLUGIN_DEFAULT_ERROR = 2199001 // 插件异常默认 + const val PLUGIN_CREATE_QUALITY_INDICATOR_ERROR = 2199002 + const val PLUGIN_SAVE_QUALITY_DATA_ERROR = 2199003 + // 用户使用错误 + const val USER_INPUT_INVAILD = 2199002 // 用户输入数据有误 + const val USER_RESOURCE_NOT_FOUND = 2199003 // 找不到对应系统资源 + const val USER_TASK_OPERATE_FAIL = 2199004 // 插件执行过程出错 + const val USER_JOB_OUTTIME_LIMIT = 2199005 // 用户Job排队超时(自行限制) + const val USER_TASK_OUTTIME_LIMIT = 2199006 // 用户插件执行超时(自行限制) + const val USER_QUALITY_CHECK_FAIL = 2199007 // 质量红线检查失败 + const val USER_QUALITY_REVIEW_ABORT = 2199008 // 质量红线审核驳回 + const val USER_SCRIPT_COMMAND_INVAILD = 2199009 // 脚本命令无法正常执行 + const val USER_STAGE_FASTKILL_TERMINATE = 2199010 // 因用户配置了FastKill导致的终止执行 + const val USER_SCRIPT_TASK_FAIL = 2199011 // bash脚本发生用户错误 +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/common/RunEnvHelper.kt b/src/main/kotlin/com/tencent/bk/devops/atom/common/RunEnvHelper.kt new file mode 100644 index 0000000..06f4556 --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/common/RunEnvHelper.kt @@ -0,0 +1,156 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.common + +import com.fasterxml.jackson.core.type.TypeReference +import com.tencent.bk.devops.atom.pojo.StringData +import com.tencent.bk.devops.atom.utils.http.SdkUtils +import com.tencent.bk.devops.plugin.utils.JsonUtil +// import com.tencent.devops.api.CodeccApi +// import com.tencent.devops.pojo.BuildScriptType +// import com.tencent.devops.pojo.CodeccCheckAtomParam +// import com.tencent.devops.pojo.CodeccCheckAtomParamV3 +// import com.tencent.devops.pojo.CodeccExecuteConfig +// import com.tencent.devops.pojo.OSType +// import com.tencent.devops.utils.common.AgentEnv +import java.io.File + +object RunEnvHelper { + +// private val api = CodeccApi() + + private val ENV_FILES = arrayOf("result.log", "result.ini") + + private var codeccWorkspace: File? = null + + init { + // 第三方构建机 +// if (getOS().isThirdParty()) { +// println("[初始化] 检测到这是第三方构建机") +// } + } + + fun getRunEnv(workspace: String): Map { + val result = mutableMapOf() + ENV_FILES.forEach { result.putAll(readScriptEnv(File(workspace), it)) } + return result + } + + private fun readScriptEnv(workspace: File, file: String): Map { + val f = File(workspace, file) + if (!f.exists()) { + return mapOf() + } + if (f.isDirectory) { + return mapOf() + } + + val lines = f.readLines() + if (lines.isEmpty()) { + return mapOf() + } + // KEY-VALUE + return lines.filter { it.contains("=") }.map { + val split = it.split("=", ignoreCase = false, limit = 2) + split[0].trim() to StringData(split[1].trim()) + }.toMap() + } + +// fun saveTask(atomContext: AtomContext) { +// with(atomContext.param) { +// api.saveTask(projectName, pipelineId, pipelineBuildId) +// } +// } + +// fun getScriptType(): BuildScriptType { +// return when (getOS()) { +// OSType.MAC_OS, OSType.LINUX -> BuildScriptType.SHELL +// OSType.WINDOWS -> BuildScriptType.BAT +// else -> BuildScriptType.SHELL +// } +// } + +// fun getOS(): OSType { +// val osName = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH) +// return if (osName.indexOf(string = "mac") >= 0 || osName.indexOf("darwin") >= 0) { +// OSType.MAC_OS +// } else if (osName.indexOf("win") >= 0) { +// OSType.WINDOWS +// } else if (osName.indexOf("nux") >= 0) { +// OSType.LINUX +// } else { +// OSType.OTHER +// } +// } + + fun getVariable(): Map { + val map = JsonUtil.to(File(SdkUtils.getInputFile()).readText(), object : TypeReference>() {}) + return map.map { it.key to it.value.toString() }.toMap() + } + +// // 第三方构建机初始化 +// fun thirdInit(codeccExecuteConfig: CodeccExecuteConfig) { +// // 第三方构建机安装环境 +// if (AgentEnv.isThirdParty()) { +// when (getOS()) { +// OSType.LINUX -> { +// CodeccInstaller.setUpPython3(codeccExecuteConfig.atomContext.param) +// } +// else -> { +// } +// } +// } +// } + +// fun getCodeccWorkspace(param: CodeccCheckAtomParamV3): File { +// if (codeccWorkspace != null) return codeccWorkspace!! +// +// val workspace = File(param.bkWorkspace) +// val buildId = param.pipelineBuildId +// +// // Copy the nfs coverity file to workspace +// println("[初始化] get the workspace: ${workspace.canonicalPath}") +// println("[初始化] get the workspace parent: ${workspace.parentFile?.canonicalPath} | '${File.separatorChar}'") +// println("[初始化] get the workspace parent string: ${workspace.parent}") +// +// val tempDir = File(workspace, ".temp") +// println("[初始化] get the workspace path parent: ${tempDir.canonicalPath}") +// codeccWorkspace = File(tempDir, "codecc_$buildId") +// if (!codeccWorkspace!!.exists()) { +// codeccWorkspace!!.mkdirs() +// } +// +// return codeccWorkspace!! +// } + + fun deleteCodeccWorkspace() { + println("delete the workspace path parent: $codeccWorkspace") + if (codeccWorkspace != null) { + codeccWorkspace!!.deleteOnExit() + } + } +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/common/utils/ReplacementUtils.kt b/src/main/kotlin/com/tencent/bk/devops/atom/common/utils/ReplacementUtils.kt new file mode 100644 index 0000000..e3c296d --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/common/utils/ReplacementUtils.kt @@ -0,0 +1,105 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.common.utils + +import java.util.regex.Matcher +import java.util.regex.Pattern + +object ReplacementUtils { + + fun replace(command: String, replacement: KeyReplacement): String { + return replace(command, replacement, emptyMap()) + } + + fun replace( + command: String, + replacement: KeyReplacement, + contextMap: Map? = emptyMap() + ): String { + if (command.isBlank()) { + return command + } + val sb = StringBuilder() + + val lines = command.lines() + lines.forEachIndexed { index, line -> + // 忽略注释 + val template = if (line.trim().startsWith("#")) { + line + } else { + parseTemplate(line, replacement, contextMap) + } + sb.append(template) + if (index != lines.size - 1) { + sb.append("\n") + } + } + return sb.toString() + } + + private fun parseTemplate( + command: String, + replacement: KeyReplacement, + contextMap: Map?, + depth: Int = 1 + ): String { + if (depth < 0) { + return command + } + val matcher = tPattern.matcher(command) + val buff = StringBuffer() + while (matcher.find()) { + val key = (matcher.group("single") ?: matcher.group("double")).trim() + var value = replacement.getReplacement(key) ?: contextMap?.get(key) + if (value == null) { + // 修复错误的替换(bad substitution)错误 + if (matcher.group().startsWith("\${{")) { + value = "\\${matcher.group()}" + } else { + value = matcher.group() + } + } else { + if (depth > 0 && tPattern.matcher(value).find()) { + value = parseTemplate(value, replacement, contextMap, depth = depth - 1) + } + } + matcher.appendReplacement(buff, Matcher.quoteReplacement(value)) + } + matcher.appendTail(buff) + return if (buff.isNotEmpty()) buff.toString() else command + } + + private val tPattern = Pattern.compile("(\\$[{](?[^$^{}]+)})|(\\$[{]{2}(?[^$^{}]+)[}]{2})") + + interface KeyReplacement { + /** + * 如果[key]替换不成功需要返回null,不建议直接返回[key],避免无法判断到底替换成功 + */ + fun getReplacement(key: String): String? + } +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/enums/CharsetType.kt b/src/main/kotlin/com/tencent/bk/devops/atom/enums/CharsetType.kt new file mode 100644 index 0000000..9e4c3aa --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/enums/CharsetType.kt @@ -0,0 +1,37 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.enums + +enum class CharsetType { + /*默认类型*/ + DEFAULT, + /*UTF_8*/ + UTF_8, + /*GBK*/ + GBK +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/enums/OSType.kt b/src/main/kotlin/com/tencent/bk/devops/atom/enums/OSType.kt new file mode 100644 index 0000000..275093e --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/enums/OSType.kt @@ -0,0 +1,43 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.enums + +/** + * + * Powered By Tencent + */ +enum class OSType { + /*WINDOWS系统*/ + WINDOWS, + /*LINUX系统*/ + LINUX, + /*MAC_OS系统*/ + MAC_OS, + /*其他系统*/ + OTHER +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/pojo/AdditionalOptions.kt b/src/main/kotlin/com/tencent/bk/devops/atom/pojo/AdditionalOptions.kt new file mode 100644 index 0000000..25e1965 --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/pojo/AdditionalOptions.kt @@ -0,0 +1,89 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.pojo + +import com.tencent.bk.devops.atom.enums.OSType +import com.tencent.bk.devops.atom.exception.AtomException + +data class AdditionalOptions( + var shell: ShellType +) { + constructor(shell: String) : this(ShellType.BASH) { + /*用户没有配置脚本类型就按照系统类型默认选择*/ + this.shell = if (shell.isNullOrBlank()) { + ShellType.get(AgentEnv.getOS()) + } else { + ShellType.get(shell) + } + } +} + +enum class ShellType(val shellName: String) { + /*bash*/ + BASH("bash"), + + /*cmd*/ + CMD("cmd"), + + /*powershell*/ + POWERSHELL_CORE("pwsh"), + + /*powershell*/ + POWERSHELL_DESKTOP("powershell"), + + /*python*/ + PYTHON("python"), + + /*sh命令*/ + SH("sh"), + + /*按系统默认*/ + AUTO("auto"); + + companion object { + fun get(value: String): ShellType { + if (value == AUTO.shellName) { + return get(AgentEnv.getOS()) + } + values().forEach { + if (value == it.shellName) return it + } + throw AtomException( + "The current system(${AgentEnv.getOS()}) not support $value yet" + ) + } + + fun get(value: OSType): ShellType { + return when { + value == OSType.WINDOWS -> CMD + value == OSType.LINUX -> BASH + else -> BASH + } + } + } +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/pojo/AgentEnv.kt b/src/main/kotlin/com/tencent/bk/devops/atom/pojo/AgentEnv.kt new file mode 100644 index 0000000..d6ef328 --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/pojo/AgentEnv.kt @@ -0,0 +1,34 @@ +package com.tencent.bk.devops.atom.pojo + +import com.tencent.bk.devops.atom.enums.OSType +import org.slf4j.LoggerFactory +import java.util.Locale + +object AgentEnv { + + private val logger = LoggerFactory.getLogger(AgentEnv::class.java) + + private var os: OSType? = null + + /*获取系统类型*/ + fun getOS(): OSType { + if (os == null) { + synchronized(this) { + if (os == null) { + val os = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH) + logger.info("Get the os name - ($os)") + this.os = if (os.indexOf(string = "mac") >= 0 || os.indexOf("darwin") >= 0) { + OSType.MAC_OS + } else if (os.indexOf("win") >= 0) { + OSType.WINDOWS + } else if (os.indexOf("nux") >= 0) { + OSType.LINUX + } else { + OSType.OTHER + } + } + } + } + return os!! + } +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/pojo/BuildEnv.kt b/src/main/kotlin/com/tencent/bk/devops/atom/pojo/BuildEnv.kt new file mode 100644 index 0000000..2edea29 --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/pojo/BuildEnv.kt @@ -0,0 +1,35 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.pojo + +data class BuildEnv( + val name: String, + val version: String, + val binPath: String, + val env: Map +) diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/pojo/ScriptRunAtomParam.kt b/src/main/kotlin/com/tencent/bk/devops/atom/pojo/ScriptRunAtomParam.kt new file mode 100644 index 0000000..cf63499 --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/pojo/ScriptRunAtomParam.kt @@ -0,0 +1,22 @@ +package com.tencent.bk.devops.atom.pojo + +import com.fasterxml.jackson.annotation.JsonProperty +import lombok.Data +import lombok.EqualsAndHashCode + +/** + * 插件参数定义 + * @version 1.0 + */ +@Data +@EqualsAndHashCode(callSuper = true) +class ScriptRunAtomParam : AtomBaseParam() { + + /*脚本内容*/ + val script: String = "" + /*字符集类型*/ + @JsonProperty("charsetType") + val charSetType: String = "" + /*脚本类型*/ + val shell: String = "" +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/pojo/request/IndicatorCreate.kt b/src/main/kotlin/com/tencent/bk/devops/atom/pojo/request/IndicatorCreate.kt new file mode 100644 index 0000000..d54db84 --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/pojo/request/IndicatorCreate.kt @@ -0,0 +1,65 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.pojo.request + +data class IndicatorCreate( + val name: String, + val cnName: String = "", + val desc: String = "", + val dataType: QualityDataType, + val operation: List = listOf(), + val threshold: String = "", + val elementType: String = "" +) + +enum class QualityDataType { + INT, + BOOLEAN, + FLOAT, + STRING +} + +enum class QualityOperation { + GT, + GE, + LT, + LE, + EQ; + + companion object { + fun convertToSymbol(operation: QualityOperation): String { + return when (operation) { + GT -> ">" + GE -> ">=" + LT -> "<" + LE -> "<=" + EQ -> "=" + } + } + } +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/utils/CommandLineExecutor.kt b/src/main/kotlin/com/tencent/bk/devops/atom/utils/CommandLineExecutor.kt new file mode 100644 index 0000000..654a5f1 --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/utils/CommandLineExecutor.kt @@ -0,0 +1,194 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.utils + +import org.apache.commons.exec.CommandLine +import org.apache.commons.exec.DefaultExecutor +import org.apache.commons.exec.ExecuteStreamHandler +import org.apache.commons.exec.Executor +import org.slf4j.LoggerFactory +import java.io.File +import java.io.IOException + +@Suppress("ALL") +class CommandLineExecutor : DefaultExecutor() { + + /** the first exception being caught to be thrown to the caller */ + private var exceptionCaught: IOException? = null + + override fun execute(command: CommandLine, environment: MutableMap?): Int { + if (workingDirectory != null && !workingDirectory.exists()) { + throw IOException("$workingDirectory doesn't exist.") + } + + return executeInternal(command, environment, workingDirectory, streamHandler) + } + + /** + * Execute an internal process. If the executing thread is interrupted while waiting for the + * child process to return the child process will be killed. + * + * @param command the command to execute + * @param environment the execution environment + * @param dir the working directory + * @param streams process the streams (in, out, err) of the process + * @return the exit code of the process + * @throws IOException executing the process failed + */ + private fun executeInternal( + command: CommandLine, + environment: Map?, + dir: File, + streams: ExecuteStreamHandler + ): Int { + + setExceptionCaught(null) + + val process = this.launch(command, environment, dir) + + try { + streams.setProcessInputStream(process.outputStream) + streams.setProcessOutputStream(process.inputStream) + streams.setProcessErrorStream(process.errorStream) + } catch (e: IOException) { + process.destroy() + throw e + } + + streams.start() + try { + + // add the process to the list of those to destroy if the VM exits + if (this.processDestroyer != null) { + this.processDestroyer.add(process) + } + + // associate the watchdog with the newly created process + if (watchdog != null) { + watchdog.start(process) + } + + var exitValue = Executor.INVALID_EXITVALUE + + try { + exitValue = process.waitFor() + } catch (e: InterruptedException) { + process.destroy() + } finally { + // see http://bugs.sun.com/view_bug.do?bug_id=6420270 + // see https://issues.apache.org/jira/browse/EXEC-46 + // Process.waitFor should clear interrupt status when throwing InterruptedException + // but we have to do that manually + Thread.interrupted() + } + + if (watchdog != null) { + watchdog.stop() + } + + try { + streams.stop() + } catch (e: IOException) { + setExceptionCaught(e) + } + + closeProcessStreams(process) + + if (getExceptionCaught() != null) { + throw getExceptionCaught()!! + } + + if (watchdog != null) { + try { + watchdog.checkException() + } catch (e: IOException) { + throw e + } catch (e: Exception) { + throw IOException(e.message) + } + } + + return exitValue + } finally { + // remove the process to the list of those to destroy if the VM exits + if (this.processDestroyer != null) { + this.processDestroyer.remove(process) + } + } + } + + /** + * Close the streams belonging to the given Process. + * + * @param process the Process. + */ + private fun closeProcessStreams(process: Process) { + + try { + process.inputStream.close() + } catch (e: IOException) { + setExceptionCaught(e) + } + + try { + process.outputStream.close() + } catch (e: IOException) { + setExceptionCaught(e) + } + + try { + process.errorStream.close() + } catch (e: IOException) { + setExceptionCaught(e) + } + } + + /** + * Keep track of the first IOException being thrown. + * + * @param e the IOException + */ + private fun setExceptionCaught(e: IOException?) { + if (this.exceptionCaught == null) { + this.exceptionCaught = e + } + } + + /** + * Get the first IOException being thrown. + * + * @return the first IOException being caught + */ + private fun getExceptionCaught(): IOException? { + return this.exceptionCaught + } + + companion object { + private val logger = LoggerFactory.getLogger(CommandLineExecutor::class.java) + } +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/utils/CommandLineUtils.kt b/src/main/kotlin/com/tencent/bk/devops/atom/utils/CommandLineUtils.kt new file mode 100644 index 0000000..6df34ea --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/utils/CommandLineUtils.kt @@ -0,0 +1,286 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.utils + +import com.tencent.bk.devops.atom.enums.CharsetType +import com.tencent.bk.devops.atom.exception.AtomException +import org.apache.commons.exec.CommandLine +import org.apache.commons.exec.LogOutputStream +import org.apache.commons.exec.PumpStreamHandler +import org.slf4j.LoggerFactory +import java.io.ByteArrayOutputStream +import java.io.File +import java.nio.charset.Charset +import java.util.regex.Matcher +import java.util.regex.Pattern + +object CommandLineUtils { + + private val logger = LoggerFactory.getLogger(CommandLineUtils::class.java) + /*OUTPUT_NAME 正则匹配规则*/ + private val OUTPUT_NAME = Pattern.compile("name=([^,:=\\s]*)") + /*OUTPUT_TYPE 正则匹配规则*/ + private val OUTPUT_TYPE = Pattern.compile("type=([^,:=\\s]*)") + /*OUTPUT_LABEL 正则匹配规则*/ + private val OUTPUT_LABEL = Pattern.compile("label=([^,:=\\s]*)") + /*OUTPUT_PATH 正则匹配规则*/ + private val OUTPUT_PATH = Pattern.compile("path=([^,:=\\s]*)") + /*OUTPUT_REPORT_TYPE 正则匹配规则*/ + private val OUTPUT_REPORT_TYPE = Pattern.compile("reportType=([^,:=\\s]*)") + /*OUTPUT_GATE_TITLE 正则匹配规则*/ + private val OUTPUT_GATE_TITLE = Pattern.compile("title=([^,:=\\s]*)") + + private val lineParser = listOf(OauthCredentialLineParser()) + + fun execute( + command: String, + workspace: File?, + print2Logger: Boolean, + prefix: String = "", + executeErrorMessage: String? = null, + buildId: String, + stepId: String? = null, + charSetType: CharsetType? = null + ): String { + /*result 用于装载返回信息*/ + val result = StringBuilder() + logger.debug("will execute command >>> $command") + + /*解析命令*/ + val cmdLine = CommandLine.parse(command) + /*生成executor*/ + val executor = CommandLineExecutor() + if (workspace != null) { + /*工作空间已知则装载进去*/ + executor.workingDirectory = workspace + } + /*获取上下文文件*/ + val contextLogFile = if (buildId.isNotBlank()) { + ScriptEnvUtils.getContextFile(buildId) + } else { + null + } + + /*获取字符集编码类型*/ + val charset = when (charSetType) { + CharsetType.UTF_8 -> "UTF-8" + CharsetType.GBK -> "GBK" + else -> Charset.defaultCharset().name() + } + /*定义output标准输出流*/ + val outputStream = object : LogOutputStream() { + override fun processBuffer() { + val privateStringField = LogOutputStream::class.java.getDeclaredField("buffer") + privateStringField.isAccessible = true + /*反射拿到buffer 解决字符集编码问题*/ + val buffer = privateStringField.get(this) as ByteArrayOutputStream + processLine(buffer.toString(charset)) + /*手动reset*/ + buffer.reset() + } + + override fun processLine(line: String?, level: Int) { + if (line == null) + return + + /*补齐前缀*/ + var tmpLine: String = prefix + line + + lineParser.forEach { + /*做日志脱敏*/ + tmpLine = it.onParseLine(tmpLine) + } + if (print2Logger) { + /*提取特殊内容到文件进行持久化存储并输出到上下文*/ + appendResultToFile(executor.workingDirectory, contextLogFile, tmpLine) + } + println(tmpLine) + /*装载result*/ + result.append(tmpLine).append("\n") + } + } + /*定义error输出流*/ + val errStream = object : LogOutputStream() { + override fun processBuffer() { + val privateStringField = LogOutputStream::class.java.getDeclaredField("buffer") + privateStringField.isAccessible = true + /*反射拿到buffer 解决字符集编码问题*/ + val buffer = privateStringField.get(this) as ByteArrayOutputStream + processLine(buffer.toString(charset)) + /*手动reset*/ + buffer.reset() + } + + override fun processLine(line: String?, level: Int) { + if (line == null) + return + + /*补齐前缀*/ + var tmpLine: String = prefix + line + + lineParser.forEach { + /*做日志脱敏*/ + tmpLine = it.onParseLine(tmpLine) + } + if (print2Logger) { + /*提取特殊内容到文件进行持久化存储并输出到上下文*/ + appendResultToFile(executor.workingDirectory, contextLogFile, tmpLine) + } + logger.error(tmpLine) + /*装载result*/ + result.append(tmpLine).append("\n") + } + } + /*定义好输出流*/ + executor.streamHandler = PumpStreamHandler(outputStream, errStream) + try { + /*执行脚本*/ + val exitCode = executor.execute(cmdLine) + if (exitCode != 0) { + /*执行返回码,非零表示执行出错,这时直接抛错。为用户自己的脚本问题*/ + throw AtomException( + "$prefix Script command execution failed with exit code($exitCode)" + ) + } + } catch (ignored: Throwable) { + /*对其余异常兜底处理,可能是执行脚本时抛错的错。*/ + val errorMessage = executeErrorMessage ?: "Fail to execute the command($command)" + logger.warn(errorMessage) + throw AtomException( + ignored.message ?: "" + ) + } + return result.toString() + } + + /*写内容到文件*/ + private fun appendResultToFile( + workspace: File?, + resultLogFile: String?, + tmpLine: String + ) { + /*写入红线指标信息*/ + parseGate(tmpLine)?.let { + File(workspace, ScriptEnvUtils.getQualityGatewayEnvFile()).appendText(it + "\n") + } + + if (resultLogFile == null) { + return + } + /*写variable到文件*/ + parseVariable(tmpLine)?.let { + File(workspace, resultLogFile).appendText(it + "\n") + } + /*写output到文件*/ + parseOutput(tmpLine)?.let { + File(workspace, resultLogFile).appendText(it + "\n") + } + } + + /*解析variable格式的变量*/ + fun parseVariable( + tmpLine: String + ): String? { + /*相应匹配规则*/ + val pattenVar = "[\"]?::set-variable\\sname=.*" + val prefixVar = "::set-variable name=" + if (Pattern.matches(pattenVar, tmpLine)) { + /*正则匹配后做拆分处理*/ + val value = tmpLine.removeSurrounding("\"").removePrefix(prefixVar) + val keyValue = value.split("::") + if (keyValue.size >= 2) { + return "variables.${keyValue[0]}=${value.removePrefix("${keyValue[0]}::")}" + } + } + return null + } + + fun parseOutput( + tmpLine: String + ): String? { + /*相应匹配规则*/ + val pattenOutput = "[\"]?::set-output\\s(.*)" + val prefixOutput = "::set-output " + if (Pattern.matches(pattenOutput, tmpLine)) { + /*正则匹配后做拆分处理*/ + val value = tmpLine.removeSurrounding("\"").removePrefix(prefixOutput) + + val nameMatcher = getOutputMarcher(OUTPUT_NAME.matcher(value)) ?: "" + val typeMatcher = getOutputMarcher(OUTPUT_TYPE.matcher(value)) ?: "string" // type 默认为string + val labelMatcher = getOutputMarcher(OUTPUT_LABEL.matcher(value)) ?: "" + val pathMatcher = getOutputMarcher(OUTPUT_PATH.matcher(value)) ?: "" + val reportTypeMatcher = getOutputMarcher(OUTPUT_REPORT_TYPE.matcher(value)) ?: "" + + /*对5种类型的标志位分别存储,互不干扰*/ + val keyValue = value.split("::") + if (keyValue.size >= 2) { + // 以逗号为分隔符 左右依次为name type label path reportType + return "$nameMatcher," + + "$typeMatcher," + + "$labelMatcher," + + "$pathMatcher," + + "$reportTypeMatcher=${value.removePrefix("${keyValue[0]}::")}" + } + } + return null + } + + fun parseGate( + tmpLine: String + ): String? { + /*相应匹配规则*/ + val pattenOutput = "[\"]?::set-gate-value\\s(.*)" + val prefixOutput = "::set-gate-value " + if (Pattern.matches(pattenOutput, tmpLine)) { + /*正则匹配后做拆分处理*/ + val value = tmpLine.removeSurrounding("\"").removePrefix(prefixOutput) + val name = getOutputMarcher(OUTPUT_NAME.matcher(value)) + val title = getOutputMarcher(OUTPUT_GATE_TITLE.matcher(value)) + /*对2种类型的标志位分别存储,互不干扰*/ + val keyValue = value.split("::") + if (keyValue.size >= 2) { + // pass_rate=1,pass_rate=通过率\n + var text = "$name=${value.removePrefix("${keyValue[0]}::")}" + if (!title.isNullOrBlank()) { + text = text.plus(",$name=$title") + } + return text + } + } + return null + } + + private fun getOutputMarcher(macher: Matcher): String? { + return with(macher) { + /*只返回匹配到的第一个,否则返回null*/ + if (this.find()) { + this.group(1) + } else null + } + } +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/utils/CommonEnv.kt b/src/main/kotlin/com/tencent/bk/devops/atom/utils/CommonEnv.kt new file mode 100644 index 0000000..8a342fb --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/utils/CommonEnv.kt @@ -0,0 +1,68 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.utils + +import org.slf4j.LoggerFactory + +// import com.tencent.devops.worker.common.logger.LoggerService + +/** + * This is to store the common build env + */ +object CommonEnv { + + private val logger = LoggerFactory.getLogger(CommonEnv::class.java) + + private val envMap = HashMap() + private var svnUser: String? = null + private var svnPass: String? = null + + var fileDevnetGateway: String? = null + var fileIdcGateway: String? = null + + fun addCommonEnv(env: Map) { + logger.info("Add the env($env) to common environment") + envMap.putAll(env) + } + + fun getCommonEnv(): Map { + return HashMap(envMap) + } + + fun addSvnHttpCredential(user: String, password: String) { + svnUser = user + svnPass = password + } + + fun getSvnHttpCredential(): Pair? { + if (svnUser.isNullOrBlank() || svnPass.isNullOrBlank()) { + return null + } + return Pair(svnUser!!, svnPass!!) + } +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/utils/CommonUtil.kt b/src/main/kotlin/com/tencent/bk/devops/atom/utils/CommonUtil.kt new file mode 100644 index 0000000..0ff7615 --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/utils/CommonUtil.kt @@ -0,0 +1,45 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.utils + +import org.slf4j.LoggerFactory +import java.io.File + +object CommonUtil { + + private val logger = LoggerFactory.getLogger(CommonUtil::class.java) + + /*统一打日志,debug信息用于问题排查*/ + fun printTempFileInfo(file: File) { + logger.debug("--------file(${file.name}) debug info-------------") + logger.debug("absolutePath: ${file.absolutePath}") + logger.debug("Size: ${file.length()}") + logger.debug("canExecute/canRead/canWrite: ${file.canExecute()}/${file.canRead()}/${file.canWrite()}") + logger.debug("--------file debug info end-------------") + } +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/utils/Constants.kt b/src/main/kotlin/com/tencent/bk/devops/atom/utils/Constants.kt new file mode 100644 index 0000000..320458a --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/utils/Constants.kt @@ -0,0 +1,46 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.utils + +/*getEnvironmentPathPrefix 公共方法*/ +fun getEnvironmentPathPrefix(): String { + val os = System.getProperty("os.name") + if (os.isNullOrEmpty()) { + return ENVIRONMENT_LINUX_PATH_PREFIX + } + if (os.startsWith("mac", true)) { + return ENVIRONMENT_MAC_PATH_PREFIX + } + return ENVIRONMENT_LINUX_PATH_PREFIX +} + +/*LINUX前缀*/ +private const val ENVIRONMENT_LINUX_PATH_PREFIX = "/data/bkdevops/apps/" + +/*MAC前缀*/ +private const val ENVIRONMENT_MAC_PATH_PREFIX = "/Users/bkdevops/apps/" diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/utils/ExecutorUtil.kt b/src/main/kotlin/com/tencent/bk/devops/atom/utils/ExecutorUtil.kt new file mode 100644 index 0000000..12f396e --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/utils/ExecutorUtil.kt @@ -0,0 +1,53 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.utils + +object ExecutorUtil { + + private val threadLocal = ThreadLocal() + + /*设置线程本地变量*/ + private fun setThreadLocal() { + val randomNum = UUIDUtil.generate() + threadLocal.set(randomNum) + } + + /*同线程会获取相同的变量*/ + fun getThreadLocal(): String { + val value = threadLocal.get() + if (value == null) { + setThreadLocal() + } + return threadLocal.get() + } + + /*删除线程变量*/ + fun removeThreadLocal() { + threadLocal.remove() + } +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/utils/OauthCredentialLineParser.kt b/src/main/kotlin/com/tencent/bk/devops/atom/utils/OauthCredentialLineParser.kt new file mode 100644 index 0000000..68ada4c --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/utils/OauthCredentialLineParser.kt @@ -0,0 +1,50 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.utils + +import org.slf4j.LoggerFactory +import java.util.regex.Pattern + +/*oauth 凭证处理*/ +class OauthCredentialLineParser { + fun onParseLine(line: String): String { + if (line.contains("http://oauth2:")) { + val pattern = Pattern.compile("oauth2:(\\w+)@") + val matcher = pattern.matcher(line) + /*脱敏*/ + val replace = matcher.replaceAll("") + logger.info("Parse the line from $line to $replace") + return replace + } + return line + } + + companion object { + private val logger = LoggerFactory.getLogger(OauthCredentialLineParser::class.java) + } +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/utils/ScriptEnvUtils.kt b/src/main/kotlin/com/tencent/bk/devops/atom/utils/ScriptEnvUtils.kt new file mode 100644 index 0000000..b7db6fd --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/utils/ScriptEnvUtils.kt @@ -0,0 +1,178 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.utils + +import org.slf4j.LoggerFactory +import java.io.File + +object ScriptEnvUtils { + private const val ENV_FILE = "result.log" + private const val MULTILINE_FILE = "multiLine.log" + private const val CONTEXT_FILE = "context.log" + private const val QUALITY_GATEWAY_FILE = "gatewayValueFile.ini" + private val keyRegex = Regex("^[a-zA-Z_][a-zA-Z0-9_]*$") + private val logger = LoggerFactory.getLogger(ScriptEnvUtils::class.java) + + fun cleanEnv(buildId: String, workspace: File) { + cleanScriptEnv(workspace, getEnvFile(buildId)) + cleanScriptEnv(workspace, getDefaultEnvFile(buildId)) + } + + fun cleanContext(buildId: String, workspace: File) { + cleanScriptEnv(workspace, getContextFile(buildId)) + } + + /*通过env文件,获取到环境变量map*/ + fun getEnv(buildId: String, workspace: File): Map { + return readScriptEnv(workspace, "$buildId-$ENV_FILE") + .plus(readScriptEnv(workspace, getEnvFile(buildId))) + } + + /*通过上下文文件,获取到上下文内容*/ + fun getContext(buildId: String, workspace: File): Map { + return readScriptContext(workspace, getContextFile(buildId)) + } + + /*限定文件名*/ + fun getEnvFile(buildId: String): String { + val randomNum = ExecutorUtil.getThreadLocal() + return "$buildId-$randomNum-$ENV_FILE" + } + + /*获取多行内容*/ + fun getMultipleLines(buildId: String, workspace: File): List { + return readLines(workspace, getMultipleLineFile(buildId)) + } + + /*限定文件名*/ + fun getMultipleLineFile(buildId: String): String { + val randomNum = ExecutorUtil.getThreadLocal() + return "$buildId-$randomNum-$MULTILINE_FILE" + } + + /*限定文件名*/ + fun getContextFile(buildId: String): String { + val randomNum = ExecutorUtil.getThreadLocal() + return "$buildId-$randomNum-$CONTEXT_FILE" + } + + /*限定文件名*/ + private fun getDefaultEnvFile(buildId: String): String { + return "$buildId-$ENV_FILE" + } + + /*清理逻辑*/ + fun cleanWhenEnd(buildId: String, workspace: File) { + /*获取文件路径*/ + val defaultEnvFilePath = getDefaultEnvFile(buildId) + val randomEnvFilePath = getEnvFile(buildId) + val randomContextFilePath = getContextFile(buildId) + val multiLineFilePath = getMultipleLineFile(buildId) + /*清理文件*/ + deleteFile(multiLineFilePath, workspace) + deleteFile(defaultEnvFilePath, workspace) + deleteFile(randomEnvFilePath, workspace) + deleteFile(randomContextFilePath, workspace) + /*销毁线程临时变量数据*/ + ExecutorUtil.removeThreadLocal() + } + + /*清理文件逻辑*/ + private fun deleteFile(filePath: String, workspace: File) { + val defaultFile = File(workspace, filePath) + if (defaultFile.exists()) { + defaultFile.delete() + } + } + + fun getQualityGatewayEnvFile() = QUALITY_GATEWAY_FILE + + /*清理文件*/ + private fun cleanScriptEnv(workspace: File, file: String) { + val scriptFile = File(workspace, file) + if (scriptFile.exists()) { + scriptFile.delete() + } + if (!scriptFile.createNewFile()) { + logger.warn("Fail to create the file - (${scriptFile.absolutePath})") + } else { + scriptFile.deleteOnExit() + } + } + + /*读取env文件*/ + private fun readScriptEnv(workspace: File, file: String): Map { + val f = File(workspace, file) + if (!f.exists() || f.isDirectory) { + return mapOf() + } + /*读取文件内容*/ + val lines = f.readLines() + return if (lines.isEmpty()) { + mapOf() + } else { + // KEY-VALUE 格式读取 + lines.filter { it.contains("=") }.map { + val split = it.split("=", ignoreCase = false, limit = 2) + split[0].trim() to split[1].trim() + }.filter { + // #3453 保存时再次校验key的合法性 + keyRegex.matches(it.first) + }.toMap() + } + } + + private fun readLines(workspace: File, file: String): List { + val f = File(workspace, file) + /*不存在或是文件夹则返回空。非预期情况*/ + if (!f.exists() || f.isDirectory) { + return emptyList() + } + return f.readLines() + } + + /*读取上下文文件*/ + private fun readScriptContext(workspace: File, file: String): Map { + val f = File(workspace, file) + if (!f.exists() || f.isDirectory) { + return mapOf() + } + + /*读取文件内容*/ + val lines = f.readLines() + return if (lines.isEmpty()) { + mapOf() + } else { + // KEY-VALUE + lines.filter { it.contains("=") }.map { + val split = it.split("=", ignoreCase = false, limit = 2) + split[0].trim() to split[1].trim() + }.toMap() + } + } +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/utils/SensitiveLineParser.kt b/src/main/kotlin/com/tencent/bk/devops/atom/utils/SensitiveLineParser.kt new file mode 100644 index 0000000..14cbc36 --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/utils/SensitiveLineParser.kt @@ -0,0 +1,51 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.utils + +import org.slf4j.LoggerFactory +import java.util.regex.Pattern + +object SensitiveLineParser { + private val pattern = Pattern.compile("oauth2:(\\w+)@") + private val patternPassword = Pattern.compile("http://.*:.*@") + + fun onParseLine(line: String): String { + if (line.contains("http://oauth2:")) { + val matcher = pattern.matcher(line) + val replace = matcher.replaceAll("oauth2:***@") + logger.info("Parse the line from $line to $replace") + return replace + } + if (line.contains("http://")) { + return patternPassword.matcher(line).replaceAll("http://***:***@") + } + return line + } + + private val logger = LoggerFactory.getLogger(SensitiveLineParser::class.java) +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/utils/UUIDUtil.kt b/src/main/kotlin/com/tencent/bk/devops/atom/utils/UUIDUtil.kt new file mode 100644 index 0000000..78f60a9 --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/utils/UUIDUtil.kt @@ -0,0 +1,50 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.utils + +import java.util.UUID + +/** + * + * Powered By Tencent + */ +object UUIDUtil { + /** + * 生成32位字符随机UUID + * @return UUID字符串 + */ + fun generate(): String { + val uuid = UUID.randomUUID() + val str = uuid.toString() + // 去掉"-"符号 + return str.substring(0, 8) + str.substring(9, 13) + str.substring(14, 18) + str.substring( + 19, + 23 + ) + str.substring(24) + } +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/utils/script/BashUtil.kt b/src/main/kotlin/com/tencent/bk/devops/atom/utils/script/BashUtil.kt new file mode 100644 index 0000000..2c72cd3 --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/utils/script/BashUtil.kt @@ -0,0 +1,282 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.utils.script + +import com.tencent.bk.devops.atom.enums.CharsetType +import com.tencent.bk.devops.atom.enums.OSType +import com.tencent.bk.devops.atom.exception.AtomException +import com.tencent.bk.devops.atom.pojo.AgentEnv +import com.tencent.bk.devops.atom.pojo.BuildEnv +import com.tencent.bk.devops.atom.utils.CommandLineUtils +import com.tencent.bk.devops.atom.utils.CommonUtil +import com.tencent.bk.devops.atom.utils.ScriptEnvUtils +import com.tencent.bk.devops.atom.utils.getEnvironmentPathPrefix +import org.slf4j.LoggerFactory +import java.io.File +import java.nio.charset.Charset +import java.nio.file.Files + +@Suppress("ALL") +object BashUtil { + + // + private const val setEnv = "setEnv(){\n" + + " local key=\$1\n" + + " local val=\$2\n" + + "\n" + + " if [[ -z \"\$@\" ]]; then\n" + + " return 0\n" + + " fi\n" + + "\n" + + " if ! echo \"\$key\" | grep -qE \"^[a-zA-Z_][a-zA-Z0-9_]*\$\"; then\n" + + " echo \"[\$key] is invalid\" >&2\n" + + " return 1\n" + + " fi\n" + + "\n" + + " echo \$key=\$val >> ##resultFile##\n" + + " export \$key=\"\$val\"\n" + + " }\n" + private const val format_multiple_lines = "format_multiple_lines() {\n" + + " local content=\$1\n" + + " content=\"\${content//'%'/'%25'}\"\n" + + " content=\"\${content//\$'\\n'/'%0A'}\"\n" + + " content=\"\${content//\$'\\r'/'%0D'}\"\n" + + " /bin/echo \"\$content\"|sed 's/\\\\n/%0A/g'|sed 's/\\\\r/%0D/g' >> ##resultFile##\n" + + "}\n" + // +// private const val setGateValue = "setGateValue(){\n" + +// " local key=\$1\n" + +// " local val=\$2\n" + +// "\n" + +// " if [[ -z \"\$@\" ]]; then\n" + +// " return 0\n" + +// " fi\n" + +// "\n" + +// " if ! echo \"\$key\" | grep -qE \"^[a-zA-Z_][a-zA-Z0-9_]*\$\"; then\n" + +// " echo \"[\$key] is invalid\" >&2\n" + +// " return 1\n" + +// " fi\n" + +// "\n" + +// " echo \$key=\$val >> ##gateValueFile##\n" + +// " }\n" + +// lateinit var buildEnvs: List + + private val specialKey = listOf(".", "-") + + // private val specialValue = listOf("|", "&", "(", ")") + private val specialCharToReplace = Regex("['\n]") // --bug=75509999 Agent环境变量中替换掉破坏性字符 + private val logger = LoggerFactory.getLogger(BashUtil::class.java) + + fun execute( + buildId: String, + script: String, + dir: File, + buildEnvs: List, + runtimeVariables: Map, + continueNoneZero: Boolean = false, + systemEnvVariables: Map? = null, + prefix: String = "", + errorMessage: String? = null, + workspace: File = dir, + print2Logger: Boolean = true, + stepId: String? = null, + paramClassName: List + ): String { + return executeUnixCommand( + command = getCommandFile( + buildId = buildId, + script = script, + dir = dir, + workspace = workspace, + buildEnvs = buildEnvs, + runtimeVariables = runtimeVariables, + continueNoneZero = continueNoneZero, + systemEnvVariables = systemEnvVariables, + paramClassName = paramClassName + ).canonicalPath, + sourceDir = dir, + prefix = prefix, + errorMessage = errorMessage, + print2Logger = print2Logger, + executeErrorMessage = "", + buildId = buildId, + stepId = stepId + ) + } + + fun getCommandFile( + buildId: String, + script: String, + dir: File, + buildEnvs: List, + runtimeVariables: Map, + continueNoneZero: Boolean = false, + systemEnvVariables: Map? = null, + workspace: File = dir, + charSetType: CharsetType = CharsetType.UTF_8, + paramClassName: List + ): File { + val file = Files.createTempFile("devops_script", ".sh").toFile() + file.deleteOnExit() + + val command = StringBuilder() + val bashStr = script.split("\n")[0] + if (bashStr.startsWith("#!/")) { + command.append(bashStr).append("\n") + } + + command.append("export WORKSPACE=${workspace.absolutePath}\n") + .append("export DEVOPS_BUILD_SCRIPT_FILE=${file.absolutePath}\n") + + // 设置系统环境变量 + systemEnvVariables?.forEach { (name, value) -> + command.append("export $name=$value\n") + } + + val commonEnv = runtimeVariables + .filterNot { specialEnv(it.key) || it.key in paramClassName } + if (commonEnv.isNotEmpty()) { + commonEnv.forEach { (name, value) -> + // --bug=75509999 Agent环境变量中替换掉破坏性字符 + val clean = value.replace(specialCharToReplace, "") + command.append("export $name='$clean'\n") + } + } + if (buildEnvs.isNotEmpty()) { + var path = "" + buildEnvs.forEach { buildEnv -> + val home = File(getEnvironmentPathPrefix(), "${buildEnv.name}/${buildEnv.version}/") + if (!home.exists()) { + logger.error("环境变量路径(${home.absolutePath})不存在") + } + val envFile = File(home, buildEnv.binPath) + if (!envFile.exists()) { + logger.error("环境变量路径(${envFile.absolutePath})不存在") + return@forEach + } + // command.append("export $name=$path") + path = if (path.isEmpty()) { + envFile.absolutePath + } else { + "$path:${envFile.absolutePath}" + } + if (buildEnv.env.isNotEmpty()) { + buildEnv.env.forEach { (name, path) -> + val p = File(home, path) + command.append("export $name=${p.absolutePath}\n") + } + } + } + if (path.isNotEmpty()) { + path = "$path:\$PATH" + command.append("export PATH=$path\n") + } + } + + if (!continueNoneZero) { + command.append("set -e\n") + } else { + logger.info("每行命令运行返回值非零时,继续执行脚本") + command.append("set +e\n") + } + + command.append( + setEnv.replace( + oldValue = "##resultFile##", + newValue = File(dir, ScriptEnvUtils.getEnvFile(buildId)).absolutePath + ) + ) + + command.append( + format_multiple_lines.replace( + oldValue = "##resultFile##", + newValue = File(dir, ScriptEnvUtils.getMultipleLineFile(buildId)).absolutePath + ) + ) +// command.append( +// setGateValue.replace(oldValue = "##gateValueFile##", +// newValue = File(dir, ScriptEnvUtils.getQualityGatewayEnvFile()).absolutePath)) + command.append(script) + + val charset = when (charSetType) { + CharsetType.UTF_8 -> Charsets.UTF_8 + CharsetType.GBK -> Charset.forName(CharsetType.GBK.name) + else -> Charset.defaultCharset() + } + logger.info("The default charset is $charset") + + file.writeText(command.toString(), charset) + + if (AgentEnv.getOS() != OSType.WINDOWS) { + executeUnixCommand( + command = "chmod +x ${file.absolutePath}", + sourceDir = dir, + buildId = buildId + ) + } + CommonUtil.printTempFileInfo(file) + return file + } + + private fun executeUnixCommand( + command: String, + sourceDir: File, + prefix: String = "", + errorMessage: String? = null, + print2Logger: Boolean = true, + executeErrorMessage: String? = null, + buildId: String, + stepId: String? = null + ): String { + try { + return CommandLineUtils.execute( + command = command, + workspace = sourceDir, + print2Logger = print2Logger, + prefix = prefix, + executeErrorMessage = executeErrorMessage, + buildId = buildId, + stepId = stepId + ) + } catch (taskError: AtomException) { + throw taskError + } catch (ignored: Throwable) { + val errorInfo = errorMessage ?: "Fail to run the command $command" + logger.info("$errorInfo because of error(${ignored.message})") + throw AtomException( + ignored.message ?: "" + ) + } + } + + /*过滤处理特殊的key*/ + private fun specialEnv(key: String): Boolean { + return specialKey.any { key.contains(it) } + } +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/utils/script/BatScriptUtil.kt b/src/main/kotlin/com/tencent/bk/devops/atom/utils/script/BatScriptUtil.kt new file mode 100644 index 0000000..1a0ac4b --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/utils/script/BatScriptUtil.kt @@ -0,0 +1,203 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.utils.script + +import com.tencent.bk.devops.atom.enums.CharsetType +import com.tencent.bk.devops.atom.utils.CommandLineUtils +import com.tencent.bk.devops.atom.utils.CommonUtil +import com.tencent.bk.devops.atom.utils.ScriptEnvUtils +import org.slf4j.LoggerFactory +import java.io.File +import java.nio.charset.Charset + +object BatScriptUtil { + + // + private const val setEnv = ":setEnv\r\n" + + " set file_save_dir=\"##resultFile##\"\r\n" + + " echo %~1=%~2 >>%file_save_dir%\r\n" + + " set %~1=%~2\r\n" + + " goto:eof\r\n" + + // + private const val setGateValue = ":setGateValue\r\n" + + " set file_save_dir=\"##gateValueFile##\"\r\n" + + " echo %~1=%~2 >>%file_save_dir%\r\n" + + " set %~1=%~2\r\n" + + " goto:eof\r\n" + + private val logger = LoggerFactory.getLogger(BatScriptUtil::class.java) + + // 2021-06-11 batchScript需要过滤掉上下文产生的变量,防止注入到环境变量中 + private val specialKey = listOf("variables.", "settings.", "envs.", "ci.", "job.", "jobs.", "steps.") + + private val specialValue = listOf("\n", "\r") + private val escapeValue = mapOf( + "&" to "^&", + "<" to "^<", + ">" to "^>", + "|" to "^|", + "\"" to "\\\"" + ) + + @Suppress("ALL") + fun execute( + script: String, + buildId: String, + runtimeVariables: Map, + dir: File, + prefix: String = "", + paramClassName: List, + errorMessage: String? = null, + workspace: File = dir, + print2Logger: Boolean = true, + stepId: String? = null, + charsetType: CharsetType? = null + ): String { + try { + val file = getCommandFile( + buildId = buildId, + script = script, + runtimeVariables = runtimeVariables, + dir = dir, + workspace = workspace, + charsetType = charsetType, + paramClassName = paramClassName + ) + return CommandLineUtils.execute( + command = "cmd.exe /C \"${file.canonicalPath}\"", + workspace = dir, + print2Logger = print2Logger, + prefix = prefix, + executeErrorMessage = "", + buildId = buildId, + stepId = stepId, + charSetType = charsetType + ) + } catch (ignore: Throwable) { + val errorInfo = errorMessage ?: "Fail to execute bat script" + logger.warn(errorInfo, ignore) + throw ignore + } + } + + @Suppress("ALL") + fun getCommandFile( + buildId: String, + script: String, + runtimeVariables: Map, + dir: File, + workspace: File = dir, + paramClassName: List, + charsetType: CharsetType? = null + ): File { + val tmpDir = System.getProperty("java.io.tmpdir") + val file = if (tmpDir.isNullOrBlank()) { + File.createTempFile("paas_build_script_", ".bat") + } else { + File(tmpDir).mkdirs() + File.createTempFile("paas_build_script_", ".bat", File(tmpDir)) + } + file.deleteOnExit() + + val command = StringBuilder() + + command.append("@echo off") + .append("\r\n") + .append("set WORKSPACE=${workspace.absolutePath}\r\n") + .append("set DEVOPS_BUILD_SCRIPT_FILE=${file.absolutePath}\r\n") + .append("\r\n") + + runtimeVariables +// .plus(CommonEnv.getCommonEnv()) // + .filterNot { specialEnv(it.key, it.value) || it.key in paramClassName } + .forEach { (name, value) -> + // 特殊保留字符转义 + val clean = escapeEnv(value) + command.append("set $name=\"$clean\"\r\n") // 双引号防止变量值有空格而意外截断定义 + command.append("set $name=%$name:~1,-1%\r\n") // 去除双引号,防止被程序读到有双引号的变量值 + } + + command.append(script.replace("\n", "\r\n")) + .append("\r\n") + .append("exit") + .append("\r\n") + .append( + setEnv.replace( + oldValue = "##resultFile##", + newValue = File(dir, ScriptEnvUtils.getEnvFile(buildId)).absolutePath + ) + ) + .append( + setGateValue.replace( + oldValue = "##gateValueFile##", + newValue = File(dir, ScriptEnvUtils.getQualityGatewayEnvFile()).canonicalPath + ) + ) + + val charset = when (charsetType) { + CharsetType.UTF_8 -> Charsets.UTF_8 + CharsetType.GBK -> Charset.forName(CharsetType.GBK.name) + else -> Charset.defaultCharset() + } + logger.info("The default charset is $charset") + + file.writeText(command.toString(), charset) + CommonUtil.printTempFileInfo(file) + return file + } + + private fun specialEnv(key: String, value: String): Boolean { + var match = false + /*过滤处理特殊的key*/ + for (it in specialKey) { + if (key.trim().startsWith(it)) { + match = true + break + } + } + + /*过滤处理特殊的value*/ + for (it in specialValue) { + if (value.contains(it)) { + match = true + break + } + } + return match + } + + /*做好转义,避免意外*/ + private fun escapeEnv(value: String): String { + var result = value + escapeValue.forEach { (k, v) -> + result = result.replace(k, v) + } + return result + } +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/utils/script/PowerShellUtil.kt b/src/main/kotlin/com/tencent/bk/devops/atom/utils/script/PowerShellUtil.kt new file mode 100644 index 0000000..0bebaca --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/utils/script/PowerShellUtil.kt @@ -0,0 +1,162 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.utils.script + +import com.tencent.bk.devops.atom.enums.CharsetType +import com.tencent.bk.devops.atom.utils.CommandLineUtils +import com.tencent.bk.devops.atom.utils.CommonUtil +import org.slf4j.LoggerFactory +import java.io.File +import java.nio.charset.Charset + +object PowerShellUtil { + + // + private const val setEnv = "function setEnv(\$key, \$value)\n" + + "{\n" + + " Set-Item -Path Env:\\\$key -Value \$value\n" + + " echo \"::set-output name=\$key::\$value\"\n" + + "}\n" + + private val logger = LoggerFactory.getLogger(PowerShellUtil::class.java) + + // 2021-06-11 batchScript需要过滤掉上下文产生的变量,防止注入到环境变量中 + private val specialKey = listOf("variables.", "settings.", "envs.", "ci.", "job.", "jobs.", "steps.") + + private val specialValue = listOf("\n", "\r") + + @Suppress("ALL") + fun execute( + script: String, + buildId: String, + runtimeVariables: Map, + dir: File, + prefix: String = "", + paramClassName: List, + errorMessage: String? = null, + workspace: File = dir, + print2Logger: Boolean = true, + stepId: String? = null, + charsetType: CharsetType? = null + ): String { + try { + val file = getCommandFile( + buildId = buildId, + script = script, + runtimeVariables = runtimeVariables, + dir = dir, + workspace = workspace, + charsetType = charsetType, + paramClassName = paramClassName + ) + return CommandLineUtils.execute( + command = "powershell.exe /C \"${file.canonicalPath}\"", + workspace = dir, + print2Logger = print2Logger, + prefix = prefix, + executeErrorMessage = "", + buildId = buildId, + stepId = stepId, + charSetType = charsetType + ) + } catch (ignore: Throwable) { + val errorInfo = errorMessage ?: "Fail to execute bat script" + logger.warn(errorInfo, ignore) + throw ignore + } + } + + @Suppress("ALL") + fun getCommandFile( + buildId: String, + script: String, + runtimeVariables: Map, + dir: File, + workspace: File = dir, + paramClassName: List, + charsetType: CharsetType? = null + ): File { + val tmpDir = System.getProperty("java.io.tmpdir") + val file = if (tmpDir.isNullOrBlank()) { + File.createTempFile("devops_script", ".ps1") + } else { + File(tmpDir).mkdirs() + File.createTempFile("devops_script", ".ps1", File(tmpDir)) + } + file.deleteOnExit() + + val command = StringBuilder() + + command.append("Set-Item -Path Env:\\WORKSPACE -Value '${workspace.absolutePath}'\n") + .append("Set-Item -Path Env:\\DEVOPS_BUILD_SCRIPT_FILE -Value '${file.absolutePath}'\n") + .append("\r\n") + + runtimeVariables +// .plus(CommonEnv.getCommonEnv()) // + .filterNot { specialEnv(it.key, it.value) || it.key in paramClassName } + .forEach { (name, value) -> + command.append("Set-Item -Path Env:\\$name -Value '$value'\n") + } + + command.append(setEnv) + .append(script.replace("\n", "\r\n")) + .append("\r\n") + .append("exit") + + val charset = when (charsetType) { + CharsetType.UTF_8 -> Charsets.UTF_8 + CharsetType.GBK -> Charset.forName(CharsetType.GBK.name) + else -> Charset.defaultCharset() + } + logger.info("The default charset is $charset") + + file.writeText(command.toString(), charset) + CommonUtil.printTempFileInfo(file) + return file + } + + private fun specialEnv(key: String, value: String): Boolean { + var match = false + /*过滤处理特殊的key*/ + for (it in specialKey) { + if (key.trim().startsWith(it)) { + match = true + break + } + } + + /*过滤处理特殊的value*/ + for (it in specialValue) { + if (value.contains(it)) { + match = true + break + } + } + return match + } +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/utils/script/PwshUtil.kt b/src/main/kotlin/com/tencent/bk/devops/atom/utils/script/PwshUtil.kt new file mode 100644 index 0000000..e1f9dc2 --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/utils/script/PwshUtil.kt @@ -0,0 +1,175 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.utils.script + +import com.tencent.bk.devops.atom.enums.CharsetType +import com.tencent.bk.devops.atom.utils.CommandLineUtils +import com.tencent.bk.devops.atom.utils.CommonUtil +import com.tencent.bk.devops.atom.utils.ScriptEnvUtils +import org.slf4j.LoggerFactory +import java.io.File +import java.nio.charset.Charset +import java.nio.file.Files + +object PwshUtil { + + // + private const val setEnv = "function setEnv(\$key, \$value)\n" + + "{\n" + + " Set-Item -Path Env:\\\$key -Value \$value\n" + + " \"\$key=\$value\" | Out-File -Append ##resultFile##\n" + + "}\n" + + // + private const val setGateValue = "function setGateValue(\$key, \$value)\n" + + "{\n" + + " \"\$key=\$value\" | Out-File -Append ##gateValueFile##\n" + + "}\n" + + private val logger = LoggerFactory.getLogger(PwshUtil::class.java) + + // 2021-06-11 batchScript需要过滤掉上下文产生的变量,防止注入到环境变量中 + private val specialKey = listOf("variables.", "settings.", "envs.", "ci.", "job.", "jobs.", "steps.") + + private val specialValue = listOf("\n", "\r") + + @Suppress("ALL") + fun execute( + script: String, + buildId: String, + runtimeVariables: Map, + dir: File, + prefix: String = "", + paramClassName: List, + errorMessage: String? = null, + workspace: File = dir, + print2Logger: Boolean = true, + stepId: String? = null, + charsetType: CharsetType? = null + ): String { + try { + val file = getCommandFile( + buildId = buildId, + script = script, + runtimeVariables = runtimeVariables, + dir = dir, + workspace = workspace, + charsetType = charsetType, + paramClassName = paramClassName + ) + return CommandLineUtils.execute( + command = "pwsh \"${file.canonicalPath}\"", + workspace = dir, + print2Logger = print2Logger, + prefix = prefix, + executeErrorMessage = "", + buildId = buildId, + stepId = stepId, + charSetType = charsetType + ) + } catch (ignore: Throwable) { + val errorInfo = errorMessage ?: "Fail to execute bat script" + logger.warn(errorInfo, ignore) + throw ignore + } + } + + @Suppress("ALL") + fun getCommandFile( + buildId: String, + script: String, + runtimeVariables: Map, + dir: File, + workspace: File = dir, + paramClassName: List, + charsetType: CharsetType? = null + ): File { + val file = Files.createTempFile("devops_script", ".ps1").toFile() + file.deleteOnExit() + + val command = StringBuilder() + + command.append("Set-Item -Path Env:\\WORKSPACE -Value '${workspace.absolutePath}'\n") + .append("Set-Item -Path Env:\\DEVOPS_BUILD_SCRIPT_FILE -Value '${file.absolutePath}'\n") + .append("\r\n") + + runtimeVariables +// .plus(CommonEnv.getCommonEnv()) // + .filterNot { specialEnv(it.key, it.value) || it.key in paramClassName } + .forEach { (name, value) -> + command.append("Set-Item -Path Env:\\$name -Value '$value'\n") + } + + command.append( + setEnv.replace( + oldValue = "##resultFile##", + newValue = File(dir, ScriptEnvUtils.getEnvFile(buildId)).absolutePath + ) + ) + .append( + setGateValue.replace( + oldValue = "##gateValueFile##", + newValue = File(dir, ScriptEnvUtils.getQualityGatewayEnvFile()).canonicalPath + ) + ) + .append(script.replace("\n", "\r\n")) + .append("\r\n") + .append("exit") + + val charset = when (charsetType) { + CharsetType.UTF_8 -> Charsets.UTF_8 + CharsetType.GBK -> Charset.forName(CharsetType.GBK.name) + else -> Charset.defaultCharset() + } + logger.info("The default charset is $charset") + + file.writeText(command.toString(), charset) + CommonUtil.printTempFileInfo(file) + return file + } + + private fun specialEnv(key: String, value: String): Boolean { + var match = false + /*过滤处理特殊的key*/ + for (it in specialKey) { + if (key.trim().startsWith(it)) { + match = true + break + } + } + + /*过滤处理特殊的value*/ + for (it in specialValue) { + if (value.contains(it)) { + match = true + break + } + } + return match + } +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/utils/script/PythonUtil.kt b/src/main/kotlin/com/tencent/bk/devops/atom/utils/script/PythonUtil.kt new file mode 100644 index 0000000..59f3e91 --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/utils/script/PythonUtil.kt @@ -0,0 +1,265 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.utils.script + +import com.tencent.bk.devops.atom.enums.CharsetType +import com.tencent.bk.devops.atom.enums.OSType +import com.tencent.bk.devops.atom.exception.AtomException +import com.tencent.bk.devops.atom.pojo.AgentEnv +import com.tencent.bk.devops.atom.pojo.BuildEnv +import com.tencent.bk.devops.atom.utils.CommandLineUtils +import com.tencent.bk.devops.atom.utils.CommonUtil +import com.tencent.bk.devops.atom.utils.ScriptEnvUtils +import com.tencent.bk.devops.atom.utils.getEnvironmentPathPrefix +import org.slf4j.LoggerFactory +import java.io.File +import java.nio.charset.Charset +import java.nio.file.Files + +@Suppress("ALL") +object PythonUtil { + + // + private const val setEnv = "def setEnv(key,value):\n" + + " os.environ[key]=value\n" + + " f = open(\"##resultFile##\", 'a+')\n" + + " print(\"{0}={1}\".format(key, value), file=f)\n" + private const val format_multiple_lines = "def format_multiple_lines(s: str):\n" + + " out = s.replace('%','%25').replace('\\n','%0A').replace('\\r','%0D')\n" + + " f = open(\"##resultFile##\", 'a+')\n" + + " print(out, file=f)\n" + + " return out\n" + // +// private const val setGateValue = "setGateValue(){\n" + +// " local key=\$1\n" + +// " local val=\$2\n" + +// "\n" + +// " if [[ -z \"\$@\" ]]; then\n" + +// " return 0\n" + +// " fi\n" + +// "\n" + +// " if ! echo \"\$key\" | grep -qE \"^[a-zA-Z_][a-zA-Z0-9_]*\$\"; then\n" + +// " echo \"[\$key] is invalid\" >&2\n" + +// " return 1\n" + +// " fi\n" + +// "\n" + +// " echo \$key=\$val >> ##gateValueFile##\n" + +// " }\n" + +// lateinit var buildEnvs: List + + private val specialKey = listOf(".", "-") + + // private val specialValue = listOf("|", "&", "(", ")") + private val specialCharToReplace = Regex("['\n]") // --bug=75509999 Agent环境变量中替换掉破坏性字符 + private val logger = LoggerFactory.getLogger(PythonUtil::class.java) + + fun execute( + buildId: String, + script: String, + dir: File, + buildEnvs: List, + runtimeVariables: Map, + continueNoneZero: Boolean = false, + systemEnvVariables: Map? = null, + prefix: String = "", + errorMessage: String? = null, + workspace: File = dir, + print2Logger: Boolean = true, + stepId: String? = null, + paramClassName: List, + charsetType: CharsetType? = null + ): String { + return executeUnixCommand( + command = "python3 " + getCommandFile( + buildId = buildId, + script = script, + dir = dir, + workspace = workspace, + buildEnvs = buildEnvs, + runtimeVariables = runtimeVariables, + continueNoneZero = continueNoneZero, + systemEnvVariables = systemEnvVariables, + paramClassName = paramClassName, + charsetType = charsetType + ).canonicalPath, + sourceDir = dir, + prefix = prefix, + errorMessage = errorMessage, + print2Logger = print2Logger, + executeErrorMessage = "", + buildId = buildId, + stepId = stepId, + charsetType = charsetType + ) + } + + fun getCommandFile( + buildId: String, + script: String, + dir: File, + buildEnvs: List, + runtimeVariables: Map, + continueNoneZero: Boolean = false, + systemEnvVariables: Map? = null, + workspace: File = dir, + charSetType: CharsetType = CharsetType.UTF_8, + paramClassName: List, + charsetType: CharsetType? = null + ): File { + val file = Files.createTempFile("devops_script", ".py").toFile() + file.deleteOnExit() + + val command = StringBuilder() + + command.append("import os\n") + .append("os.environ['WORKSPACE']='${workspace.absolutePath}'\n") + .append("os.environ['DEVOPS_BUILD_SCRIPT_FILE']='${file.absolutePath}'\n") + + // 设置系统环境变量 + systemEnvVariables?.forEach { (name, value) -> + command.append("os.environ['$name']='$value'\n") + } + + val commonEnv = runtimeVariables + .filterNot { specialEnv(it.key) || it.key in paramClassName } + if (commonEnv.isNotEmpty()) { + commonEnv.forEach { (name, value) -> + // --bug=75509999 Agent环境变量中替换掉破坏性字符 + val clean = value.replace(specialCharToReplace, "") + command.append("os.environ['$name']='$clean'\n") + } + } + if (buildEnvs.isNotEmpty()) { + var path = "" + buildEnvs.forEach { buildEnv -> + val home = File(getEnvironmentPathPrefix(), "${buildEnv.name}/${buildEnv.version}/") + if (!home.exists()) { + logger.error("环境变量路径(${home.absolutePath})不存在") + } + val envFile = File(home, buildEnv.binPath) + if (!envFile.exists()) { + logger.error("环境变量路径(${envFile.absolutePath})不存在") + return@forEach + } + // command.append("export $name=$path") + path = if (path.isEmpty()) { + envFile.absolutePath + } else { + "$path:${envFile.absolutePath}" + } + if (buildEnv.env.isNotEmpty()) { + buildEnv.env.forEach { (name, path) -> + val p = File(home, path) + command.append("$name='${p.absolutePath}'\n") + } + } + } + if (path.isNotEmpty()) { + path = "$path:\$PATH" + command.append("os.environ['PATH']='$path'\n") + } + } + + command.append( + setEnv.replace( + oldValue = "##resultFile##", + newValue = File(dir, ScriptEnvUtils.getEnvFile(buildId)).absolutePath.replace("\\", "/") + ) + ) + + command.append( + format_multiple_lines.replace( + oldValue = "##resultFile##", + newValue = File(dir, ScriptEnvUtils.getMultipleLineFile(buildId)).absolutePath + ) + ) +// command.append( +// setGateValue.replace(oldValue = "##gateValueFile##", +// newValue = File(dir, ScriptEnvUtils.getQualityGatewayEnvFile()).absolutePath)) + command.append(script) + + val charset = when (charSetType) { + CharsetType.UTF_8 -> Charsets.UTF_8 + CharsetType.GBK -> Charset.forName(CharsetType.GBK.name) + else -> Charset.defaultCharset() + } + logger.info("The default charset is $charset") + + file.writeText(command.toString(), charset) + + if (AgentEnv.getOS() != OSType.WINDOWS) { + executeUnixCommand( + command = "chmod +x ${file.absolutePath}", + sourceDir = dir, + buildId = buildId, + charsetType = charsetType + ) + } + CommonUtil.printTempFileInfo(file) + return file + } + + private fun executeUnixCommand( + command: String, + sourceDir: File, + prefix: String = "", + errorMessage: String? = null, + print2Logger: Boolean = true, + executeErrorMessage: String? = null, + buildId: String, + stepId: String? = null, + charsetType: CharsetType? = null + ): String { + try { + return CommandLineUtils.execute( + command = command, + workspace = sourceDir, + print2Logger = print2Logger, + prefix = prefix, + executeErrorMessage = executeErrorMessage, + buildId = buildId, + charSetType = charsetType, + stepId = stepId + ) + } catch (taskError: AtomException) { + throw taskError + } catch (ignored: Throwable) { + val errorInfo = errorMessage ?: "Fail to run the command $command" + logger.info("$errorInfo because of error(${ignored.message})") + throw AtomException( + ignored.message ?: "" + ) + } + } + + /*过滤处理特殊的key*/ + private fun specialEnv(key: String): Boolean { + return specialKey.any { key.contains(it) } + } +} diff --git a/src/main/kotlin/com/tencent/bk/devops/atom/utils/script/ShUtil.kt b/src/main/kotlin/com/tencent/bk/devops/atom/utils/script/ShUtil.kt new file mode 100644 index 0000000..7e2a07b --- /dev/null +++ b/src/main/kotlin/com/tencent/bk/devops/atom/utils/script/ShUtil.kt @@ -0,0 +1,273 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.bk.devops.atom.utils.script + +import com.tencent.bk.devops.atom.enums.CharsetType +import com.tencent.bk.devops.atom.exception.AtomException +import com.tencent.bk.devops.atom.pojo.BuildEnv +import com.tencent.bk.devops.atom.utils.CommandLineUtils +import com.tencent.bk.devops.atom.utils.CommonUtil +import com.tencent.bk.devops.atom.utils.ScriptEnvUtils +import com.tencent.bk.devops.atom.utils.getEnvironmentPathPrefix +import org.slf4j.LoggerFactory +import java.io.File +import java.nio.charset.Charset +import java.nio.file.Files + +@Suppress("ALL") +object ShUtil { + + // + private const val setEnv = "setEnv(){\n" + + " local key=\$1\n" + + " local val=\$2\n" + + "\n" + + " if [[ -z \"\$@\" ]]; then\n" + + " return 0\n" + + " fi\n" + + "\n" + + " if ! echo \"\$key\" | grep -qE \"^[a-zA-Z_][a-zA-Z0-9_]*\$\"; then\n" + + " echo \"[\$key] is invalid\" >&2\n" + + " return 1\n" + + " fi\n" + + "\n" + + " echo \$key=\$val >> ##resultFile##\n" + + " export \$key=\"\$val\"\n" + + " }\n" + private const val format_multiple_lines = "format_multiple_lines() {\n" + + " local content=\$1\n" + + " content=\"\${content//'%'/'%25'}\"\n" + + " content=\"\${content//\$'\\n'/'%0A'}\"\n" + + " content=\"\${content//\$'\\r'/'%0D'}\"\n" + + " echo \"\$content\"\n" + + "}\n" + // +// private const val setGateValue = "setGateValue(){\n" + +// " local key=\$1\n" + +// " local val=\$2\n" + +// "\n" + +// " if [[ -z \"\$@\" ]]; then\n" + +// " return 0\n" + +// " fi\n" + +// "\n" + +// " if ! echo \"\$key\" | grep -qE \"^[a-zA-Z_][a-zA-Z0-9_]*\$\"; then\n" + +// " echo \"[\$key] is invalid\" >&2\n" + +// " return 1\n" + +// " fi\n" + +// "\n" + +// " echo \$key=\$val >> ##gateValueFile##\n" + +// " }\n" + +// lateinit var buildEnvs: List + + private val specialKey = listOf(".", "-") + + // private val specialValue = listOf("|", "&", "(", ")") + private val specialCharToReplace = Regex("['\n]") // --bug=75509999 Agent环境变量中替换掉破坏性字符 + private val logger = LoggerFactory.getLogger(ShUtil::class.java) + + fun execute( + buildId: String, + script: String, + dir: File, + buildEnvs: List, + runtimeVariables: Map, + continueNoneZero: Boolean = false, + systemEnvVariables: Map? = null, + prefix: String = "", + errorMessage: String? = null, + workspace: File = dir, + print2Logger: Boolean = true, + stepId: String? = null, + paramClassName: List + ): String { + return executeUnixCommand( + command = "sh -e " + getCommandFile( + buildId = buildId, + script = script, + dir = dir, + workspace = workspace, + buildEnvs = buildEnvs, + runtimeVariables = runtimeVariables, + continueNoneZero = continueNoneZero, + systemEnvVariables = systemEnvVariables, + paramClassName = paramClassName + ).canonicalPath, + sourceDir = dir, + prefix = prefix, + errorMessage = errorMessage, + print2Logger = print2Logger, + executeErrorMessage = "", + buildId = buildId, + stepId = stepId + ) + } + + fun getCommandFile( + buildId: String, + script: String, + dir: File, + buildEnvs: List, + runtimeVariables: Map, + continueNoneZero: Boolean = false, + systemEnvVariables: Map? = null, + workspace: File = dir, + charSetType: CharsetType = CharsetType.UTF_8, + paramClassName: List + ): File { + val file = Files.createTempFile("devops_script", ".sh").toFile() + file.deleteOnExit() + + val command = StringBuilder() + val bashStr = script.split("\n")[0] + if (bashStr.startsWith("#!/")) { + command.append(bashStr).append("\n") + } + + command.append("export WORKSPACE=${workspace.absolutePath}\n") + .append("export DEVOPS_BUILD_SCRIPT_FILE=${file.absolutePath}\n") + + // 设置系统环境变量 + systemEnvVariables?.forEach { (name, value) -> + command.append("export $name=$value\n") + } + + val commonEnv = runtimeVariables + .filterNot { specialEnv(it.key) || it.key in paramClassName } + if (commonEnv.isNotEmpty()) { + commonEnv.forEach { (name, value) -> + // --bug=75509999 Agent环境变量中替换掉破坏性字符 + val clean = value.replace(specialCharToReplace, "") + command.append("export $name='$clean'\n") + } + } + if (buildEnvs.isNotEmpty()) { + var path = "" + buildEnvs.forEach { buildEnv -> + val home = File(getEnvironmentPathPrefix(), "${buildEnv.name}/${buildEnv.version}/") + if (!home.exists()) { + logger.error("环境变量路径(${home.absolutePath})不存在") + } + val envFile = File(home, buildEnv.binPath) + if (!envFile.exists()) { + logger.error("环境变量路径(${envFile.absolutePath})不存在") + return@forEach + } + // command.append("export $name=$path") + path = if (path.isEmpty()) { + envFile.absolutePath + } else { + "$path:${envFile.absolutePath}" + } + if (buildEnv.env.isNotEmpty()) { + buildEnv.env.forEach { (name, path) -> + val p = File(home, path) + command.append("export $name=${p.absolutePath}\n") + } + } + } + if (path.isNotEmpty()) { + path = "$path:\$PATH" + command.append("export PATH=$path\n") + } + } + + if (!continueNoneZero) { + command.append("set -e\n") + } else { + logger.info("每行命令运行返回值非零时,继续执行脚本") + command.append("set +e\n") + } + + command.append( + setEnv.replace( + oldValue = "##resultFile##", + newValue = File(dir, ScriptEnvUtils.getEnvFile(buildId)).absolutePath + ) + ) + + command.append(format_multiple_lines) +// command.append( +// setGateValue.replace(oldValue = "##gateValueFile##", +// newValue = File(dir, ScriptEnvUtils.getQualityGatewayEnvFile()).absolutePath)) + command.append(script) + + val charset = when (charSetType) { + CharsetType.UTF_8 -> Charsets.UTF_8 + CharsetType.GBK -> Charset.forName(CharsetType.GBK.name) + else -> Charset.defaultCharset() + } + logger.info("The default charset is $charset") + + file.writeText(command.toString(), charset) + + executeUnixCommand( + command = "chmod +x ${file.absolutePath}", + sourceDir = dir, + buildId = buildId + ) + CommonUtil.printTempFileInfo(file) + return file + } + + private fun executeUnixCommand( + command: String, + sourceDir: File, + prefix: String = "", + errorMessage: String? = null, + print2Logger: Boolean = true, + executeErrorMessage: String? = null, + buildId: String, + stepId: String? = null + ): String { + try { + return CommandLineUtils.execute( + command = command, + workspace = sourceDir, + print2Logger = print2Logger, + prefix = prefix, + executeErrorMessage = executeErrorMessage, + buildId = buildId, + stepId = stepId + ) + } catch (taskError: AtomException) { + throw taskError + } catch (ignored: Throwable) { + val errorInfo = errorMessage ?: "Fail to run the command $command" + logger.info("$errorInfo because of error(${ignored.message})") + throw AtomException( + ignored.message ?: "" + ) + } + } + + /*过滤处理特殊的key*/ + private fun specialEnv(key: String): Boolean { + return specialKey.any { key.contains(it) } + } +} diff --git a/src/main/resources/META-INF/services/com.tencent.bk.devops.atom.spi.TaskAtom b/src/main/resources/META-INF/services/com.tencent.bk.devops.atom.spi.TaskAtom new file mode 100644 index 0000000..6a467b1 --- /dev/null +++ b/src/main/resources/META-INF/services/com.tencent.bk.devops.atom.spi.TaskAtom @@ -0,0 +1 @@ +com.tencent.bk.devops.atom.ScriptRunAtom diff --git a/src/test/resources/.sdk.json b/src/test/resources/.sdk.json new file mode 100644 index 0000000..efaeacd --- /dev/null +++ b/src/test/resources/.sdk.json @@ -0,0 +1,9 @@ +{ + "buildType": "DOCKER" , + "projectId": "232", + "agentId": "1x", + "secretKey": "2323232", + "gateway": "api.github.com", + "buildId": "vvadsaaf", + "vmSeqId": "33223" +} diff --git a/src/test/resources/input.json b/src/test/resources/input.json new file mode 100644 index 0000000..5377971 --- /dev/null +++ b/src/test/resources/input.json @@ -0,0 +1,3 @@ +{ + "desc": "hello world!" +} diff --git a/task.json b/task.json new file mode 100644 index 0000000..12f22ab --- /dev/null +++ b/task.json @@ -0,0 +1,86 @@ +{ + "atomCode": "run", + "execution": { + "packagePath": "run-jar-with-dependencies.jar", + "language": "java", + "minimumVersion": "1.8", + "demands": [], + "target": "java -jar run-jar-with-dependencies.jar" + }, + "input": { + "shell": { + "rule": {}, + "type": "enum-input", + "label": "指定脚本语言", + "desc": "指定脚本语言,默认时Windows执行Batch,Linux和Macos执行Shell。", + "required": false, + "hidden": false, + "component": "enum-input", + "list": [ + { + "value": "auto", + "label": "默认" + },{ + "value": "bash", + "label": "BASH" + }, + { + "value": "cmd", + "label": "CMD" + }, + { + "value": "pwsh", + "label": "POWERSHELL_CORE" + }, + { + "value": "powershell", + "label": "POWERSHELL_DESKTOP" + }, + { + "value": "python", + "label": "PYTHON" + }, + { + "value": "sh", + "label": "SH" + } + ], + "default": "auto" + }, + "script": { + "label": "执行脚本", + "default": "", + "placeholder": "输入脚本", + "type": "atom-ace-editor", + "desc": "输入脚本", + "required": true, + "disabled": false, + "hidden": false, + "isSensitive": false + }, + "charsetType": { + "rule": {}, + "type": "enum-input", + "label": "windows下字符集类型", + "desc": "仅windows构建机所需参数", + "required": false, + "hidden": false, + "component": "enum-input", + "list": [ + { + "value": "DEFAULT", + "label": "DEFAULT" + }, + { + "value": "UTF_8", + "label": "UTF-8" + }, + { + "value": "GBK", + "label": "GBK" + } + ], + "default": "DEFAULT" + } + } +}