E!;9`=-Wxalj
zVUrKv{hjZQHVNBKUqQ1@KB=c!zzbt!P2R0GkD6)gUdXjG0f!)qdM72bpkY!3?w4uH
zS;|s}!QRJyhCbcrDb^J#3e{B0|=Y*cso#g@okGo&_f6ke+G4|JSfHCCwC^mKQ6YUma3|4Fowb>|Rm6%{
z63}%BpL}2$upAqDaL-x<_?}@E^N!0$I0Zd)G9hq@z?Hn5e^~7Yorm;e9vAW4id4G%
z1*b}DskIQMm9eij$GkMoc?ed%VGz6f2qcQ@W|O2Dx~qN;mcZ4%#dN{!&SsAw);SV-
z3!NfJ(ltaOR`O4oRr!8=>*p+k
zvNkqJN(L6NRRtEe6v1s$`aKL1%q%^}&sv7Y``_-jV^Oeo`0l>l=E5u{@Uu2_PWeh|
zWoRv6n_|5RjrC*0=^eTof%>{`@fSU8XK=$JMYDK|#amsuttv^2fl8Q0^3OhBw4vCa
zvI_VcJ%Xm+@fXK;MS|yVJ$As&(v1|Qe!!n_;MK<$>a@RZFodG(SSus7OVt?moxQ(S
z_8*&(lM+PNy>t=(yyI1JoWRIO1XtyA|s62t@(D
zam;1mFUFP{W5BI4nlI0*ZA9(g{#METYy%ZOZNqMzpA+md^yXa4Q-x03ovE0jY|
zU_Py$8!>qKX2e>NI2$k`@`T28Is(uqhY4q?|Fad$s}5&!OX3gMggh4JOErWZBxeDS
zZucGh^1KpU;+;Hj!qGdHeEzA+4kUaZ!_04Gh!&^Sg8G`eJT2rhOKiDOGVcjuu=*f8
zmKru`JwQGqyDiN$^IFvNP(64yZcU?1z6ioi3WFHaZNS$>wgid?{S#%%748p2a#o*U
zq8Nr~-d=fk6Xt1S0&+9X{Wf$JjNuHVIVbf;i*p;ORP*#2#d&1AoqXU9(EOvgHu3Z6
znpb=lTMx7iFg0EM0Z3@Q1$8PxN@2Fp^Tzr{Z
z+mJUhdRn&PqvnG$63|ta{ld?UnKZf?&DO?G4YlrR>UwPar$DV#*F7i3D!2;GhkhVY
z)sR&GlCH*b-svCueQ$)(l@t*8Ff3XrRzZguyxRY1?<>Qq`o47mL8Mf=Lr}V;ln^%}
z-3@|(qI8H-BDD>amK2cgMwHkXz)xuqw;&-Qu_Xl*K^nbdq2T$S59c`_?s?96?zwzp
z?zQHcIp&ySyyG3|*ItttywIyvoff#^eRrGDHZk)ls9{k}Bl(iLT6r6v%iIL1t|93K3-O{B@t
z69gC&e%hITDvrO=74B(?QkLt~ok!>_rB71IC;DY25qe;?+pkp}JN!M9^U;Tm!Zrbk
z*XeQC(Y8^RvZ-bBQ`%|oi2PlnbAH8aP1c$&-9NjHP7nJqA(j2jNt~RDqF2#r+-mU!
zX`p6IQXcIL5eQ4m(;Xv$;pF34cJ5eQjw0u(+FgRql-DJ^VDqz_J2o%(_;JR=1_{3-
zC(MgG)yhgWTMCs0OeR}1v%RZ&jl*bbl|n@0%GKD@XtXp}O7M=vy_4}0eri`bA?0=M
z4KbTf25=pEw$I#H^eeXj*;>=ebJEi+rO78>u3rMMcSYaG62|d6zchYr{-#^mj**u0
zN;LbjL2bCqukwRAjeqSU3(=q%ti05Bkz2;)cqToV_5^;`#h;mhoRVY`qZDzU)p#1S
z!BZsa&9*&ncQy>oZFSKiCL7PDi-BJqRSVka-Gq(r886~u0RBp_lX~n1Hn88^?bZ6x
zE(J1)@wzSLA%I4!Y#7={S(;+iE?Z$ESq2tZZzNn~ZDS!m`>}1Z1Ao09
zkY8%%l}|_Bz1DFvm&iXWKIwerO5C+&!IQkl1i9KKE|!s#aUHxWXZBX@S(5ydDW#jj
z3+|^};|HRXE@R46@E7%qURtXVi}rlwSxd(xsinq9gNcqqBPcT+(2W|7LN!f2%<89G
zMcO{_H-8h|@M9=rHD0RWyeVr_Bv((h!F-PUjvvPBpL)$Vjj9IMU;7+$-JaIbvKQ_2
zOh@*%FT#sO-}SU*;0fNS@`p#$REYF!FzAf$&Ash?vHe(q{$`fNGHbDIjOuOI
znA7(RF!YJH#4w#ID3-7#RyySpZgDg7$-Dy7egNkY;>*ihkgGW!4f@V`Uu_nriZvYr
z&jmMYdKlgEGb)KDH>Kz2X3w1H10rGeg`*x~qNx`wJjV~KHb(_B3qKn?X@y>-o57(^
zj`-hg%l=Y$M~ltJByZLk0sTr#aR0(-_f8$W1U@q(>9Xu45s^9`OnXzm975v550g9;{m
zz?9S!!oU5kI9#F4PMzYX){bk*Q<9&qPgaS~t}7fmv(fX9fE*Rya808jo8JKA#>HGp
zlOH9hmU{xV-WWFs>yAj9`lcalS?FBg3-rFR43no#`GfskXGDc8s_|Y06S}}vlA8Il
zDqR%wIO(nLQ+>B(Q3jn%t%^g)Mssb7FgPeyLH4NCDfs1hrk~L&8c!zpaVA5}^ZN{O
znAn*cV7+5qk^02%@*Kiv!JH8>1CBHuw-cTlLE-{U84-6Q_4DG`JzV%>)gHX@(1Tmi
z_3N~6?wgWpog|@|^_^;0t+sxy@`s!Ire2-Or|uE2M7gq9hBz`ypbKKAul8w`tzkTA
zCc%Dj>MMB-@e)zPBlXErjmGGKc-)jOtWt(&k
zhL*3Bp=n1`^S~p)cUC+H4}1%$8@s)n#2jg+QEVlp))(P$OPTXth%iF%bUSw7#(h-z
zrJ3i}-9EwFA`o+ZpXK@?@5Er@oqrU7-}M*=&Nx$)%b!KR?Sk^vrT+Az%fNVjyo(G(
zcVOB;OuQLOFbQrnz<_qw5ofB5h(o
z+xR|%esp7^B;{>fj75o#@u^Z&KUs=rpIN+j;f)W4ALAZyfGF`xw~(j@C+&g5XN_w<
z+A<3(oEPAB#S!z+?_j>YetSF9U$`&dSCLV+i-XZNkgT3|W5s%rb{5}^au
zs@GbM>5xtav{P%Zzk4I5@6DJ;%0s`Atd!Q^TAeRG=r^CI!
zs1Mz##4y7pPaV@nd-JE}a>^tyX#D)qZ)I2Kdt2Mm@X!v6nrxlMxg(8$FF#?RbKeL;
z+#P0!#+P`;9hu(qc#B?>6|&O4MC%_xSa-f)xS`Qmq)@y4Gq(??)eqT8z@X23P};ci
z<4W>$)m`GX6XL{L%M>Q>-ds3X>Xb*2D$oGNqtCP~BcEdf{bk3%FhO-$*EKYsIxhWb
zp`}`x=h0wpt;B0w8x0Shg#P@ne(LT?xlRg|PIb2vOT5cRkNB$HWsKbTp<$uH!kG3b
zVOZTbIr_k*Ck}#7065!RNkSxJVr}X;2PmK%BixZt=SMhB&9;*Rw?myzt&~kZDQbC^
zBX)^9qib+_lW*pkU;uuy)6zJ>DV^v@o7CE&kh5!k#V3SI#il1omeIkCeC09SM`7ZfQtfZ#
z4A7F|o{el)S>|Ow#G9?LXTH{TmIa>8*EUCG;j-2prcSERye7F
z&ZKSK$-imu{YAPF8`NG=h`?urQa#N##SV+$X77#qgL>Vm|$zT
z;B{nyRq_w^=q&&>T@rE@_6tZQfVyxHFG1-qk|h~f5(fp2@?sGtFk7`j
zgt+++Hs}DXVhU?B!@X*BGt3D5$8WAmCwKgdb$0h7Oth`6+>>RkY}GksuQDU*P6_^{
zn#2i2#Yl^`UA_R%{b(n3qY0I>2{NRcrgDb
z5neOl{Mjl|i{Y%EBH*elBnVi2+@AE&bjHnCmb#Ue^uw_+A$3mK?uoD8^t*l^$s0
z{QK5hh^BC%}MUZK%s&xNmBO+v(&y-yDBZ_r1kYFsChk}2hX;^1uON9;Y5;N{@dlc&~(Woa4
zbB^j_Xk~ARC-E`Szq3}2S`_8;R*K9x;kiKJvU;oKU%HD-ReAP6Anoohs_ZU$K8_bU
zzZ^)3*^?=xaa<3%EM~Fm?VqsMKPgoI-QBuI&?WvTfxoh)9q_?IExpEnONxNS&3Mv1
z_wU3=!{kk328;fd7(tJU#FU#F+Jmg)O^V%IS?hSzF*G5hoT1hzN)qndp+IUF9eJF@jsEDvYp8Dg6Cs%F
zv8P|}eE*S@nl?0avF6r`tyQ<53~v3U<*vQuzDe(%hv4B85FR?pfy&4XlBhfumAd;+
z5R|4;J`DD1HY(mPpdiZ0q_{bM9EJP}r8D91hIrY96MF^ryc`rRtu{P=Y79cz5eDaJ
zrw%412=vZD&H;K{lJpJRT;;2xNSzM_L?t9>GJgtx-VAwc%HdyrkmnS0`#O(=j0}?x
z7Z(>bwUpq3v9pKcV5NgWo~FaVix6++bRMl_nk}&sZdW}%{ZD|`!INQrg7$fS|i?M
zXJ_B!>O36&=7)`QS`6>+c@IKq5PWpd_pOG_o)bI;N_m|b{
zUsTBOqh-(lq1gtjaj8HWhU5FYfsBf|
z=P)u~l><8JrNk8e^N3zZLSe_o9-yocmN1pFg
zKyM<|-*`BhS;P&CbugUi2=ar6VZ&{Em79|Cy08h*$*y;hjf#FsU_*ai=w3oE&DjAc
z|0gXln+GZHBNBX2?aF<-fsg=MDAY8k^x{|NsYUekjCNVFkPR8+h%
zZ+iH=b=M*CuF=tzuQm18pNVbsers)2EgsE`6R{&~Mn9*0+-#cnu9Uw}=g$gkJ=&^j
zvMEu|)Pu=xru(mT!2~w+Y#?D{V1J>i~IYP4~AgsB@-<&1c+Q~
zLH4V>!t=cyUXTYWw^yaF1qclsbmJcSLCXw>JVk{{H>@m%_Hh&EuOVrF7J2_BwO|Juks0EBd@Y-n2MV$p54v
z_wM?G?$}2)>`sADuAelQ=;05g0}{<*{;o!9{=Y{r2o;P{e%_tG5#NgqFTa2EPbspD
zy0K4hPmmxh133=FVB0X-pF>7`_y2Ynq7KqfWNGcTqV?#*VA~9mV|)Me3UE*e3Gk-<
z{*?p8p@?EiMswfY`#+-uW~n5@Xa9rbFvJIw|0^3{eVR-8(C;DBrS=&^6AMFblFZCZ
z9lr@`pmfwJ9X^Op5CN+x!hinvnfHux9a3(2%w*nI&IO2Bey)Va=E^MGZ@+R~15f%z
zfY{mHV0b}dsEuN}P|GLDf%ImE8V#vZJOC>Apz9Oxow)^W4Q=K0KUd3efibPtb9{h|
z!9fFSh@*Rn;8N#}C5V^FtUr7tc+-=o)p2<7MFX12dGu}MVQLPP_2ENuU@<`Q-H3Gv
z_KcYV_gLWN)a$@*Xa$%DCP0DJP9vFmAOnh9gk+z+?hOx*6~Nur9IwKUzGy?VDSWZ*
z>$lFnz=;b9>kelYdmp$qHD~5)rzVd~uS~X*(ZeD~OY@D@n@~F<5r?wh8V=Ay&Zp1R
z%t&S&!Ap_FEP+}`;wwX*0F^4>JzTvidgFjRw=#v4-gtOM*2!JFqeB9w7p5@6SZ
zfYK(wI5S9EL5MfN7WvgD47a4GQ=S2SEjHb$Sy#7BAOe(Cwq@)^LV^vSLI7;9qcW2(n7?wP09@g07}h?EyWmu6RBSxt
z^X}Fs>f%zp(u6yGUI^o1Ro8$j0g2r;iKi{*+$#Lk%mxPB_1}lFgJ%Po5!s-tg+>6K
zfV2p!Xd1k7CDc7&8vx82W@9{OH`*kokOXWXBX2p2)P9Zuq_ca9OW^wWeZ-N<8xjTH
zTD78tfOLrG*IT%ezGYGQ5_^s;Vnosl5YJyDvk^!vg1AR`6HPfIUOlq(b!hkIDttBn
zv_pS~HzfBX3~it@#)TlB{#uy+Wc~miY){b{f{M?Kgh;G1U~1l7p**6x`I^4&2}CZ8
z0P5a-e5$pzm4qkdp;r?%3Q4Fn5UC-Jcs~)^%?egNo05<`(k0oL4rW)(B|zjC#nZTK
z1M+(WLf~z_BC+Xq%ec-hn0GI%0&!C&U?Xx**QOI7Oa{#+kLaq(Q&3i9rW6ZbW_xZT
z>p$@oYFLRT9$?W45L+)E(?xUCDgBqv0?&0U?*=Shw)#^auf)dP$&;z*z6oRvfnx{Q
z86LSKs2H5O{*HS+f{%xvzbnz19jC$$M8@}H3>+=4vXhW!1W9<{@r*n~x@}4@JM0_2
zkd*Hcu7xCLZzX2)pXu{$u^a3yVfg^SYfEXSy6HzC^?)6r%hgE+m
zrznkYWc-!IJ4tv@1;r$D`i4smG@d~h4G0IBMNW14JIw_47A1^p3{5w`)x4Y(au>gq
z(TGc;dJ7z6BjI}LmUiYhz=gqV!lT`oAA@mn92Wj}O}s|nesvmRx!I>DbpVb5Y5BM#
zegOn2id^zK7Kyix-v;EL8<*BF<=L{T3#+vdZ5uT-$%V!T4e0P#4zm~{C{#Jvc$e$T
zcN%w4?zy%vw|5W%Hp%xZbn62Tm=l4>t@ikJ2;`2+4&m`%glvDn1N#B<-Bup)q2h{^
z_*2v!JF;;6>#EinAj1zxjvUm09gn7dFfZ<^7nNQSMHKOD&OP7nsSku&PbWK-VWj!w
ze*(E)WQ%lfIFMk3AffJT1BUfwX2m_$IDlIejF6=lr2^EXg7V18($adVLp5QjNsyfJ
z>6Z_{{h4>nur4)kVCWq|NSpzv4VUPrtV$5f`E?oEIf{Yl-^Uc-PFCzEmLv)`RZc5edkB77tpz5F#`c*3a!=>S2VR`9ADy0G
zT6GBcG5U+_^pjDy1G`ElKIH=w&L&)vt?U30bF5X#CR@I;t`s`Ew7LQ89JLAt?<>|F
zbpH`1FrvMtUsojVWPyw!=nUvII#AM9R%Td!AJC|bA~-CIx=P%4HRDG_7Be3wNv^G@
z(KBfGSK6+K%Zf(!MpZDL-_=O0@rXFLJi-R>x^6MwPf6vzN~}Eng33vRq*
z(vk{;1gQYYbKuYA^s6$GHqGI`ZZ7ahCA;x_GGQGlaZQODaVFAOj67DnR7rtFn4^elpZGca~
z3C_2rC4{>94f{_~y9K-N`AozGfhjbQ|0vM6cGt(f|)Uo!Jq4z2*^
zAQC>i1ZTkx0AT!JH|%`oIg0?#ki?Br0dMTW?E#7AfX%o_=xYX=Z^yH2H)I+_!=O1!
z(yy#Wep8mi8T;j5Pe=4Oa)>oZ&J^*{ajq2ecCFb1xkj|g~c
zr0Wy^gHs*sa16Z9M>2zb;!c*S0d?V&CB;S1CVZV3d=)cjK>!F`m_zQHs3?;2t4Nkq
zrTp%qaUj5*9f+KFfrOP0B7D-fKuRT6JF!NEod)vQ)xnkuW^vnX9{=qok${tOa!cv0
zfN(@2b|M|&)YNIGOuspmwC)2(MN`Yw1T${!!j^k6`r0;lcyw`U=M@Z#^kTZcwh&C8
zcw`Zi95D)*wI2Ai`BtJ7A|P9faBCoyx1*!{a;n#Y4je9U8RF1AgAVx>VdR#gd>+w}
z<7pn)HRUh&SH<=(RV;l8`?)=RHV|?uI+nN(I^@$DnL@0aK9esXG7*yQDo2RfQFs1Y
zGa3Mal~>eJJ!SQ;Rt<_*L5}rR_b40w?6i}o-mwCCR|otJD!;gDBD2Gbr?}8_Bye{*
zm5+QB%}>@Cii4Azj>`|U#EG@rWWtGE?*nHwtU}G>>Ofh^H;)TgeU?tkS5iBboWA)X
ze{QXLM7zpDY9aN>AP9=IRpb~i7GxFYnS@jhZWLSX@Ov$Q27)kD^g~n?=0||V+rgF#rzRrT
zA*HVPVq;*><4C}t*s{eY6#1TQDO)+oiR&4j}ne`E~bel=B3C=A|
zhnZV|wNzC^BAg(wPrCS($(?S4bSA4~M{m`Ck1zjVY>LZpA90i;1CG|poLGX2Gmjg
zg-f^y(5mIj1R`W?-1KzV0sK3ML5X*Dh|~;Uk6;dYLyKggUgD;lYdL&gqoG4?%c*W90lEL!
zQkuF{^{9U;UCjom^x>{~91GIf->AGmcFGUitNa8oUkM3_bL
zjouZ|NxQ%NJS!5op%K^kRzKQIxr&I+G{d;o9tR>zOWNMh6rg2cMM^g{%Do-2ks#$1
z8XWCKI&aG(M-A#3L9|#iZYFyb(gU;BbItnozxYIZiUHAgm*l$M!G0*rv@I(NPj>hA
zd$}x5g@xzN5W#x-5?Rr}D8%mhK@UQBhnYWp3m*K0VHkrz(5Q=5#5k3~&APQ$B7Gvb
zfDKdS2)lE+8YVcYY>&Y+7mp_=XgDo2Hm|`0;JS5l4sjtkz-cY%f#Mz%1!WeyMo@LH
zf&JBLY#&EY0TNz2;4?JziO!Q#!TEXtlH%6jC%u{Nu$%lPLyP-NYK||5&ZOnhkfm42
zESct>7P9P6$d;3bEuQ9X<(KmqxqQ*ruQwTPFpsV)=j6DH`GbZO8Z*f2sMW-{4Hp1{
zNN~4axb$8b$D9_L6-Paz1#;D`E6fr(EE{~#ao){YHp3U#&MwLPxDerU-Od>aM|Cg0
zm2#y#p5CP~=E}=q-hixg!(>RlSGwNt`@&7saT6I8-x&F3fgC-`Q2ct2soxo(;-B#I
zeJN5YFZaq`V)16w#U<@LB+X<~UwJS>aPR)WXFHbWS(=_&6;VoD`5!xM5!b_ROsl+2
zueh?2>;l``K_ms~lI|wPPR=zpUB=UKClqH2w@{yPBs_ZUv5GBYR7N1=M9{y7Tqz^M
zgrD2bcgSKpq1w8?6-^_AS#AU**G_pw-rS;WrF4_jq}5uvxGVK`t!alBnjdKIAm9r5FnAX6c6Wy@gfd
z{rCIwE15njez>hs+|GAB@q}|YqLpZ;G(;U8d>7}DS0A+WX+t#|cV%Zla%6DanH+=i
zN(P(V*u{`4CYdoS&o`iRp+*mZFcN>??~^fwVQV>~tVi{=JG`oM1&j4q)2C5Z`T
zp1&{6()J(f;ZA~E1Losc{dcXBIG$Hjd?Ph+i+Sv~-t`pUUsAMgAPlzKG9B@KxT{wg
zjD%`s^Y%>p5}(+WGCV|}@J5~T|uN`%Lv7m1)
zBqwz_QzGCjm@JlFB(+56nbs4lTUU=(_(tq1a(z0vRJ$e_CA3G6&hY&>nMaF1ZbGzn
zPfP1*9-Xw8(8;D-QBYTPTriMtJ$Ik97gTFrIFFcviWtdaKfci@4`HWIOK>n4hh
z6C;|a%Q985fV&Bf_EE()WB!N@tHfX^aeqt5x{jwpjq;$3DG=^?9%tmuYS{&165%Di7M7z
zIVsHl;J!=~shOP79Gq?;jz3JeX`1+XupMyVTBfo3@9MF?NE_pmD=-*HR?sr-b730p
zIBd+!&QJbvGP&6G^f}|Hv=-TqKoU2hJ=7#*utbyZ5r=4GX%`UELX}PC4SP9!kNMo(
zsx-imvQ_Bcf*^QhHb!ri-gaiUM&`Mmro1XgYI*1S>i&c107?eOZNo5<`|nG?9BF4c
zb&%?p+x7N1KI+H<_e(6NcFxX|7dHzHA(zr>vK(f2{*BRMOf+u-GmhYG>*_>~SB`&l
zqITlKJ;&h#G&=5~vERY4;M>~G*C8EjZb>YK
zn$F=_ngJ&$QolKgYGg`y&>dz6i=GR5{;bWbGlH*}bqu@{hCw_D}{_m=8-1DpMVx1bn4
zzM;Rn9+8JKN}yF{SD}VxN^!SSW))fCceF0g~3$rZ)W(U6>*sUFY6#wl-3eQHo
zNV3Zm-TZ%k2|_9EdXa)@xd)QIPlh+x7lR*(;p(Ow{$0*L=3UeaqP@#i_125*Gk=Ql
z(A;2>(BdfEwf!S4#k#`ay-=#!Gv;$(??OjOjvMWDi*OM9buKjptNR7XV0o58=j{4c
zcE{=k7$O$vdiTuzK`3hIDEC9decQBs4b(Z3HTZY%&cWaj#_;ct)dJi`tIZAf90K?R
zT8CipQ2O`w$7%%X^a^u~+Ako1hEuEVdQx`FD$SuzSG|S5*Uw|nZ~`^j{l0r4J|v-{
z|7bAKnS}=V)i2(XRwMGbslc`?#q6I7Mk;)QUMdDArlP)}@Zbi#ogJ-T0mO)RRkc)E
zMFlt72ws=w`f#uinEbI2yp-2Tmf#~0FA+P;gV^A&!nj{uXDR+!52MRg90IONO5na6`|tFO{I|Pi{(S>}4>$AwA8tg|{I7S#{U?Uo
z3v=QTez}~4gv1y@@SgkD{?TvmqRW56yO>wU)|0REZe9C)&&BNvnvbPiKnKvGaO+ba
znY)pMYd#e2-RB^Q)eL3gRADNC0h2u{^Dl>T3@ehSFM?i`N
zcE(!nVi%wqe#EyvaF=NpSb&_5XF-hZoYe7E`*Xw=B!U>aOzn7jugafr@~r+3Y=ojg
zzIN%rL-PCk0(6Zi7>KeC>SjrIK5Ko(37W`)UDz#Y;*PHTOE(x
zakg4EKaMT^dL;?BDNp~_*8k@3(cio&$Uoc15-;uAw&AH?H`9@^FR?T3!0;cQa7^C+
zBA7ZZ9MAq=_`og-=dB%a^LV3_TUV%2bkJAjXib0VV&B*-Dq|(FACo0>TA}Kuv~3?A
z9fic?(jPUM>KBu6cmPW}y
zJZJ38WE1$KHavbw>Q|{SZn)HR*(o55@cEBa!5bE%(^(%&L_~H=&Cf_2?q=aE2-ZrMYP`dxVNr>3NRzU-X!?f(Pi
C$!UfF
literal 0
HcmV?d00001
diff --git a/devops/go.mod b/devops/go.mod
new file mode 100644
index 0000000..b80ccd7
--- /dev/null
+++ b/devops/go.mod
@@ -0,0 +1,25 @@
+module devops
+
+go 1.25.6
+
+require (
+ github.com/infraboard/mcube v1.9.29
+ github.com/infraboard/mcube/v2 v2.1.3
+ github.com/rs/zerolog v1.34.0
+)
+
+require (
+ github.com/BurntSushi/toml v1.5.0 // indirect
+ github.com/caarlos0/env/v6 v6.10.1 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mitchellh/mapstructure v1.5.0 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ go.opentelemetry.io/otel v1.38.0 // indirect
+ go.opentelemetry.io/otel/trace v1.38.0 // indirect
+ go.yaml.in/yaml/v2 v2.4.2 // indirect
+ golang.org/x/sys v0.35.0 // indirect
+ gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ sigs.k8s.io/yaml v1.6.0 // indirect
+)
diff --git a/devops/go.sum b/devops/go.sum
new file mode 100644
index 0000000..2fd6380
--- /dev/null
+++ b/devops/go.sum
@@ -0,0 +1,52 @@
+github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
+github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II=
+github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc=
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/infraboard/mcube v1.9.29 h1:sta2Ca+H83sXaQFaTKAX2uVwsXKhAbFbbUr5m1El3UE=
+github.com/infraboard/mcube v1.9.29/go.mod h1:5VqpDng1zHVoLF9WXYelO/jV0WkxSURooVSHzMznx0U=
+github.com/infraboard/mcube/v2 v2.1.3 h1:2UCceLoMkcjxp7btEZQgajyBW/Tzf7meB4OwEA8Hzs4=
+github.com/infraboard/mcube/v2 v2.1.3/go.mod h1:M/UxG9LsdiBVdMKnoCnDOzr3CR7PNBXsygTbB5U6Ibg=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
+github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
+github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
+go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
+go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
+go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
+go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
+go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
+go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
+go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
+golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
diff --git a/devops/server/apps/README.md b/devops/server/apps/README.md
new file mode 100644
index 0000000..2605aa1
--- /dev/null
+++ b/devops/server/apps/README.md
@@ -0,0 +1,2 @@
+# 业务模块存放模块
+
diff --git a/devops/server/apps/task/enum.go b/devops/server/apps/task/enum.go
new file mode 100644
index 0000000..3461fd2
--- /dev/null
+++ b/devops/server/apps/task/enum.go
@@ -0,0 +1,27 @@
+package task
+
+type STATUS string
+
+func (s STATUS) IsComplete() bool {
+ return s == STATUS_SUCCESS || s == STATUS_FAILED || s == STATUS_SKIP || s == STATUS_CANCELED
+}
+
+func (s STATUS) String() string {
+ return string(s)
+}
+
+const (
+ STATUS_PENDDING STATUS = "等待处理"
+ STATUS_RUNNING STATUS = "运行中"
+ // 忽略执行, 等同为成功
+ STATUS_SKIP STATUS = "忽略执行"
+ STATUS_SUCCESS STATUS = "成功"
+ STATUS_CANCELED STATUS = "取消"
+ STATUS_FAILED STATUS = "失败"
+)
+
+const (
+ CONDITION_OPERATOR_IN CONDITION_OPERATOR = "in"
+)
+
+type CONDITION_OPERATOR string
diff --git a/devops/server/apps/task/model.go b/devops/server/apps/task/model.go
new file mode 100644
index 0000000..b31e241
--- /dev/null
+++ b/devops/server/apps/task/model.go
@@ -0,0 +1,240 @@
+package task
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/infraboard/mcube/tools/pretty"
+)
+
+type Task struct {
+ // 任务定义
+ *TaskSpec
+ // 任务状态
+ *TaskStatus
+}
+
+func (e *Task) TableName() string {
+ return "devops_tasks"
+}
+
+type TaskSpec struct {
+ // 任务Id(唯一标识,由调用方生成, 比如 uuid, 如果没有自动生成唯一Id)
+ Id string `json:"id" gorm:"column:id;type:string;primary_key"`
+ // 描述, 比如 "构建任务", "部署任务"
+ Description string `json:"description" gorm:"column:description;type:text"`
+ // 任务名称, 比如 "build", "deploy" 每一个名称 在Agent测有一个唯一的Task与之对应
+ Name string `json:"name" gorm:"column:name;type:varchar(255)"`
+ // 创建时间
+ CreateAt time.Time `json:"create_at" gorm:"column:create_at;type:datetime"`
+ // 任务定义, 比如名称, job定义
+ Define string `json:"define" gorm:"column:define;type:text"`
+ // 运行参数, 比如构建任务的代码分支、部署任务的目标环境等 作为环境变量传递给任务脚本执行
+ InputParams map[string]string `json:"input_params" gorm:"column:input_params;type:json;serializer:json;not null;default:'{}'"`
+ // 流水线任务Id(忽略)
+ PipelineTaskId string `json:"pipeline_task_id" gorm:"column:pipeline_task_id;type:varchar(100);index"`
+
+ // 任务超时时间, 0表示不超时
+ TimeoutSecond int64 `json:"timeout_second" gorm:"column:timeout_second;type:bigint"`
+ // 是否忽略错误
+ IgnoreError *bool `json:"ignore_error" gorm:"column:ignore_error;type:bool;default:false"`
+ // 需要调度那个Agent执行, 为空表示不指定, 由调度系统根据任务类型和Agent能力自动选择一个Agent执行
+ ScheduleAgent string `json:"schedule_agent" gorm:"column:schedule_agent;type:varchar(255);index"`
+ // 依赖的任务节点列表
+ DependsTasks []string `json:"depends_tasks" gorm:"column:depends_tasks;type:json;serializer:json;not null;default:'[]'"`
+ // 执行条件(旧版,保留向后兼容)
+ When []Contiditon `json:"when" gorm:"column:when;type:json;serializer:json;"`
+ // when 条件表达式(新版 DAG 条件系统)
+ // 支持表达式如: "always", "never", "params.env == 'prod'", "deps.build.status == 'success'"
+ WhenCondition string `json:"when_condition" gorm:"column:when_condition;type:varchar(1000)"`
+ // 额外的其他属性
+ Extras map[string]string `json:"extras" form:"extras" gorm:"column:extras;type:json;serializer:json;"`
+ // 标签
+ Label map[string]string `json:"label" gorm:"column:label;type:json;serializer:json;"`
+}
+
+// inputa == "a"
+type Contiditon struct {
+ // 输入参数
+ InputParam string `json:"input_param"`
+ // 操作符
+ Operator CONDITION_OPERATOR `json:"operator"`
+ // In的值列吧
+ Values []string `json:"values"`
+}
+
+type TaskStatus struct {
+ // 状态
+ Status STATUS `json:"status" gorm:"column:status;type:varchar(100);index"`
+ // 关联URL (比如日志URL, 结果URL等)
+ RefURL string `json:"ref_url" gorm:"column:ref_url;type:varchar(255)"`
+ // 失败原因
+ Message string `json:"message" gorm:"column:message;type:text"`
+ // 异步任务调用时的返回详情
+ Detail string `json:"detail,omitempty" gorm:"column:detail;type:text"`
+ // 启动人
+ RunBy string `json:"run_by" gorm:"column:run_by;type:varchar(100)"`
+ // 开始时间
+ StartAt *time.Time `json:"start_at" gorm:"column:start_at;type:datetime"`
+ // 更新时间
+ UpdateAt time.Time `json:"update_at" gorm:"column:update_at;type:datetime"`
+ // 结束时间
+ EndAt time.Time `json:"end_at" gorm:"column:end_at;type:datetime"`
+ // 其他信息
+ Extras map[string]string `json:"extras" gorm:"column:extras;type:json;serializer:json;not null;default:'{}'"`
+
+ // 调度时间
+ ScheduledAt *time.Time `json:"scheduled_at" gorm:"column:scheduled_at;type:datetime"`
+ // 具体执行任务的AgentId
+ ScheduledAgentId *string `json:"scheduled_agent" gorm:"column:scheduled_agent;type:varchar(255);index"`
+ // 确认调度超时, 默认15秒
+ ScheduledConfirmTTL int64 `json:"scheduled_confirm_ttl" gorm:"column:scheduled_confirm_ttl;type:bigint;default:15"`
+ // 调度确认, Agent确认接收任务, 下发成功后设置为true
+ ScheduledConfirmed *bool `json:"scheduled_confirmed" gorm:"column:scheduled_confirmed;type:boolean;default:false"`
+
+ // 任务输出参数(供下一个任务使用)
+ Output map[string]string `json:"output,omitempty" gorm:"column:output;type:json;serializer:json;not null;default:'{}'"`
+}
+
+// // 命令
+// Command string `json:"command"`
+// // 错误原因
+// Error string `json:"error,omitempty"`
+// // 命令退出码
+// ExitCode int `json:"exit_code"`
+// // 命令开始执行时间
+// StartTime time.Time `json:"start_time"`
+// // 命令结束执行时间
+// EndTime *time.Time `json:"end_time"`
+// // 命令执行时长
+// Duration time.Duration `json:"duration"`
+// // 命令执行是否成功
+// Success bool `json:"success"`
+// // 是否跳过执行(跳过视为成功,但标记为 Skip 以便管道状态同步)
+// Skipped bool `json:"skipped,omitempty"`
+// // 非错误的说明信息(比如跳过原因等)
+// Message string `json:"message,omitempty"`
+// // 元数据
+// Metadata *CommandMetadata `json:"metadata"`
+// // 文件内容集合
+// FileContents map[string]string `json:"file_contents,omitempty"`
+// // 脚本输出参数(供下一个任务使用)
+// OutputParams map[string]string `json:"output_params,omitempty"`
+
+func (r *TaskStatus) String() string {
+ return pretty.ToJSON(r)
+}
+
+func (r *TaskStatus) SetScheduledConfirmed(confirmed bool) *TaskStatus {
+ r.ScheduledConfirmed = &confirmed
+ return r
+}
+
+func (r *TaskStatus) SetScheduledAgentId(agentId string) *TaskStatus {
+ r.ScheduledAgentId = &agentId
+ return r
+}
+
+func (r *TaskStatus) GetScheduledAgentId() string {
+ if r.ScheduledAgentId == nil {
+ return ""
+ }
+ return *r.ScheduledAgentId
+}
+
+func (r *TaskStatus) TableName() string {
+ return "devops_tasks"
+}
+
+func (r *TaskStatus) MarkedRunning() {
+ r.setStartAt(time.Now())
+ r.Status = STATUS_RUNNING
+}
+
+func (r *TaskStatus) IsRunning() bool {
+ return r.Status == STATUS_RUNNING
+}
+
+// MarkScheduled 标记任务已被分配给指定的Agent
+// 用于Agent模式下的任务分发
+// 同时设置确认超时时间窗口(默认15秒)
+func (r *TaskStatus) MarkScheduled(agentId string) *TaskStatus {
+ now := time.Now()
+ r.ScheduledAt = &now
+ r.SetScheduledAgentId(agentId)
+ r.SetScheduledConfirmed(false)
+ // 确保设置确认超时时间,如果未设置则默认15秒
+ if r.ScheduledConfirmTTL <= 0 {
+ r.ScheduledConfirmTTL = 15
+ }
+ return r
+}
+
+// IsScheduled 检查任务是否已被分配给Agent
+func (r *TaskStatus) IsScheduled() bool {
+ return r.ScheduledAgentId != nil && *r.ScheduledAgentId != ""
+}
+
+// IsScheduleConfirm 检查任务是否具有完整的调度信息(调度时间和超时时间都已设置)
+// 用于判断是否需要进行调度超时检查
+func (r *TaskStatus) IsScheduleConfirm() bool {
+ return r.ScheduledAt != nil && r.ScheduledConfirmTTL > 0
+}
+
+// ConfirmScheduled 确认Agent已接收任务分配
+func (r *TaskStatus) ConfirmScheduled() *TaskStatus {
+ r.SetScheduledConfirmed(true)
+ return r
+}
+
+// IsScheduleConfirmed 检查任务分配是否已被Agent确认
+func (r *TaskStatus) IsScheduleConfirmed() bool {
+ return r.ScheduledConfirmed != nil && *r.ScheduledConfirmed
+}
+
+func (r *TaskStatus) setStartAt(t time.Time) {
+ r.StartAt = &t
+}
+
+func (t *TaskStatus) WithExtra(key, value string) *TaskStatus {
+ t.Extras[key] = value
+ return t
+}
+
+func (t *TaskStatus) WithRefURL(refURL string) *TaskStatus {
+ t.RefURL = refURL
+ return t
+}
+
+func (t *TaskStatus) Failedf(format string, a ...any) *TaskStatus {
+ t.EndAt = time.Now()
+ t.Status = STATUS_FAILED
+ t.Message = fmt.Sprintf(format, a...)
+ return t
+}
+
+func (t *TaskStatus) Canceledf(format string, a ...any) *TaskStatus {
+ t.EndAt = time.Now()
+ t.Status = STATUS_CANCELED
+ t.Message = fmt.Sprintf(format, a...)
+ return t
+}
+
+func (t *TaskStatus) Success(format string, a ...any) *TaskStatus {
+ t.EndAt = time.Now()
+ t.Status = STATUS_SUCCESS
+ t.Message = fmt.Sprintf(format, a...)
+ return t
+}
+
+func (t *TaskStatus) Skipf(format string, a ...any) *TaskStatus {
+ t.EndAt = time.Now()
+ t.Status = STATUS_SKIP
+ t.Message = fmt.Sprintf(format, a...)
+ return t
+}
+
+func (t *TaskStatus) WithDetail(format string, a ...any) *TaskStatus {
+ t.Detail = fmt.Sprintf(format, a...)
+ return t
+}