From 6576a6b2dffb5cd8c8b4441d7d7046e55be3b773 Mon Sep 17 00:00:00 2001 From: Jackson Raj A <150916845+BackendBits@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:50:34 +0530 Subject: [PATCH 001/161] fix: e2e parameter precedence: instance repository parameters now override template parameters (#979) --- scripts/build_env/build_env.py | 9 +-- .../tests/env-build/test_paramset_sorting.py | 57 +++++++++++++++++++ 2 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 scripts/build_env/tests/env-build/test_paramset_sorting.py diff --git a/scripts/build_env/build_env.py b/scripts/build_env/build_env.py index 9caae2f1d..4fb10c395 100644 --- a/scripts/build_env/build_env.py +++ b/scripts/build_env/build_env.py @@ -180,14 +180,15 @@ def findEnvDefinitionFromTemplatePath(templatePath, env_instances_dir=None): def sort_paramsets_with_same_name(entries: list[dict]) -> list[dict]: - # strict order processing paramsets template -> instance + # Strict order processing paramsets template -> cluster -> instance + # Lower sort keys are processed first, later values override earlier ones def sort_key(e): path = e["filePath"] if "from_template" in path: - return 1, path + return 0, path # Template processed first (can be overridden) elif "from_instance" in path: - return 2, path - return 0, path + return 2, path # Env-specific instance processed last (highest priority) + return 1, path # Root/cluster instance processed after template return sorted(entries, key=sort_key) diff --git a/scripts/build_env/tests/env-build/test_paramset_sorting.py b/scripts/build_env/tests/env-build/test_paramset_sorting.py new file mode 100644 index 000000000..7c0552d65 --- /dev/null +++ b/scripts/build_env/tests/env-build/test_paramset_sorting.py @@ -0,0 +1,57 @@ +import pytest + + +class TestSortParamsetsWithSameName: + + @staticmethod + def sort_paramsets_with_same_name(entries: list[dict]) -> list[dict]: + def sort_key(e): + path = e["filePath"] + if "from_template" in path: + return 0, path + elif "from_instance" in path: + return 2, path + return 1, path + return sorted(entries, key=sort_key) + + def test_all_three_levels(self): + entries = [ + {"filePath": "/tmp/render/parameters/from_instance/test.yml", "envSpecific": True}, + {"filePath": "/tmp/render/parameters/test.yml", "envSpecific": False}, + {"filePath": "/tmp/render/parameters/from_template/test.yml", "envSpecific": False}, + ] + sorted_entries = self.sort_paramsets_with_same_name(entries) + assert "from_template" in sorted_entries[0]["filePath"] + assert "from_instance" not in sorted_entries[1]["filePath"] + assert "from_instance" in sorted_entries[2]["filePath"] + + def test_template_and_instance(self): + entries = [ + {"filePath": "/tmp/render/parameters/from_instance/test.yml", "envSpecific": True}, + {"filePath": "/tmp/render/parameters/from_template/test.yml", "envSpecific": False}, + ] + sorted_entries = self.sort_paramsets_with_same_name(entries) + assert "from_template" in sorted_entries[0]["filePath"] + assert "from_instance" in sorted_entries[1]["filePath"] + + def test_multiple_files_sorted_alphabetically(self): + entries = [ + {"filePath": "/tmp/render/parameters/from_template/z_params.yml", "envSpecific": False}, + {"filePath": "/tmp/render/parameters/from_template/a_params.yml", "envSpecific": False}, + {"filePath": "/tmp/render/parameters/from_template/m_params.yml", "envSpecific": False}, + ] + sorted_entries = self.sort_paramsets_with_same_name(entries) + paths = [e["filePath"] for e in sorted_entries] + assert paths == sorted(paths) + + def test_real_world_dcl_e2e(self): + entries = [ + {"filePath": "/tmp/render/parameters/from_instance/DCL_E2E_parameters.yaml", "envSpecific": True}, + {"filePath": "/tmp/render/parameters/from_template/e2e/dcl.yaml", "envSpecific": False}, + ] + sorted_entries = self.sort_paramsets_with_same_name(entries) + assert "from_template" in sorted_entries[0]["filePath"] + assert "from_instance" in sorted_entries[1]["filePath"] + + def test_empty_list(self): + assert len(self.sort_paramsets_with_same_name([])) == 0 From db7bdc93100f68a6643a2595f5fa8ea0319d3299 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Wed, 4 Feb 2026 12:22:37 +0000 Subject: [PATCH 002/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index bf9146cec..61069d6ce 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.21.2" - DOCKER_IMAGE_TAG_ENVGENE: "1.21.2" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.21.2" + DOCKER_IMAGE_TAG_PIPEGENE: "1.21.3" + DOCKER_IMAGE_TAG_ENVGENE: "1.21.3" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.21.3" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 1ae54f281..6ea5346fe 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.21.2 +version: 1.21.3 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index df9796cc0..766ad25b2 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.21.2 +version: 1.21.3 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 7198d12d4..debea1fc7 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.21.2", + "envgene_version": "1.21.3", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 2a8061052c2e289d21f3e7e8ead9f5f87d7e7c7f Mon Sep 17 00:00:00 2001 From: Dias <120464230+dysmon@users.noreply.github.com> Date: Wed, 4 Feb 2026 19:00:30 +0500 Subject: [PATCH 003/161] docs: app_reg_def_render job (#976) --- docs/dev/app_reg_def_job.drawio.png | Bin 0 -> 246778 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/dev/app_reg_def_job.drawio.png diff --git a/docs/dev/app_reg_def_job.drawio.png b/docs/dev/app_reg_def_job.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..3ee00509f01e30213fc127f6bf5528c92276748f GIT binary patch literal 246778 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

e}LizSrZOt>Y<(rrO@ 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%0A0sOL|3uwmQt`+p1D z8By%DY|$ZS>nqgMk{cqx3(KzHc+;A*K{cOIAxnRlyAG85P9d%?@Kb?;6pw{!Oog3I zqJ;4rVUN4`e9iJTjr<5Uv_?578Ti}~Tnd~xPnUckDSv4Q?;(4#SxYd&lKczBSXN)1 z^8h(QH{d1lAWovX5hsf0oKEtqi*@G=Z1HJ>V{5*xGE6>pifI*AgQN%`6=U>kMQ|qE z;bdg}ues%9XLF5EA?$!xm z{1qYAPMhXgE?K~ND69z2p~W``q5_!Ftu=_KKptX9)*+uxqie+E(h>mIV6f{R3&f5R zgMP_h!@SOAhyYIxteTR4!^15mp`H>2^`K5a-t2A9cIo?mOeksG>}r`s3IAbVDX*n=9oRCMAM^>X|IP*$i=D6DxpNVaU1~2I zwXio{;;?W2A1S=YNDe~O{Yd2rpf01eCDVedmh5uwi7C8;D;GAC8Zg@w#usI%gm>;c zWGr_JS=N`i`u{tu^X0}vFyFpg4;&H^ZWle*zG$zL=ITI2Tl^<{fQSUH+pmUJ&(e3v zSlC?_MpxkSN~^_0#8V?!y?Q&>BEJVl?c{&rxA}cmCKhZh*{64MQ7WdE zjNLMaYS5C8UoV(cgUFRalPnQ4Ye-QyF#+&w@DDicIT)y(^y~zw2MLBi!&wHs)JU} z$7akePoF+L1m_Vm^PD8dlx$anWtyxg5i&mb;Big8BKet|l@&_7>*Y9ZhSH_OA-uo} z*1j#=l^9{G8wE_9=5Ob(cl1HDAckZQeGsNeA5V~o0PXpvfQ*gBR~>Gj?9vn3zH3&S zem4#AH_qwl>8-^fSx>hvyXYnXceEAY8r;sRJMCo>)@N~b`wqYLLIpxG**V`?VTMn+ zp|Kd`*J&h#&rAPIRyEx!F$;3-76{L6ZE9fvf?JHn&yqy2#S!^gcZPN16%!a)S%>5G z(v`uONRUQ-nx{Y(rvGO@e!;c!%)&Hh1FWcol5RV-@!4{Kp@+=D|5b}T{cA0<|Nqt^ zdA6rE+1ucwcevXti+rcO4-G8dcocMQY)DPl zW^bIk#s-8L)`Z^Ibo^b(V~cfZAqQ0;YudUpKF|4~weE_;*bD^Q3L(aiBx?2O!LVpq z-P+zh-ceImvZ?V}*MjIEDU%Hr}z>xPgubJwCb1++V~sy0q<`^!%9)QX^a` zQGXxPiQ&@O${j;`_r%NY;02UG&t17Y2g`mmjJw%?;#f=}qW7Ov_>Vr-*Nf6FaiG zK5l(s(fo5D8(r#N{HzqXxk)tCn*SO56zc&TT0h+byn}PPy0pwvL-w%?sC_(Fk4BCy zfGNxVYFMAtxDyF6v{9#Z6Sx52*LXnt#eRHdTmM<8a^8h1C&3c018l5Shsr8+tm>nQ zH#u<^UkX9?WH#VHCXy@$Q_sa2Ug^j`LhFd0px>4Df6lUcQT$l_s$qx7g8PGQ-}>&? z0B919pDg`Y9xV}&Q>vDhKgjKY*f|##;zgIft^O5|R7pqQiSDM`&Eym{$AX0(O+ad# z=K7o(=f)cl?XWktRd^0-2c{pYbI=isv+YD`A49gLf%=C}W^(DnsdDEs5ChUepu$N} zw*l<8TRk>1?SrZWdDxVJ;3|dMv{Al11iPv_LHBciG9g7KKkULAE-KfIfJ~@>)DpU2 z>eXm!W1mV=$d~2I&C2>PyT-)3nrB7Yp8uqOi?wTP=XMip0RrUf%j_+iK_^4+gtSNfLt{7oe=4DTty~R|P)uW` zuW21*bAXLq=++4D6ur;^B`?Jz90)CWiRwvi9suc&C81bvxj?R(`yW&N&^)+Vi9FroPiE-O;5F9N5FqZ0N%f)ma} za9rM+t>6U(+6jTxy4y?Hk$SdA>3r}@#mTPFw;;6$1jnnGV%kM7(4DzNUp5@e>j@p z3jPk)(wO{rh?Dw;y0a2$kysBEw_p?>A0G(CISN#!A(8|FXII9L;UQ3B>$d;85jF6g zU-B72QVyi>N=E+Jxe?3a&iH@F^m96~7Z-RH*ceplukXuAw_TDH7M^pOmxdl&x=vs@ z^fVhZm`WIA^-bKx5Vil0iy-hANGu|Ht=4Z_RE^SD4n{^sr`YBTl7di*2>TD*!1m|r$))|G<~$qdq z2v(=5zD!Aq^OTDT(s|KS!eY{Jy|wx;=udd38m)B_hlS;z>~C9yYH;QWEE5FCmri2< ziUwW4pJ@jzA1sMCV;+pUI4E4Krgz zi9}Xjz9cK)-JB%sQ~e|m>Y(2n{AGiJXM`kg^5P4DCN6AHD((!(oM78GzdJZTDhr#> z{Eh((Ezo;fk%bVs=BIv{+63Ui0V2m+p+Y=0(bO3=mmx^9fe>%4&Zqf=(BkhnIn>}B z^+0LLBeee&pJ5B4l9MfXQPI^Sc1VU-#7EOBNr@xlk&9G!+lP({fYAg$3Bvti``{>T zy{gm~EJsLJrDak-b>h5~WIPd{o$GIek3mh94#;$dl8{)t_-SZ&m<2KFq7vrP<19#G zuE3hC^Z&9+=+OP$US>W(`60x6_nOO!Q=%+fGglda7h%~2Ssp+-=L%Myj({GOBP1KvJ~|Ad`k4MWBM@D3q0q8BM zbvs|=ToempVqrUDjS7N7-Y!IJ3scFN5{Xku-cc9qHGvJ~vqk>xoF?cRoMe#xIMiE=Jm8kmwbW=L;ctNTTG3@n~K`hQLfxXEO3WGNlWCG#;ZWeHbMetQs zE&0O<=JIOevgOCQhX}%Nh|xQV{AL~{GQSw+=Yg{qkHUQ&$v*OjIbY*^AzSJ@*@k69 z`ifz{r`q&R7?H-x*srNwWjMQ|A5aW>vM9#Esn)GCl>M49+pnzR-=JyA^hsW&!w1?| zhk<~B05v55yDJUYmDAV4+`}U#A|^_Ti*nnvd6a21Q9}RevdG=|i57zNXCF@=wnL!h z&Qo`6UQT2o*BfOe>*u$%o~+jS?Yqy>ghV{uS%*q5{g=O4U>jf9&g5{F!>8Nz7akY; zp8wX*WW41VZnCbyMvZmL+?z2(ig+0K(j$rYV8W)dq|GxA0sz4M5K^_;PiK^GRH@LYzaZ7N+x%IlS3ROd{KdqgO z!%WswAF@Fi#R$W?s`%rn5n-1#eVRa2W5$2jytC?SBs#A}zq!>kafZVB(q=3|Mn6r^ z@B0*8l}HMZ>}#2^>>c;t^Qew|v)R+I*{>Osu+~TQs@1-ClHYHSZ5W=j!kUlV0*7Gs zP0oG6SCJ{ye^+77H@_TVn|4&Q{0N`;IwD_(D7(G+Wb}i|webY?Eiz)cCnjjf3mUQ* zPBxP4(K~8oRewe9d7zH_6XDz-7-P3B8LdMRu*9*&xsTuQ_J2M=QYQJ6zF9efI;qa~ z4~KbXiFy_|yKl+~d<)3a~Cer0bKH_T%-@ z>bDJMSXAlZq}ofG>rO?=T9O_1Rx6IY-QRG8=;=m}BhEJ6HsoDt@kY7)XY>GH+uJiq zIXcNAVH2fL3|D$^=un9gS?W~=n~7l+R((xT;60VwEN14F&7&94Z6GZz{S%KX|9tpR ziE~Tpg)bA=#2iPZ`X+LF$li*{d|?+u5(-~SC*7YLb67d6_};oOVr|=McDBF!i(AmE zpOw^>ZEeM3l#>aV${+kDDH|fC^pnar=-oDG$UZyNzWtiir&OWah6ZMrFP+SpoPEQlKcv>H{Zx3}R?;#3hHNmb|KrKp!3mdok6fuk z!tIn+ryHE7u$4hw1azJ{7pp%!@eq;jO^h(kT#dTF+vjS`@d+A99P^WSQ6_e3!zB=` zwyO)-J-X}T(~v{_qizqAKaV+{<1w-Oh@bhMAK9rp8*hBwBALmrb)J%PMPJe{@m2;q zGW(|9%0x}~55-e8m)8tTxE#6I_t+GKXqt-*mi99<1MGb^Q?sF9m$e0ZyTK2lHqZL1 zUa+{r9F?zsP`^tG?Y`B$^O=P(QG;0J;~o%KR_?AS>xe49)(N1&X7mr@U%cm3JuJ-G zA51HCiGS&AD#dc8hNyn)CXA^uaBDp57jglUw!8MUc2EOJ6W7k^v-aH^op9M=b|uVM z-EqBM;od17b(O`rap#$(?+=YCA+id~H+HN2gQ~+ZjqLB}Km$4@5tAB!XIG&CU3q6A zpX|RqF%uIm4`y(IHiJube}&uAnnT=! z`;l+VpF7BhrPwYRDlzf|2Yuq)jj*6`z93z`a9kmNY+e`yZtGrUMIq0%c%`U)NeIMi z;omOEVb*6Ze-PEM<@~SToF^mBoxX^tE@0_C|2r)AFY+Mp6(pYg?gPi{uloM*hf1#C zWd#N7Uc^m#!uNa^*C#RC3yaS&_hF&d#KkQLr2)!2jRlk&*F6jM@r7qzc!(+9;GaMj z)0pL=s4jaY7*`E-@nu(*umT5s$V(f_TV^YBmk;>7E5fOC&TG)-Kv#*mQH(m6^b38F zGY8mLq36{dzYkpe#g71YW6sl<`{|X1{wF+}j|+cUPGfwf9AzZg~rGQ}_6tAq&P++*GA2jb>vDm2O0RkG(DX=6e((kA@T# zeJ0;)@>`i;+qZpJitT0(w**!@2R01I(uc$Cj<7{A1QixZbU-3enf#0qKx)8Dk=|TT zc~Q0V;n3XljEQ5USAzvug|bdja#HxT1M zFvT<4-GHqS$@LR(3epis?q3;FvLsNdvNBnP00p4X(OyV7|FmWuk3_~r=uvQFd4(5= z9Kx>KbK6Co2f&rsaJMVthR=^-;`tU~MtOWCGbe*3hHMxySDJEC z8qfLmiNrOad)PCi{t2Sx69tt-#vrqH=J#1{CmWSDnAoz?IolWpLwEARu1NoVbSkCI-Zw(0yE8OlQ$)ll_T^! zb2Qa+dZ@z!?c!w-^0ej!K4jpVv%PdR<)dkIO5<_uUbWf$O;NnK>G9~s^8{9kpcBOZ)@>zM+xlgQ2|wb9bsM~~9uykd9gWh%I&^$yYU+4NvvqqIzNr`p7@Tu7II z3pVoBZc>2i_%PtIi=H557V4fonPbz$v1uv*8zFl^X-VkUjY+AxZ`zKXo}jCSf>Ph^ zw?GH#aka0pzVT``$vtt#QW2s)in(E?UtUXYv4SaORI`?mrcj-Upnar3zu{sR9Hr&Z zJ9IdrDpM4Z+euFw?@DY7kO*k`5r@W=OC{0-TV(5$X4IlvtqFFOKN@g?AOYyoZ$@p5 zuBa2b@2o&?wFl(CxQhcOs)tEX{j=s2#QfX;KClV4k^#OO^PBOt7a28LP^XU)=wJzYnWwTkE_GXj8WgL z?u`f2#{Do*lXpYFCtkW6uyU7X#@PiJ9H1!^K8AC=(~^pZ`!T6vibm!;$$C z-hur#k(~jZ-tBay-+Jw9LBi0xz8+C{O?VDtTv&@yc2XO4N!Prb27Rg3xliKVJX6`uDA+Iw! zPU|cG_`QCrm)X+umUN1Uz?-0>w5eU66h97;hpXrX zcD0gmgtI6cgcy2#F_enQ4>~$hL7+7$b-eDh)Z0`{rkWVhTE8+gyAtpITbbYQTvY#E zQ=JCVq!Kkx7e$s`U9_722tGQ_m36+HTE{+nwnmgb6?h5l(>IMPP@S4)y`tNZ*_CpY z!vVU&gIx{@k~TA+T1ml0%{ie>(>J>7(9(FXmfa+^SqA2i5K}=Y&eZ`YTV1#HrEh1H zccf0fzh+Nt*z+w?DXopQw@a z)R>cQeUB9Ey_t6(3^0|8!l0fo-HuHY9APQ@0%e2J@}$4sLCanY0qv5YMs|o(f)z`D zW{8lqeiNq)KFt>Lyl1{A56@zJ=nn0v^p0-vACN=*G^w&4FAJu5f|oR?yA*=FvCm8b zdtSa7{O|~J3q=t;e81Q(J#_!(W<9p}~0odYZnBF~Q?C~-Gz<~*QTFNE>wY;As zd&ZZO_a?R=sL|`SiKOQ_tU7(X!IJKRRzYj(bhEz(!ZolR%Y+EA`YgRG2O*Bk!!VKqjY&|#YZMvRh{(fCvH z*GfI*0@c_rjXl#nHd4pQe#4_h`X_RqF|xuNl5JRSnF@q3%g2{wTd{QLSq%vseF1F(<+4+XU*!tGa}YqTky^f%=0vP*Lv~X1dbjw$V`}R zJTda3g0KADQD*B@>L;$?!LKnes==^V!45OGex)_kGG^9BLy{zCoTEJ2_=63yKz z|F%5Zu)4HlCUF?ypKfgDC%YMl(-6F_$PC}Pw|5IH`x{0dQG$uLw0cv}&U(^?(${_K z_L_XCMKW28jtT|K_QZcmkGhG_*j;6B|0x;Dxm3VSxgVV993V(V5 z+Zdg%;yCJ&lvy9Xiui*ten>2uiuqZtCL!(nev2K``&|FKs-bU^{E^XnCO%NJ@DK=g zqkoRMwe;Xkx6csSj3Vi1{@efqMRqQ^^9-h6h;^;n3VrL^U3E1ZO&%{!0eXDcXS~Tl z^R_&e;BYF6C+})Z%R~Pukrk1nja5wr5hp-ovnZS8cAC=QHAS8ngs$Z|t7PlnF?}Mg zAb|VfX@=kl`YueiUG<6GSwcNNaWs2NcUp3NXZ5rEbM6vX&c;kKp@a7!dSjCpTKl|Y zVmf_xL@G|02(m#1gXkX!KvKU1Wc%`SA= zkmAI^{#)b6i=<2>V#UDg%N>s+1@_3#!T8zk4dKhCSf>$2J{P1*@6 zby-P#W}S8ChAlH$4al{>e0{-v%ETz$_1=^+t4m0dDsXhXLma`jeKpw$A=>T#u!y zMJ%(rfzEgzSLWxJ7wADAvP#TteE!ULSW8AK@%j#L-1o=$ZCk6HsCGFptbufYUEF}# zxU6RYh^vYtN;ypPxGrN*lcn1aqum>y)Le!#({}NLkpAN`xu8zsIHFKY;YsK>@oSRo zcY9cFZ(<-vICwmwDrRNdNXzJOY>-}5murZmM@-kr@@=-RHk~P$LLZ|T$*1laN_>a3 zYp&(_?=Y?HES`W$#a^01^bG@|GeeVmudj?Vx zfh2L8%%bLP?+C9NGbp9HOde&D#W$FXE9F0@0nMVs zlZNtD?tFdES3E z1}4l+WafRh3F9&As91nngb{jomnx{(Qo4mee7%}Bx^^NahDl%FoRgo%l9-@JUvW>3 zXd=5(2Y6-5Juf$(p|*4oN20kU&On?d=15pPg+O+$H~%knApR72RP17pjL+&ZyU){` zQdDdhiKOvaaCJ9{u88bzwAFTwyUPIin@)I7TROzfVOvJLZl^C-ehIJ zD^-o>59UA8>~5tN|4lqvq+>qFQPUlqT8V4>;eNt9(sZ^*p&c(7IhvYs{Ie&pp3&B+ zv)y@J7jff4%)Sqw3#EZx>^knQd*N4ZAgY_esBMn}?>3$C6s~B%eu5{uivGBqk zx^HJI=tfkkb>E@X`3JNV1ouG{q8m*i`V%C1CwxrDpOT)_XMF)Hevu=5ZaWkmLD&MV`aWlSshC(V9rSF;M8aA1+}hXH1x^m^+a>uD>Z@wdl;>1?`6 zj({Lbk|^uo@&m^b?X~(4NT?cwpj$56NsQLi6}C$J;0x&JGSp4m4NPad?$WP3zsuig zzMJ9y@C5%jbw8~r;GqitYwDFB^v85iQGp}0J~zm%K$31GMS}AL((#gAV?D$OpT1p0 zGpgh$S0U7lc^=r^yFhIz#Sf948zOZ4fWEw4!y&=Po>{qX$W+Sk7&8FjP7cf*V>^3x zD=4FqzbjGNeOp>^=~qiXWQPQJ($^B%n+U02QI!iLsl$jeoe9XPOMo=fAKVF>F5ybYG(U00h{Lbzu+o9~=6iXcy z_3yUq8WlQ;4aA2~S-b9mNqJ?*Jc<|{nD_=F<5}o!(bHf31FvF2S|Vk;)9K0t@@YVK zcXh_8m6bQm_)rhLeB$NiRN45Jfb*w+0bpgu!_*!@0BKHE3VE&q|4WTb@l)Lt*OKY4 zXMZBmo4!xOHLi0PH-T)5(m?Q$0hO=OM-4D}UY+hu@|_W%OG_1dD}$%Oabi`3K)7l# zZROqL=~+r=oG`^xZtvl4-245u;#4;Wh`tCTw{2g&b%mp=;`HNb7wNO?s}pHMfmSz~ zr}c_{n^^o*O5e?KnHOy-dKc1f{d4dhH-7^o{m@Mfse-EX1Q2|A%lcJt!j{uzt zDTZ3%x^U^bC(!>UnkfYhups(t3xGgH~nf7y(B(370# zgrUSrrp8C;tDF@h_79JZOS8U7N5Kd8@D{h9t2MrQwq-wE(bB9xCQ)5Btdlgzho_5+ z6lEA5Jek$6!*X_D$4|306x&9bPZaD7r{YcVMW69xTt#kM^p&%qj7m169zYWg=y!w$ z4(Vt|b?LDtC4Gzr*lam9D+qsL|u()HN#ybtBXAmtvklZNb;ZAMs8OPH!Lwqo7jtQKbO_c5IACbFhJ2)6k7t2#X?Oem~x?Qilj zufbeDVF&-*r(3z>bSeLb>3%0dYF^41wab-KFG~l6TkziQv(>Z%`co~@A51=J!5-HK zsgfK8T1xLoLt&T0*)Qjjszct4ugx+)pzxfp@a9 zHcnP%*-Oc#JsaBJD7L^Jli*a6N>CE2Po3|dUzQ^<| z{@@W)9kD~FC^fPChl5Fl53THXTM=u`7~@tZPPs8rq$m9db7ahW+PAhX-uq+c1o9GN zM~)8joEnhx_*j>O8HI+Hj&(jq*FmI^^D=17|4kKhWNqdfEw#_(>X9$m{y_c0&4o&{ z3Dmfoj^`fxe_Vs}zoA1jXkkd^abP@$?k)oK=Z~WKT)jG?!p48kcqx2w*($Sr>0gNV zDkA}ABkp~$?f=Dt;gl7&BRxk~95ClR^3&Cm|BFxy>1_&JD>-ltZIK5aR3pq){GWCt za~)EvPHu1C{!7+;d5HlqJiyY)%5~^?$+@K%YRM~`chBZDh3|ZBzEvx}kXq-AA&k`mwh7YaHNP}Ho$G$LP|cXjs?U9SJLJ|&WJH&`{2_|u zE31ED9AW4Cn;!ja)dm{B3wN6f7M7`%m?c1kggP5Bht-+aGPq~?CBw9;P?`3o`gE^O zkHud#?lw$EdQ^1PRk1UX=6jKTmO$Mx&G2f^ORJgFS2b&r3fAqe%U3Dzrw>U#YPwZ- zJ&Dr|c>l%|j?eB@IbIq4seEzA%=Qk-MDATt&p%IZ4ZFDS+2v)A=D|~yw3}T#xbty4 znp?TV8?8u58E4c+0UShJEKL21)c3rryC|xlxeJKNZv~zxn?MF8D!TgTiHizStMhhp%53ISTMWzU;I59Ytg_-vdV zF}kM|?VLe*(!1~zO}uX<8{Ag=?~ydUv6*Cw-;jvT#%#QoxfU9>dmNiw zf(zC>Sm)oMgDp%NL_oS^-2DZ+1m5Bw^w?_ zh_t84RoXQ~6@njsY{T`8y27cu(qhC3D&7J&8QHSQQ_a)$CJL`cOC${2uoOKNUUD>SxZ(ps`+Wkc9p24 zXtG_zh_ecW2F%YolfQjErHq7$+ARtp?vEW}jKx&BP>76|-(&SF)!!}|pmdsM|3ZU9 z*1%@cs53_R^0vbfyY^v;zJuYUI95L105^StDEoW0BgWK!PYGL4+TjmXVpMWxbKycW zS7eu3Ba*Zm9_$=+@NRSJrHxVfF;I&t4-F{p_d>U>%?%D#Z?0?g$yJ7-E zEi@-`CwHIliU1k#Cl6K3L*h>Es*Rh7nusnK>8(D&dp!tRHqxRxak;Z38x_;QndkbE zpT}H+nw+xqQHp3q45p9G7@)HKTlA+gP1+?c)R5nwO1=0P{p!wldZ}$zLBkpz3V-$20ln9?Ove|jQ{x9zSeOAryAyqwWdl@{*+XmvN zs*|+&A}VY#=P4v2Ba7ONna%Vqy%1=6`1;9hZ;KD3pn0_*^GEV@J9+d3j&}7`hOr{w z+MP5XkD5!Lg#vJoX5?%y_3?*Yd>U@qCk1^F7q%PT?SLkLEOYYAs-0>U8`&YI5eQ3e zibZBy9>fe>ysouD)w@A+cw+K)vbAM1D>d|`F7bBaXm;We{*~@=Wn4!K6NEKE`zAFj zC2ObZ9j;C8A=D8hS5!|gy!!6SLN1`8qSzP3qs@#T_jl)XCntLKS5WV&%~<;UELcm( z|Lm81G-VB*S(ljC()#%RZu7k3WZ$wOi|XhWj|TdB!c0s{^4FQqnOUzU>&e}d4X+5w zaEwfRIX=&>n1h}b>m+SFXEpiS)0R?+8DLOX39+w=fA9=dGamDy-7^VJRTOn#KEzlh zrgLrWrb!4UzW1=Oi}!M!YQi zC=s^qpYiVdmvENqwtu3^x1Ig)z(&jZ+u*iE#@gV0b(m@J;!V}E4A){GBlzje*3_cW z%>Xi+pDnDxwzm)wkG2?$;sX)1XpQjdd+E82{|Y4Z4)Ux-(Kl$>+@LB8XhMbirt)y@ z++kgN68gz4W-B<d`xCZ2bd%e4e{sNAL9e$E6l2;pa$F%z*_qyhX;A&r$)BsYe9PVWe#;6pYS^jb~d0K zwkwutjs}OPDgUpiX6Kw8VlQoZw+V0wR<;0wc@w~@W14epRKN;bUtj-zv?_t`S|+mp z|J6AllVBNeaapg61xwD|{5JGeWhZ-uUh2Vx|953y7+x}*E48-&k6rc)xN2e4qH-E2 zc>qs!YSo;a2b{?|G#>GXqMN7sJP?+YC8iq^q)vu*x zoa-Ja(KzE6R5-rGCcJe$p; zYLJ$9{r&x%l5++Ehc1wfM0hmx+-u=05aod{WcqICZ+4E9&5j?}pG~uk6V$MgfSB z=%C*D5zF2DvEa)67)I{#@HIz}UBq7r5xWNL&Bl3{c(QaD`tLmGN2*|`$-c0Ynw z3U_);G2~`M7Xyg}rDCzf?+Gt|QD{tS4NQ{AAYa!mwWMO3Ab8d*?>lR(>_r z4-HUJn$r4tH?zy8-gC%P9YrQYMm z5T5P)vo$3gEW#`&X#R`@SUJ+$BYB{qZw|wL?IF^M`jhN$eaE$GLP~kKHA+Tb0e6L6st5|1r+v03-{v-9NKV?%a#rzOEnrar?%Guv#-X z)6VGOU~`-|0Eog4Vn0u%v<|H|iU!XcYr-usM-eeo#Db&`|H;hA3FC<`NZ5?dJA*vb zV04f#=k7Hqu36VMRM=I{{$s-H4EplzeR404Y!+zUJHTtqirU>8PW*5Qw#~_ZcP<63 z*g&3OHx>HYs)sj{{nyYqonsh#k?d*Ug=COWTq6-iRD(+Tc#uWAAT|;h*cTPM1KV@< z%&jPn;YJ;Lfi$!!OO7(LM1(Zg8>(Ay#>DoXy1kajA^9um8-B&(CncgNNVJJ9<>?Qc z0mbXX^!xtZwf4$$KRbeg8(N?9^5H4A1hoN#(laMTzG#G?NiOi^sp!Fj1lV@he*8Oa zZV+y)gI)6L$W!Yr!Z)bOcmOfm=@4wg1Tw?ra<};Smd@+ zD>-U+PzEdC;OVDvhSpidWr7g&Q}4C|)oL5Rmn{je ztvTZfy>X?khIZ1AeVXFw_cq{*a3sDBzV;a*(u@XAyH{}rZSE={(M^vJ2ASE7ms&g! zHst=pNgPWAr5L)^1vq?ER_`1SheeSnJVzx!T4Yxv1N`Fn%2ye0eCj#**HM1h$~9-! zUjKvZqOdJD*C(XK!<3x!aRG4I;wm-WFQc8+LZ0G&Lf{@RA3Tv$Rb zDn5r)FZ^yhHR?)hvMKW4JgL_I4qy*+8p+m{)$Q$P9%w zkQ)@TWe|?y3mC!nWn7Y)!`!em?Y3RqeIpzIu!v1G{3erqIVR{YK7oB$C`PokwQZ(p znQ}`Y;%8q?+(MT3T3(-yY-!9Z{>Swnfsw1*9$4*xQ2mmH;Y^&}puA30TgU-8AhD5e z<{Sle+Zq&dpPIi{m!gT*r64;$=(dit<6v*{A%^>ML)R@%Cwul7OiM&v`uG*Z^lL?l z@0NC^;KCXZj?~8ORU%vw@F$jV*eZR8WI)L)EQHG_aj~(E)54EJILl?yz!rKa1mx%% zgJlTpS*fxE*$dt)+4wHYKRR=5y+zsW?5c=lJ!Fp@dGGgj>dWe4&O`3s7iQuANXyhS zdg#FT$0Nq5& z%}bnPg1#ctt&ZVBfRLb6*8sQ)(-4rBRg#H(*sd!~LFCnTAu3qaAO=(Oo%g`^h|Zga z9j$#;_a@42vP=S@w)ohJbU$$LKGMwn^okm47#`^a^M)5vge?>v9=h7W{NRJJdp7TxbHRYe$Q$U0cSkC znD5i%L@^J`4;Xc70I3mk0Q~4j0z@QF&~Kz5`G`QZpc5EDfhu#&{H8khAQBgZiL;iV zpc~Hwjqhysk9drSXf-VtEqRuHY!KGXu<{r>7RvM zK>uyesI$jT^8*Qpje~xSG1d2U=f64m9lU5wzFzbTSi_ zQ9}7x;fJlp4+9lW?f9_MiItiCJL0zt4IKw3^--U>f;~!HBFT6DCN*xWWltY2+eyh4 zB~CSuY9XpY{*{%cb#w0XEo&g1wN3cx*r=&h4wvW!)ev|Oe2K6CtZaL}YDPiTz@|^v z*JAe~dMb;_MlkQy>)vYEN~}giPN;*)p;6#kMfE9MM+V5V5Eey$bv5aM+(VUT1_v3;GRjWwY60>dF>gzPFVu<>IrS>-XPydi( z`(-L$K7MgP<`YWy{9*5n*EFc7)h17af-d$s!ejenlngYPf&D~M7Mw{4Mt z{BZl*GsBHfv|l|>+#RV0eGmZIls`_KsV0hR3I%vr)$ZnUT(jvkoN4m?qFU2)vkxkM zo8PU$UgCp>&}e3;6Mmb#vx5dE!IvV&&2>>)#<69>V0b(!Q9VW^@>o;IU;lirpSb24 zH@5#-g}ezyiwm2d6uMjXtCx<9GX13^%w*U0c&bq52ovn?_qU4`UAhKr@ z*%RA*SLdE9v5uZlTML@JHm^E#%29WnR_@N~d(cOvDrh@r7l-AKU`r3I>6;WBY!Of2U6O-{fY>VOk%?w?!_2Y2 z#PbBo`LjDow^TIwrxJ>fha33!=6CS654+rK$&I{)%PO)W+m={2WVh!9wK>*1Eu6XU zOjh=RFQ?wH`g2|eN%Z4)4a1sYw*&fddZ8IXo!t)-j`tjVu@&_&2SlnK+ZN8pha5)_ zOySoKFv$pcU)w>)=KrSyj3_LB)stC^oCsSHuC|3l#DX(jeA1Q`bYYjqwdLeSWGAed zMJ>xCnO%ZwZChi4@Kd+%6Go8B3s!=BSm5>^2lB?Fi7o}Ng%x;<2e|yde)okoYSntJ z+z;IHOJ~#E-pmQX7caXJgyNMx{)@O&2s&rZi}c}IY75gI>;ZBPoUZam>l9o*9Zimc zjxV^QGll13A75f?Oo@!z#80rHMjzvnK9;5)A}Zf)o(7FVoHJ&kSfK3cCgB6+*7zsr z(wn`3Gow9S1D5*nJ-Ppk1(!p|`OLF;I2b5JT<%>Fnu^NlV3aB}ikS>jL}%XDXV<@{;rp$Q#R&01LZg#usp%O_MkvG*em^(G<> zb?Asv6)vrsZa8ixn>V5pBT-~!+|p?dPwZ`oF>X=@u^lG*d4%rnYUm*RaX_gzjyUxV zLC&Gm7I^z$zp@lA1X>I-R74M?aYqBRgHuw;u|QYTk{;SMy+RNI;3Rjf{M+7!^*$;< zv1268$%mst>xa-Y5F~hW=|I+%=QEXcF{MZYEylN3ry3gODD1wgo1r%mBKWG0i6TvS zKw7S~3kWay(a@pzrf;PmDJ5L*uxP@q%LwAO(z_E;xs}^E=?q{yX)(I8PTbje=E=W7 z3${Yk6m!h4E`}G6_HB*Z{#yh^e^Z+*1*L0?blGXmkE!x1j2AvL{j-8?-{9_d1QVB* z&`Wlr3`bs}vQG@=LQBrtdl?<|#@tqfX+a0II4C zT9pwzx8_6dI2 zNUF|esxVp?KYkU1vd~>Skq|kZce*;g=~&1Z?ikHedSd*cT=Vf{OW&c?R8HDf7Utp>nQtBH#ANj=9hhvo|Wah<{|+RVXt|z`nLlzZm9C_ zu@6g&0@n1O8V5Jf!tJX<;*K4b&^(vy8sb1aWrX8?@-RE|%5%G@*m2M=6-N}6APmmS ze(PYPx@!0lTDgDUkJbE^=T)c6TOuuwHlO>;Un;?kAVwo!1`NW-0~q`L{c?)NS257f zZ@q5$$zbtOZSnnKWbFhwp_z|$+7q9fWm{;G zPgmC{^E|KRocrA8KIfE=8+sF}?TuU(2U7N`%;{h5<}|S|=p-qtPV!3n-1AxEYa6Ji zE-TbEdwr^^k331FQIq4VgLK!ve<%kx=Ucqz~PW4NH^{qlS7mB=2T4 zyOrttU3Pt`Nh14dz*kCC$4Y8RZ+W!c+R!>ES&FmZFx6Zmj($>g_+nYb@H8M$b*6^? z$nbc+{D4-g>Mh)mAOU3ZEA9_l$cg~I zPafkVDkds_*=1hxXfd&J7xCRoA5+|_Q-6|u*?dLqmqK_KauB|I|JSYwHvy_L|u_It4aG%tHIGO(lK*%>f5_x9Rvk3UvF4pItsYqtIw z-Xyv<8em$yu% zu1CG|!I$T}*?Y2jKjZxl`uhoNGGoWB>)m+7*s{+B*Bfh{>5eks)MD=lf5^UQbNJNi z{ZXT#`)oU7kgc(4{CuD(F}t}IcUP>n1l4Hb&V9ZM!933|g*jFrnGhm~XQ79quQcAu z?XC9st<-mW05?!TS(>%E_UGM#xBp@V>F3+^+tcjWJS+d&m99*5x43AR)#`nP#$zc& z?)?!Bf%Nhwg+t0Y_3;vB`<5mzMCvk$rUb&K1qU5K@CW zBwTfs+Bf}2B~K{|=~L~3Stj`f67r}*1!N01*P{u?2oF#hfwsMZ3EXY*ji^0yCwENK z*6zL^d_L~ktz2}|Dw*rEGGE$C^Ss);hFh{#EY-2+d0Fi+qJpaM*qfSQ+eB%; zy{1jB?J5liUjg(JC**g+lpjU?v7-7MFt4(YfBVbl=)k~1m9;Qje>mx?y!=S{joy8) zsh9HyzMi6AZMP~a15$$1V7G>35qcU|r_8f5~Eq@z~Wzl45`((Je zxXfp2j)K3bP40&3LGabTM~|sD7Yi>Jbm@5%sR$G zx?O`cBWRg~=kSDsr3S63t%;4Toc;!*T%(Ci@_XDA%Qt@a^s$;&XAF9^uVD33!6!G_ z#Al6!^%WBRKms`|+md0|N9$6K0iPwyPy7#DDJ2$mhfk%soty%El3Lf8&v8;{3{qac z0rz8~)eC0r#h@)UT;>5MPx?xqN~&$kE&%7%oZzM$|D>Rr%zst|OcoZCP7LtJe*eaQ zwL|?+my#lt+QSaDC8cmKcXbP5B}8KJ7|lm zyYJ#-=(6w;63_phyA-uXCRf>r>eC|$%`&I=9A0g3Sy872;+*dRA;>KPx1H}xkE)9L z_-bT(!2CzAwfBiuk;|2W$3M0?P=NehH|o%pmEhd3g*|Jl%U|pRW_F*HmlOT4^m_Iq z3rpm<3wh)d-YP@!gc!KxOkfSC z)#V1(sb+y#qe(_qha-dDpp5N~FK;h7RHxE8R5rEB{{*j=`ASOzsj^7vA%Up5qO75x z-oHmRfs8wfl_HhX(0}AD=>4VItXx_iPJYl5@eNUG?C{nTU(|kPkw9+mtkjrEZt$_J znDU&aShg)oSyrBKtF8*LyN@s5nYQ!9s}DP1$rv>{1`d7b4+j-GzF>Cj^no?h*D|8j zLX+$vfqLwTCNFTEzongz>BRMgk{^giEj=juDk9&$R4-p}=+{FYXj-kR)Dgm;KSd(M z1=}fV%NkjknjT*^9z09gz4YSTgM;YNd)%!3p5p672@lOKQV7GewLA`RqG#2C(Bn975cBNz2~Z8B`-clVh2}1rj=fPmb=u#DM~eMa9PMMhmv-=FO$EG zUHbZ*-y`mNIQJ4Cbt0$0uq63qGyhZ8=rB}BH7%!_3F_9W)Q+U0pOP9Q9QeF6lP=Z- zT)GhG*ywS1tf}1{)i6hOT$yT|yk8KkoJDTGf%dm! z!E?H(^?reF5I_8sn`Nrg{G@2ZPXa^tOFpk|0v0<(Etaqgc}9~Zl8pSNuC}3)-$|{m zw)_}Scpbm`E|X=R_L3m2gB3RHiKYfl)chGuAx zv*yrE?F)0a!4S<5$AWid{y!HBl*d=u>?_qA zk0pyW-c;<6nkH2tRf*)AKgoV^f1=XB&|Vq+y}`c83*(_>RKBJ~Te5Zwq&Tjql8Wt=M-q%u{T-^kbzh!YRi|9#E3wjD!tuWRZt?;+)~0Eb z_PFo%&K0c|Qly*x4lA)Fp4ErWuZKd^6v+x2O*k7KXv~&(Kr=37Re9$`!lc2si|o<@ z#<&@)t-HB#K|v>~wMWMr`mCfIdT`BFhEb=vu>E&h#Xs}FfPK;%EGP#@%Yy^Uoah*7 zpv1($fz+krqwDVOHU<>PaBsmnxhcJ4>Ps1iKIn9{QsHTB?yq`pcFN~+xc%ZR_iG02 zlBa=mS(vxB(`ZXl=3jgJxf4dLW^M@(d|bZLeMMWZtyWwb`lwVS%hU;5SZhnWCpg0tNoRC`V1jP@M*3MMhtYMtEtp!c~HDpI!6K@KPKw zP{FGYM$`kkb@-f2*r_d{oC1l%3JsS6P{}`J_!`a~K!3*@3kLJ4JgxJ;|uR#SkJf)7$QU z0@VHz4!FHj=R{0N0OJJ_UR9l8eq@D&NvPb+e+_WUj5#I+dxBYh_Jh$~^KV?LS8ol< zvLj9q%X;^T9wMxQIX>~awEp93`)HuuJY(_IRwggz*k(e-447&a0;=Q{l|hI0tTz3% zCul$K*Psr2(pVu`5qEX9hxhL}z|V3i*sr`T(O_uMuJPZr+#9C@+_m^^EIJsv^Dmk@KcM&aoa%@Bt0j z-iL9D8y@-?OIiNDPg+P}zF@7pnkjS-FB5SAq?~!OPaW$!G&l#Qc^qT?{AQug(dWh| zk}^?eT68dR>p@aD;mn$)?~UEUHy2T^7`V4h4+HE^PqBUbONLbB=M^fYuF{M09t`5W zY&CnfFhor+fY7YAV{&PB7|*F!A{!gKd>E;&5_AcIyD7RsHgmd#_i?L7U{{r^{@DAE z+P??mGxi~_6Ujh{sUw~XEQCzCp5KHKzNQC^4o<*m77-u&wvk>rb8aqZh2oVnmIf;XzK&X5ns?NN^&F&s5L$0TBmZf~a?cK^f zG|U)&2O-0D<7mRQl6EZ3(}JIbxXRXb=yiS3`E5U{pK_c5EW)S(Wb{EH(`TFaNew@QFxA+2 zCVU&G_1&&S1KNrxhZ&>sYVHxt(OINw(1qA*v3vN@Jfm$%&w?}~+7=Tu-omSvDurR% zKqzxwyyTMx?@Kcl0% zQh$<(&2_)(AJFF%y{uE{d1NYf+>bWLM_@eXf1zX+3}p!66vxbs1=I#H;44^1n(TfY z)!#qpVT`_Ig@2A%q8$jsAiP;xn>(3AT`@I*U1qx%Igs*nCm~P)d8DuV?-ea5N}%3E z7z87~52Tb<+nU?YrM^2I)_$W8s8CEn@vOInWjHF=@}1JukGJv+;jeW3<-iB5%6(OS zK7*(Ls&9ZfMz-E``Jh102m7;5Ki(WS9>S%(dGiJ~ta+eb)i;CwY#`;jc-)|NHgenBHP@i1T6%hmxqK zAOV@f+56S_fY4^_gA>Nv!rSBCax8XF2ty`T8!0p&`; zmuFNd@3}z7@G8*!pV#g)9{m0G&jq9J?|g3el})GuF>Y?bj>(A$CK0`4(9iqsZmtYm z?6q#HqBDRs{B%AB8{29HmSWn+De1}fE=gacYZix|OecEBsb@fUr=$Nz=nrIWZ00Ar zZ9vPpq3QC03oQBOyd%ln*LT7I{j=`SZ*|XC z`||`0Svlz!fANoO-a|^0c)^_`+VOXY%?Y7mMj3a2L4#v_-+@Lr*I>K%OK6&}Z#?LR z4;r%oPn+a3om2r!5+7{&?i%GEFN-1+;9YCh~n zMn7G097J&Fr=&CRY+b_5EtasXQ~;`Im0XJo8J#4h<9KEAN1d-gL}=qV%tUN%cLZty5lh5@z1Xuc zN%#}ra`^FvOWd(`#%v78^=#uDm=~&{28rBF5{;qo`%Ix;9n5f3WG+l$>9h5{cqq!) z0O|u$rz`)1V-K$qN4(VKg9^#V9WesLH(mFOPJBiBw3YXBO#_aWqSBYjPm|qXJ0S2Y z2xo)#DUt)2riTeX&Trf%=m$v>(rdW&h4N}%&D0JG7}vrd$*;->Wdzk&?{FIBzWDWc z;ai?LmniPnhx=+XLkcx_nxPm)xYZ%7;R8 z=Pakht*BZ8g=hGe7ys-9O~|yOz6iOVwT@Jpe;>ySQ;QvzbrRKx9s})!&!-F#X??`iuvC;*dhB6^b^;RqOu|4hh)D8iG z=;AA%9u@jh$#aU50Qew$G0jn6>c*V}OK&l#otjv*8yGm69>_s=6eG@7_+tHm)EiNT zf*uj%hqA;ANlKiEhR;$21|Vx#1X&CQjN>~HkY3YHV%WXE@O)Os>(2X4s=gV_(`<`&>(4jzTd@S^#H zQfiX`cYd$c^p3|a0n8$N&Np`I*;>n0eO3PlUX)g=3)e49HQ->#ay#+xn{i(F;1m8Uzc=0AE(Dz*# z=GP;bovX>bbb_D#p3u@cYpDjLDYagKOsza3x(FtV>}ue|GRKl0Q!CTf9${aN0x0$LBKdmZs8;;2n-Bc`L_|fZ8b1u4wzN`?zVIv$!cQrnYhzC^ z^wJ>^PG$X;@!tvLP#XlbWcC6@16D@~LMiWuXFaux-9<3FNG|q!rbRvus0HqVME@a%z|-TZ z4Ub44<4=q|C@XjVagf}jC`>q!)z&i}v%v2?_Pj>1@0W#QT(dzkwh47%0oU8lMMCZx z58-wmLlKmB($XXwv`%(C=WS?E+&BDOndI=`1DdGAG$&G{)dWG&@T>Y^)}rFP*j$FP zUXcRjJs6f(hh!QqJ(*xqjXdA2^kBavQ^~m(Kah^|*j%T#Z{D0fUZXQ3-_S=zVlP!Z z`JNtyY)^hrRHm0?kfgQRnp%-`N}5-`=WB`CLb zC`6Gh%5IoJ%C2|J)XSIO1)m4=SeGOeX#%souCaEd0_OYZc=%eMsQN@sgQUL?Jl$0B z{Hj}1?X^CKSj17I*`#rol$&fz zQoHiGBYr4v-|&I6o77}rTJ$poov|Q`MIEMhx%u=G0 z4l+<~lih{yace|HB7CkTU)dnOEBSq04O z3W$d3f-x_X4=S+Ik8c*8c1-Cugi{@&9M7k)atFD?^OkZ?Vmye(cEqBac^^s2SYWD# z7ZSH|;pZ0UCXP+?pb-5sE#gCNc>Xl~D@Rj8q`b&P_F}y&Krq_*b z3h^dUCiurbf#ElWM-)l{SeSdPp3j9H?gx^ar%$nQ2dHpzJ{Z6nOM*c4hSgBZVM_dx zO@F(8^MgCAXSc{ea0ifTfySD-ErLac_Xk@X;9l83_ANE}S%7vHLy;$cj~R^Ab6GLx zE{s1kQyhF4RPyi(pbBVyC6?*lgiyE#V!;BGXo@WvNt#Y4q_BFpRZLKyM~d$nwar!W zKemg3BE*7Im9=kU=CW`-h0ZOI&aryZ?Gl9B zA3SR?mUIp$n;lWM&GF8^#q-`XQe<%)L&u;lT=VVLJT8=sT`_%9w+7*lQ`v5Mm0UBL z6%a#eCx1!u+$zB8>6Gq({l_eM5Z*uBzvzy?3#R1G*qBd0SbS}qgGp~M{^fg8gl}3S zh2<@nDl7i3+QdomTY}@s?+Wr;S80qH@-(hVys7_pch7JLAURgFk=MbJvSh|qWShBLSVM7n)re6Qqt4F#)Xq$|_ zeYgGi&?p7eC4WK5pL$}F>NavSKk@ac#K{IE{rtF3;qY_Gi4UgSBFiIB0QtGXfPew% z=rL9}$hI9nKXVNTxaJ!NXBAWYjYQl<6L?Ru(U-jf4cd-aB;JIPT*X_Q( zx)=&t^rbD@jYhG23+=wrL#CQjDWy$>%f?QPSZHhT9c+c13>=S4IyhQ$ zVl@%{K#D5|0e9Eb$K3I`CkA&)NBR81U3#6BNUsIW40#PeF+Iui!+sLfEOibkwKh>@ z@jI{lrxx*qQy<4`L9Ifuy?5Hr3eJ3BvF-cSaj7c+NbTyZ8_!5jeS9c1j9(B_)zdx-vWxu-?}uzvdCYTqC4#klc66ei1_7Y?O&==53jNhUk1}MH623#u1}&=U!CG&DhsrVAhx7AwV2mzJhedc+p4c`+z>;2v z-po*E(djw@M8MLgefDlS2+0C%W%k&Bj5- z3e*SzoUS6}2q=^d55yI80Up(PN_Cw%gGW5kHuuL=x-Q0QzWYk*?98_naZIje)La;9 zr*qSS@qjNl9ALlzyqqz=uAi!TSLl=WulQ zKlF|CO&DaTyy5)v3b=z45pD7VzQtA9Vco_&3HEa!8=TSpbm(E=+^Huy`ERl<(y9gq zJds@UURMUdT)zVdw{G!Ba%Ag2P7KTFvur?TPtj!Zs?JyWPrk-*5jSuiK4qOlSi z9_-A`-@t}t4ri1oYbiB&3y2`aCKmbQ*Ar0E@%X+pW;?9%$)ETMPfPq&4$%N!on&Q2 z9)LUFS#af|M_iEX?3Vr z6#;n3^WzYG`fh{S`>wO@YD5Jl^O|Nmdih3QvS0Wf#|mBC$KSbnedW$YnY zc@g6ZLHn4wrGhh|d+q{q?h$YHv~49ANP1C<6FI8deJk|j&z(P>rK^3}&D!+?@H~T) zkp;aQ8Bw4hn!ia|tNA8u#V(5JY7JK_vqEIstR zdSSnm#berY9cOVDg7-w0+=&L0Dn?x^C`f@?{by@ej~GPV0=;mJb2=4a3jHag7O5Lf zjmydx1u7B6lQMYw9-zLe-VUK@CrCk)Oc|l>&X=CovXBACdxuIPWqNl9KvglIw>noE z_jsaXmlQRv5_ZS`!0p~R3Q?|qHvYS-6&}O(8c><^r!s-;RD#6|0Pt(i)`#BV7p1Fy zZh*j6up~a^KfN85s)i#^c7C%}n&D>JOM#K)rc9Mt{Cr3ilzj`&w+k z8kJOMQbmq?aSOCNyj|&6K6ZfNsYze~TNN6Fl(jil%~Zb}WqO#Q?{plce>)4bht)NV zR~%e3?6G(3TOUG*crSin^~V4UAdf&&;rYUO5O&IYGL{vFIe=7H)&ZC#WwcdWJOEL~ zIwpv82M_A*|5AM4&g%soEOtswQDaz!eW#EOnx8{(X%IPANXGlYFpW|Cdn?*<&58|K zAK+q#I=ta`6@DKhE|@u;Qk>|Nq65^(FJUjmdm89d(h}I*ihPh=R9;&e#uqJqlR`Hq z9Md}`1vPt$AKe*8bG)t35^gK_%+Oi_j`-RsWfct1HxvALGvI!s!>iNT)iTLj`E-aZ z;fD+`PBQ7dBSJxI8`lcZVb1-)wW|z)`9oJBri25AcDeiPEnF`D^{+5*Ymst;J^8Ga zzaHM|XC3uT-O;HgzAvPYZp;K)Be2yeb^_t(V+?Xr;|ZXhQwV=6asrFUaX+nBbPY4 zr_8BY41`ngII40MzCn}Khc-7pt`hu8`S%x2BhANsXKSu)*&DTU;1)A{IJ5OO4!h_` zDQcjcT6*ZFuX0A@vv*!0@QVmj;@O(3z-Lk23#K<8A3M~5+mnde6LdP){TJJ9=D}9^ zf!qKzJ4r#9aB)w{&HU78D^2b*86IF;$@~Cv6Lor%uT3c(oC?HJ-fJU;>^r~P6H=}Qec7S}~viU>l#0A8XbUc~qu(KwlsP((WPWtIN96>|p zWRbBu6&tUo_3zBdMg~~b3ZY{{sFWUM+VH46C6gDX1y4pq-%Mn8THw-rQ2+{86j(2& zItNV0`-y5N_3NchyEc4(nUv9n1GVnEXG+hiB@Rk|kMf^O#asV@=-D!AF-f#htAj`R zYO~M&c|O1gU5uCc(fa1Utd@fiID<~^s3a{CqrwJ&hYqGc9xCm?cb_@Dpdn+6VV_Yn z$Q2eq)JIsL%kK(kg`<9a1cbas9q@6>Qz0j4mp0!LB%RXRt(uZLcM+`4)r^89!(K>} zH;79FfyGQ6QtS*BWCBB=h%PU3Q1}EzkZ{r8*BTh~vL#3X__CSB5xsc7Jpg21IQR3! z=w0p{|B_jEpIrooKODKN1%HijXH$EDo|e1L#UB*pSq!_QaX9`v&$wr^Qw0FJVCU4# zYWz0OUf(Hj5>Hy3BW=TNteEw8rf}0hV}rEka%ZjY?9jC|j-GUDxef~w8oAlh1lvc< z(x?fEV+2Atui(oM)p+|3&3E%Tjg|oGs@Bx#^%iur;81(B^y`7q{!=gAo{1GNg~nY3 zZEgehD>Obn;Pa68rJki?T)WXudI`}wAcCX#s?Ol|-hr695#P2B=TctU;bZN=eM^X-wq&P_}F{YD?SpfY51VKV+SWg#cqX-svCu z9yrZ2P-CBKM@#^&H$BbArYVrEa`hW?u`Bccz@Ih{eDqN#l;jyg4q2ORqZ?~xxaoY| zgan4p_?i82j0dMdsg5kK>`yY&%gPtoPa4Kdw*Z@d&DN#Ri3Wg~F{pey_dBLTXTM2* z{`<7k#|D{wK&ElR$u@HPHf?T#n&WN-@9nRLP@#hiIDWdfPVqznN&cY3|Ak%pDXR>L z&o_TZFqESA6xaTH6*LpDdTB0J;{Z2(vE&g zSv?X&1gftIPPt>G0<&V_`2*NI;DNqFq(TFL78H zAVk-g(gL?GqJ`qkVEJzmAWd1d>m*;_i*}^r@D&0SorYKMuxf{(4zOG-5JJ2UY2q|W zpmb`a%yu$$(C;w8r6fr;!2hwli=`*fnt3OV0)ZCp7wA8oSFXF92AVs#9ZjF00$_I+ zWYN<&pM^gxlDRqoOk@-;3*V_a@^10n9~KZ3zFefv)a?z0w4m>z;=`aY2HuoF33=7N zJOX6O6UG^O1iq_niDs&|fc`1ui1m?;w?6^4zk8IcXZ&vr7tCIGQI=8XiOdmY+E}X1 z(EGbKfc&rl;ff8=ed4)O`lE@XDEV-l+WqP_Nc{PQr3b>KKXm*-qX z01ze~Cq6LOic|HNxCo?nELOBn^8~+z-cwPb&w4Z>c^Dxvpq9vm#FvYN<(oNyuwkJY zpdCmnScGaZe+tqG>{+5+HP@{im{|k0NPE*#^yhuiqkdsE?I8R#JEj3zyu-8-+wDSM z+2sPfq4$UX4DUMd(w)N^&2$Bzeh(#0fRdu+yTfQ;Lcy&d^^r@F*yHv)!u5@bkpd`vkXY|)GKE28&Hgz%l)yV8OLff zVhvX6CXUiR$>Vb79~>HA2jLwP=ep||+I^8^O}f%wpVv9{ci?a8T>|q{A1huUA(dBmZph zF+neySI?VYY}mc0dca{(F0*U=HT{qzm!F=_IfHypk}n--l&c5-6ijY0AKV{# z5}K=lRK;eMHjjVyZSK?oJ||=(4fM`CZa_LWBhTyI#P0_n<9vmN-Ky(9;f6Z z_7f>LLY4d{n1zSb6#1N*zW#kmAZfx28Uy2eSZep*ge*vet-<;e?Yd8`F>^Q{E^e}Z ziBE=_1BFBb%UsvN*9Nx4)2W<>s}Z4`2EJ=r&QTgJMM{GFZ;jRPwTwVzpn`A2 zHF)0gFbFU8YwNx=zHaqXsL3iScGNMsz%S?mkGxiH)rf4o>3GkWuVT~=j-1Enb!vNN zyPa(dkrMnv^8=cgz-e*jfa<(i(s|e1K9N7SD?`aYjnNvs7)`c&Bvc3y_xr!+ z4zB_(u0y``90SJ~2OTME!#C6*nOub)TerQkYlhLi1O=vtUF+l;kW0Stz}_)nSeWe) zxwmZV4Z~4|j|5fsi3i#*1)zuM$qI!WJS4TGF>z4*&*oHy$w|s50wIAyiC^phZmr~#qm)m)e55$=x)hi+afJaKy z@=8qAX%bx%M4aP>N4W7if9&M)(ZvE~K9r{2BEX_7l{g@VnPMLnBGR z0lWvWalnx$w+SGTCy#DS#lBPNP=wBUCv&MbF$J@F2S~5BErZZLBg7Q83ril)Wl9n)0Nb zx$6CnBaEG_fx>D!O{eU#joZ3SuxximSFG7j&A7d-;G@o&>5dqMNZwyfBtLeOKFLoy z(X{HCf6%>PXZ@3tM7L%5Se8*6mf%(as?-;8Dw`!+8;>Cb!56nP<`A8veyT=z3tE}u z)#Ys(%QVbE&gD51Yxq-H`wNea(w=|ieLz)wN0HM!our_^w z2mVo`ZN23-QgE=v1L2&Ut~8S{MvEACzFRWqo+x%pLkl4Z>q6M@!RUiiDH8t`32ec$ zOh!oTI8J%cXY;e|)CTO>#2(q;Qe@xT$O}&HR@hKHf)r5?m>M^>De{{_J*y()1o?{M zCTBGzn@Bh>?I)nMYG|rP)M##q`9!CBU5wuq8B1EGGygeZxo0;~=!Q@I3VacCynx%1 z0BI$)FCAmeC$443G$n!x4b<0tjq^c2P7*oQatEIP+;?=n z2OH&)@Y2cj2nRNg7nG?i{H~<56$0@$P3XhN2SpgE0-5YmR>LPq>}yD+fNh}?CFzb& z)b|8%gHv2>k>K^gG^Y;vv7juD(%bg*xVZFQ+6x{a6N_C075_#mNBZo3$3B;6M17^% zsB#`%d#)2+O3T6IY2hSaO|BPrPGvlE-Vpl(FsGZ7=JpKrwGYcY&jhIFn$K{F|4{np z{ZKIQOGb$iItx?q8)h4aVgv^I_?OdP6O}B2g~jRT-aGXA)a4+f-CR=41(jqiQ!pjd z6AY7l59q)5tBV8te4c}N%c#ZFJz~3bKw>7gDJFp({|HJBowznR`pjj|Ui6YRM1dK- zg;$%NKO22?k-G5RC3AwO_)N`J(8nn&VuQ;8IUk%eD$z{SOH()@XW^?~_F9>sk?p)u_c zHTO{{>hL764QO95BXV{&zJ}BA2=ncYK6!#T}!`wy9x0p?9jrNILd z*xj1$G#QE{In3;(Q){yDo9`aV&dJw0@>t&E0K0mNY|^wJGf%#CUfEEp;W1xrxf@aI6-@`8=8$PX7bF5G@T??o*g-1_Zcc5l7QBE#IO?;=u-AOI ziIMeDPUIjzhI>}@9TLlQqJz&E@w#LGGEuI_H%K$LvdM$d9d#j+F}{db=Q>9?x7KRE zhDU$*EYZywxRyikDWkup$@&H0yLDqLS0NCX1S(lJHZtUJO&CwmIc0R@hY23CXA56c zs?g^TLccwEE(fr%_cV#W$R>L7@Or@w93C{T-|QEiT|zgFd^nl{MJ2Jpry?dEY79Q; zK2I&e&jI}N?dDO2OvIt|;7(O|+|C$1FyrWAY1Mbpj~7)H?ANhmf?!P#D&5tei(zJPLIDAd%#G?t7f_e~8_ zQk$sVetgC9pnO8@WFnR)0;4l)X5^d9VV8Zs$}GM9s7PDU=?B&aR%O*E3<#9)IGNQW zeNEzNWTqDuQ70hL%F>ER&}BcOtP3qO4`_pTHJDDvaN6ZoyGLrl~TJ)s;F`9S}%%emb>wM(yq(Ga>k__5JI<8EoRArc`&R z+7S|Nt(X}l76`)*!!>>b7H-a=iqfuHy++l|^*&@>mz|uhoe;7?*s=M7^tcD>DTyFG z-;jMdSx6woDm*<2rh~DRw375aAlAw0&3MMM_XTSc1(?w2rlQkf8n6bWQD8(BMQGOH znehcwgBm{%3BBBu$==X9Ob=SLSgz|xl5w`@dh7P8Ci0;)dqDx_3D(4B(yl(45Wx7P zqlV5{MNGI#BFF|IHJrnlJbs3mV2v8m)J`H;ONj68aX!lpymG;Jy6}kPr4rG?H;)!~ zM}9x7_@5)e3xcH5L0J5`jYIkLe9cCETAt7h&{xVvhsfH(j>4}XcCf8HXDQ>s;S$=@ zXk!|x2O$ZVXmhJ&o559J1#F-yk99<@e!LQ7eo~l;=S%d^=6%0-=C3{Q?Ry^KtHWoP zrH#HXqexi5RVOxQSrEu+7LjXX2`4mT(|_tyvNTS1rK0aE_Z@~ZbVD0K+pB(6yR0*c zO|r+7QNxc>hKqm6C`8vos|#JVcRu(#$9Sjp->QgQW{PF-WVywO|KVmSK21Rwz)0{G zuT}QIs~3FRcqk5Y9%rgH%ct;o2B^||8i35~%lT=Pf3j$R4_B(xBV@JVt+>%2NT=#Wd-8w`v1muNl-fAQ^C8s|M+uY zsqeE}+p&u$y4f0ce~4&E{%#y-%~2r97uB1M%iWo1imJy-n0uhvQO?VMlaPIvL74T{ z%4V}fQW{Cu{1*xiOQ{J=fNDd=YO;9sKF}?fEVOLO21!T?0Xd-%J~_31lHD&}NS}L+8n(eang5IsQ)N_&~Gmw@_mTa{ue^AN-2mxa612YdRpK zmLyS=`UZ-@??O!Ok9)t{g9bZ5XmolWcNLM%tK=>^gD6y=S8N}ySqeYsJoTriNC>Kh zW*9b2zX_&7!A-8pJ(4XjV}O)!a&j>FYmEV0RwU&JoCg#>FzbTyug{hLpq7-H*NlZ) zq78jV1r~6P@GDo_lX-2!@sf?yewVdTmyDGSNt6 z=msPZ4=19Vg&UHczU=a=H+4)^PVV1jj#jxU>=)1WPJ)imRDqhv56{{{6 z&*ZuIi5GU>E88z&8ciFaI033DzAr_O_Cl{uaOUjWu49>vd_EGN<-OoxP#Hk7--6km zI*?#B3cRnjFY1pH;K58^T{*90vPJ>`m?_r{suw+DHQ!8Rv_|Duz6e`j^hLF~DK2t4 zT&1%t_Km+N8`V+dksV0+PvD>d_Haz0FKqKQu|n24;C9=1&VL!fVGu5qzb6qe2YNb^aluEj1buN{_bvi z@$j)2?vdw9QLCUQab{Im-JIwYwT2W?&2Vc^jXaj-fKB6^iBe4W@m5Kg-2~?hXg5I8Aj*2$1R@!{G zvaQF2mp0CK?8+(1CYt#fS3SQV@Ia9jnZ2_!+k=8~D%7iSN}<71(Mz>^cf#!9qc7$A zuc$BxUjj>b){1WVpA@Jguryf78_U6Lg;1ZB%~PaCPZuPi92P0SlJfw;#KxTMKWZ49 z{qB|N{8@9wqSM!b$$HmxylGID06GT}_{KH-f^4D8Ou-9yDbTEF916(i0|# zcRSqW5Pu3PocDwL;DoYpbkP84*D>E7-$Xj-L}ktg%%E;`#!J5o3hxLWO(J8s>Aq6D#ww7|rLF;~R;SQR zY%}G60X&Y3I&~C-Y6UUdJb}XP#3(m=;3E7pnW{eTwZbo+4+Vl(Sh3RvLAe5{eBy5k zu~j@#KCjL&gb3D5yeA7C3+=dLyk-A3aw6BEoQR(nProZX_iw+RlHeV9Buqp3P}D_B z#iS=w*@R4p6o zi+@XXz5s;`@VGrZ3em+d4;*2><#&=BA)Evu0(|-JFV~Fn^pcbV8e)w2_KLqo1lc|4 zy0k;Kh73ytGDgc_dD8$@_>JFE)=kN3@xwC>%$5S?tj9Le2beAILALHuRO{ zoLemdS z=>)R{IU&b1|Ay^H;d@bgH_H5YCSgymG&eEB=>k5UT>w$+KHgcw)X4MN4oJjNg0R!g z@shiNZD*-qrovAlLJC!TpyY=)x9__M6{jFXhV##8~VqkA}>k~Ts5VGx-j zN1|!L+r>0|avNHP1z0caNUA7&A@hyo7j(0zPi|L!&g!p=`9>;w0rU~#i}GEPrxsUs z?d*M6fogwVX6Lnunm2eL}mOe;Y3#L zIK>R~;f>b&OrM`aheif|fm#6oVdqb*-J#lpO*6f?8=)~vH2d^J1d_TR%o?7`=f5;W z)4C9Jibr;5!|945&BcL$0PEupdwe%~`#7{NpR-z>J>U*6*(oc9(Pz8i*tOrU!PI=5 zbGVn&W`tcCYu7ih`Zhp*<^8dh(+cim`U0j)^~d~Cg|zS#j>c_$j6k&dJETv!#z0~w z2W@+`CU*w#tXB)kwchnP(bYO5Fs@l zLfK=K*_LR*pm%P~%C1Z2KJ}xJEw4>0x*SS5=nm0re#3V%0x9>6$0U$-d5Fnar4rV? zmikF(E}$JkLezj%)spRnPykBhcw^d`PRJ}@kFx%vfhIrf;`2i-sM1nZnQ~#N=;>c- z+!<(i%a+5#qd;Sa7N5cmZVZZwtas4qZPCg#W4l>(1bga)rZD*y2W)fmazE8(t$DZn z|0U-r0v$*?CIW3(7(eVJQKqg}B>K!dt2Lt49;o!J$2HNY)G7$$m5#K_ChY=SXAKpT z&3D@y8^QE|symH(Dt)a_XbdXAS7UwDKx2Azb1S7>bC#*+w*BtJFTG!GFZ_GvKw~C^ zLdX9j?7icue*gdR7iniiLlL2j>=aHoM=6q(R4A*wjIy1`$!?I0Q;{RvkXj&*R(`CX^1`n=xX-#^`+InU=c9@lj}uE)GTKzQ9=&Ue=5S){h%Le!t8 z)M)mI&x4qA!-q=axrrLj7#5m-{Mlib*LO;UHs2CTF-XyE*>o!Trds#Wf6g5q1{Ry0 zfXz%?!GOSc<>CJOE#wD>_9Mx`8T%}REKr@6N>TqDBG#m(&rK)puC@BHcC|$VneTF@ z>2B<|N@iW1b-fZi4PN)dQw2Dh?gqVzyK!_6Q;4hN)rLuK7}WfU_th4Tq+_U`&s>!q zCxF>Wx4q?s%DELNfi_ShZ)Z06Y$#0rHP@|nX-ehtch-`>NAXPdO7Lpuhy zGw0XzN4A||8^+hSN8jcBKQ~(%hFjik@_%I#V8OYBIJ&!(4Qe2U67VRu5s>XJ zhN2$J8!?q9j@t&idWC%hc_(UYmokppx&g^}=H5d-w+=C@%*JSgY?Y5BAkJ_sn8++1 z=Fq%J_wXJ{B)}!zvrXOH4_nQMAG(nRtWy4r4ZV#SXdw2y zr71<8^WXU&VEdL8%|KoZRqjpH0e=bRtD^?iOLJrX|9~I@Z?)C`hD5t0g<+Btp z6!UwZg7M*kqUR=$R7Tyu$s3ODJnXmE(&0ToajEApVnl~Kfv_vYj#)`2mHV!B^MEw0 zZTv8w!k?Bq#mb&!GW7UXbKjzFbwmng&gey@jTzA{0=DU0mlRWptLM7!rB4$0CJHOY z^j2cX*R@AmIi{<;-V0^?x@eaf2Am6s_VFy2J0g0{`MyTwvxDAqyGwUXL-cO%u@w{r zm_RjFuKW>V>`{z>?pVG55zigPDN&2CzlARMNi5GBjsk&3)AqIG!^pF+84Y7zDDX7~ zJlM*(Zywrn3;30&1($M@|vUjE0!^{EW8=N#W4p) zc&LL^GS`YR2o#3-1e{5X$y6FL(&X8afPMZUNUlmLzAaYSaT}ms)$kMab?iZ`@yB=X z-o2^-VKpP-pM>2b=02`odXhbZST(>qJw!Vhx81t(kf2Injvo>zIhKY87&rcS&zsp= zCwG4u)_+ycZZuT(1pRgs_Pz4W*^4n;iYlwdngmH8*k)H)-QnC3qFu5vi1v5Ot6m0% zh7DqHPd;0mTQwlZ9qBRIpwFx@ZxE(&gCE$H7yV?f|8aCLChXnJo4IksClY}MWL#6|4RETXXyw*z$`E;DfOfY9NyC&dIpojXeR&x9yI$ zKOi7N7zwZX_p=UwV8WeDoohycRq@MzZohNDuOY=92Fu9x@iTnwokgz6wcMgL`e$KE zSnv&g%`E2s6s)l_fW_v|ZTYxyPvRC?_khzQ-Y?`yj}N`rFt#{`;2fS_3^z>3WQkpg z;7t^C24)b8rUWkYO^MT+{}uX8n~)+um0!zpA`J{)xJZU=&75ef!=$mgFzbXIla`q| z06aYTFHf${YjsVcjF>#vhj}rfg0TPqoLk= zuGx$s7V7G>@**{vV1kmM4E;|B|!%iyp)ajPLR z-CxgVI^4gNV&rqEJqNhf1N?r4T7M7$3p)GprO=xGyT{M&cGapaAj+1Z#QBHqT^J+K z#=tTk2;btcbWjo05XuEERHZq)uAo(6p>2ECzpA%K6@NQs)`-D4Y0>9OT(m>{kYh32 zcRg3+-yaOLqNTm=j6~v+$<6P7d#qO|{);Y*cq}kee;1IJbW*C${q_2fs^-iCf8HU# z4j^z4(GM!E263+Q;SEm~XF$I?qL?_w=W#+k+N+0jZGl%~w;E7j^@{(k+qm!6+2#7S z)d8y8?aR`^p&N`~zt5TK(oV(o!2mxgU<+@fh`)LiHY?e^zFsR&D<9a3gN1c)gT$?C z%6`m=A{JWfo&J6biDYc<=e$&R`!%qXwYjbx0Y>A81i(5ANo9urVhe-UOXU7;Yn6fh zhw^?gfr};fuY5IC4MwEg)7ps>|C4~fh|usJu-E9>8bv9_k@LU0$GPr*^&9}kc$?ey z>Y6u#@j=j@PfmXx*3~Gp_XjFRxYtko%G2uV{pwui3FusX_qR8{e+0mEo$LDb3ZZ_p zPLPDbl%|{4gj;vffPH&#d(>aPW{_k9k~5jwrTzz3T~7khWdwc=rT;_v<&A4Nz>K@J zw#8Nt$FC)7C}&V;p;z(5#(COGGHC###70^*7y(JsazFgKL5cp~n;!%#ukSV6(_sHd zdNlHW{{20dxW&ZAhJG&$!y^yMSb(#YAHSZs@M|ZAg!z5=b8|W{#L7^qHwFhxnXYfJ zUi_ZP`t<`C5tKn8Y`fX&f@JvR{g2KqRBQJ8|2f4uW?e>@z}hC>12{i0QI9xH#0mB& z|3A0d48w$?cW^aqQuW<-MYD}9{c4i|$``>vwXEswczUpFT2VzcO-*EEf9>9?6wf8Ot3O1KM;+%n|ytIsdnQ zj3|Y*1qb_OOVX*nQ%YK!7dd6tQp?xqvVOQ_eu6iJmpK*xS!r>7SZl3;b{Wici6MTY z!$=#*%V_YLM#-~vTRpIKegll^c86A92?Lx}ResQV&}$*%P#Uw zk8b5c0{59QIEixRhp=n;K3UKGEZ5;A*5}PVunPbzbK!2UxAOZ*`=Our2BJ4GIMubO zw-9UKl{wF}^j|bwk?;~30o4(w9oCUKK=PD5fV=#QvK^WUE~;Mh{dp!s z!Wnz=ehv?4BH@|es_wW>tu@*9Fw3S(#XHv915PQ*|GD-`Sayb}souVCV>|!%tVIF; zbl)Ig0VMzk-#Jr^i>~1sa~Md)UT^%@)9OZ-JO)npIqBH4b;^KT-4$VCb#H(#{9oQ$C4(_Mp7<&d3FUr{yAQ)L!-?K9g{Y}6PY>F`}841vJ+0D%74C<7e-QJeYcvzY5 z@29L^sf_+}3~u4m+I(Qc0?S-t8u|O`k$KBFu`l3R2Q&YFI={o<7P!3cxL+qW;aJa# z>bYS6D}BFVr19@Tm7d-(w1#44oc7;z0_qd};L5?+(9EhZ^P4)htCs0sdXVbKHpNT- zi%MmT`_Ke3vSH2%B?~*-{+52U7%Z~7R^H`7j?MpS>-Ts-c^n|4?aN+KV*loMzZuc5 zyEu%IwWE*ftlq#E5=xAj=KtDf0D%=K)S8`oPE7E>egfxrJ@a6P=tbbiKgO~A`}**A z-{bQ^$tiRr$EWpRh1DIg z@!IPX1uT*`V{BDxt6^TZ)~t^Dx4^_mZ@J|DTMo zmH3!!1~|cgFuXO@e@E6Uu+`)XQ*~EwO?6=lhd&n(P>Vq*#bPkZ%GS<5dt%yroB2;t z9Ab!l52SJot^hn0jaAaEaf`KkLDheNabkH}OZ8VytZKmyMd|`gnRH-yzFu2>DD6oV8QFn!NZa5TcXT1TK zUf=S34P9GnRizz@A{(0e{gLe!{|wgiC(CvnVnAK}x$#0i_I$Gr$!%#wSa9vs;9VNF_G8-nDKo{Q_B zcj$v1uVi+G#E>&W&X(l=J=V2rjoSv?;(fau?{0YGu*HV8{I&l8p3|w5hg&pT+w%V8 zt$^Zb%(!)nXM!eR!6&O9Idd~6=D9;r{Qr`0McLWKkUlMhwAQ8o+UjBs`cp5CrEEC6C6g@gH~jY( zV**|)aCW`FJjmu*LTzcwj(6vpLe%C9ug*&aT6?jz97UB1SjM|a75)CUb!3)GamqA z=eG`j3Z1e)ghO#5p9W{RLo2TpP?X0&sJy6GFBkf}cN2&y?%K`pDC(l2hZs>wC)jF8 z8o3o7IUi2ieRn=~^y(GaeU*Fq+K=}e_(_g|atx>uSE$G|{7J@lT|+~|1iDFpc;F?rQoi!qp~atG=9cf~PMdbT);&4fvW3VE0Q{krqXd>IF_azbYPHxM4-umh3@-^f{>EUm>=0q4MNk%l7m4s$-SvnMFoxYy;k8b~mYuU$& z2HK?2c;b3UpAf?y$ytPZJ7T|nb^$T{!#F{Hw8GaE7L-YE ze@pt|MXn!;;-MWNF;ZgKVav{ML9Hx23tyli+hGo80VF)DHnPw4CGg}wBG9P>c18@l zB8Y180H7{q0Hok6$TVRlB1R7}Jz)2ld?2<>UFir8RPVB!1-`9&6e6k&c+ScIVqsim zxiScv;pZj;gwd#uMAHsJzMJ@!gKt2@ZnD?nD@nFNw96;Giw}CfgV2>ru+W-|AEQBF z76&81;WKmLBZPqX4q9#i8>>r%kB%m7%o z07eGJr+dfHF(5VcaES{DVvl`s--um#QniX{OO2fMeZ$!^587^2w1JcX^o6E<4&ww2 zi~+Q<`GKPKI7_YQWl1%&#D{u0M#Wx;ToCThtR7m!=`7$293l3X1Ru*s`~&EDdN^T; zzp{LJI{T>7d_B#AWbSr^`;ky9Je4eTv3l0*i{l}+^XXU2&_m!oX0Oo!JHu`~sY*z+ z+U|k(ugu62k8N#%%yA4OJ^5pv+`8`ji=T2QmE7OFbNJ}?`wVn7e^9)km72LY7^yr)z8Gz`&Y0-x~z5rb^!MpZ6 z_erZotfhX+FfuzT&$P&I-Sg=e2VeUK+){~deQ1kKryp@!E}Doj-(XQUx^Y53$9uEA zx}7r1C6G>)o_yu9GE}xQ3QB&AC9)Ws!Z*R+eL;0ChV{^d9C&qDjjt=yyj!W54mod&fY%oVETM52qL$I*_lz8<2|Q1 zYQ)Jmw>2T1LM&c91&g4tyZgNVd^7vXxRMjXK^Egx7pcM8dqt;@8hu0J7i6@*?Cwwc zo38=E>av!J@YX+EBlY4vReJ`-E_y2;I=ur*3dFW%++>9#$(V6~3>)h#|8$$I*8{ zyU5-|l0Y~)Xy=oMDaIl1(5UrDIH;b%2I`@;xFRI#MnG8NkPPZ)qYv@;XlA}+E>cB1 zbP2gCRLPtLVu>0{rU%y!#Up=+5D7sn9(%6&Jmh`n&hX8D0@))o5SUjt7xKG!`e3yc zIMJPba?x9&IZyLJrJuaG6mhgl6~-d;mT4Ofd14B2MOhf>?rP2m1gi@w+3GDf&)Z*R zwXIS=?4FQL?zFLS6ftKo+#Bi}G^QFf#Ws5|f`sLE<*$2@z#*QkZb{2hiJ&k*$o&85 z2T^j*1J0H>gY?r|FghGy!)@goV?a*E?iJPJ6Hgg_`QcL^_X=kRaN|HGo*}{~e{YWv zw4cg~j$G2ivTfT_P=qh%8-wQpHfxk#XW7I^DuT|7Rc2brJ9ZgKf3potU@JYuEDgXl zG#xnYjX;d$HPeb?aiFFI%l%y}Jlme1xSbM>+H@kT?Ck+&&6eXJGQE%i1h*u}kqKa{ zbd$?@5nhLxa=9%nlp}6f?PZF)Z2Opg85_V9&s}b`BynWX6Xb<2?Hah!Y2g#%nc8|7 zkF>ZMl3mOwQwK(dTEvkK=!KNUz#Mj2`P4apWOIS}>fv-o;m8|JNt$lApr|LZu7_@p zs*K+|y@G5sxX(GUWFzGGy08Gi$3wSs_EbW}{o0PRT>)AF)seeH1x@ zq$o0Q29wop_%`P>r1Dv(g;oJTN*xTOiR#^4t`llai%%hVg-Lo3Ae^;dn|CBRLf?$V z){vrLv$q029~sscMqxmZAu~3s7Qt!x@B6%6OZ_fkXWMV{{jMd&FfCY|02?6m%^Qbo ze?Nl_QtWR9?+?dEH-n8`@1m`c>M#rII?-0|u3y%fqu`XR=5DRN;_uGsEY&AoBGlk9 zYD{st-KTUvVbw8UcoBvbU+0gkI!(wZi z{+_(bPJgaehGDd`svtA;SzU(CwO#d#)`q`>d4N(bMXQ{Jp|jm#)QQq9=*1Z_u>k$} zPc?2$H3D*&FvE~`c7NHcm3M%9Mv-CLUhVVu(=R)SB3OzC@Xrw&`vDY&?@&Cp@dH~X z@SddeGv3$C^&q6>@1{1=T!>*SJ`S^6(<%FeETsQyv<}K3_`|`=C63{~-!Pd!(;mSO zAOv0Yw!MkN6#y$CVj;P4T)(EYt_}NxwK+BUTFGkVoxwXyzuk`;nrCS44JL9Y4!4-J zmNov-G0S!@x>EY)Z*APMK4j;?suMfN(5Y6>`{k15@kjl0)p_p|v6hPNq_8hemfM0-(+8hZzI=!Z_{=(iir&+%~~ z9(*9A)~mbm5M@8A&+-rfz+YUfqA;|_o2vAxTt`?*5f=g`Wt)UKq$ta@eAKI`m(e?sSOGoxhAKNLsNoU*Bc=)(438X}4+H&Qk_PcYS! z^S$j51@XNSMG5EMehb|vXsSG7Q&wR}3+KDlQD^rx>p*Z>)?L%P@_P~CQn|;q^PNlE z%cru8e|C|jhFy7Qxkis^jX zWIURFSB305PME_L_-J{6(yrM_j>EyUW36(;aKj;3XrR|{|Jj$f`T9hV4pP|2!V$hf z*%a|~%B)#>ig=oa@o)r}_mz?G)>Qk%LJV9(`rC<=c|tc_8&oLeugUljmKJU36FA!6;AjOsMz&13G*>Da+Q z`GQ(A*B01zF{(6O)a1A)!U0Cgkn|bI?HBP$)}YO3fn+YsNWKhp`eSC=LN}D!gjV*9 z#hacG(Jo#{DeDjJ_LPr<^^N-AqhefMnG1;ixC*@F#E0ZG)c2hI?);f?@xD0- zC;gly(o24&iBTx5FLo)+c|0OdWqKIaw>;@N`(qLJQc#*^MEz8JZKy!xUiw2Q_1c2Z zT_5kGtr?`UTbJBkj0B<7q{0t6HQzn8`(8*kEwkm(&LzuU%ZQdILyfJ# zhgHnJpMYyGnqLhFh93)~7>ge@!xLK}ONDc7`_j`F=QWB0r{VS#PoAY8gxV(jS;`Wv zsh32Up7)vyMoV^=;SGPlu1}Ho!qVfG!fsW6jKKmkX4XdlY~q7^eErkLOY)Sq9}kZ< zrHfxhB;_%52ZQ z<+}q;XBIlYs4YB86WFtK%SyvVjnv#p*pbqGFPrq_iIXp+aBkXnEbm|d=Dg1h4Ue%3 z`t#`#@yOyi55?&^r&*)WnOSKcQ7`Y-%5L%Rb6tCjX;}JulDRO9%JY*V?OkZo?OUXz zY~J6xWZ{M{HfPshqhVAQA(#4#rn3p`VlU`|i#-_Dbme)p=klW&yXDy#bGnu692WN` zov!OKmTdYnn{He;k~hBmx+}<#e*L@{?D<$Swk#-7FObWr@Xp+B2f}inbMGUKvLH(h z1GD1Y?u)bO55J}J?@H10lKau4N$K|<(fT=4Z8@uK_KpC{fqBul!bTBXVTBXkB3^V) zf=Z0e!((mAc=;;>O#&M+X&${-r^ekg3E$>SGakOPkqz*TG5DlMWv~=w?y4l+0eufXJ zJ=!z7exj(UGcR4<*ZK2z^~QgxzQcw~Ht}`aV@za7FR*7}yeQIHL`@`ClMgP64^ke^ zOlHl2oP4XtXL$@&k?DaMa*1=Pgdv0aOdO||T-mcF9V+=@c4v%-kAFA8Ts{6!-7?t} zN6H&mB)SX~-j3mg>tsmdgM7|so$fqR(`4wT-G7CoXF7c63xyh9(JF5~4_j_*n56nG zW5j;E_Z=Dexnv_8IDsO2ydMfu`#CdYFgqXhO#{6n#eB(Ck@|x_wqCrOYyh1&N1PX} zaUE~YnDL3^JB{`1dM4dW=o?1(W{bpnP|xG%&v%Q9)q2`w(60?qXR_5N3R;wFV083X zJ87?}(WN;<8gjA*)P5-<5ZL!lOyHp|IlL3pbKTD{T^%Eyjn6h!%)hCb7b$k)r3@#k z6r+^sDRe<)`hIqKnpp$}rXMh&vlSX%jGQ$}yX0lQbc#Tz5znST&-0O z{aErmKnZ*@MK1anVW#w1yniC^)(ib9>d?tzc3-&fl=u6|n{FJ0m}>fnnhFM)x#Ml= z{G0(6tw$guw!bL{x0e&`pyta8SmDGZ?Q5&KOO(&|L0bDw*WF1kO_r6P6J;TMWd>&})jaXkt%aHH=sVCl8q4JPhe(xosh4H-2C7U7xX9RtXyOx#J;WwF;D|BbtbJv)Yhe#?3om8H zUPEql#KsIGvJ*f?PE)_;jV7zs`Go`SO-X_iuhDGbPb~gj+$2nk8mh~19#5(bR{dG% zEQOuAtT^9A{+dUkpS4{uGchsu5f+nYfPo*9a@GyR6NVUqB|uPH5!jFV8#wWP{2@9J zz^||+@8wrYv7?16S3rc*Te#)(7@7VeOSY4CL0%pG0XmN1JfZ!KdLJ&Q5}uM23Gb7n zs0Pv`m-w?jViR5bo*Ie@g~nad3+>kqf7tdqxuj#}QPG22%O}ZM;=>9OO`)N8O%1!M zqD=d#R6(j&PXG{CijO44sCDfToBMX`pAv^jes4fyHa)-i__1sq&m$c;u^)cd%qNzf_gY`M&{ZOE z<-W`E3k1@Fw=|Dz(@eWimyp%!XpFchs(WQDympk z6+Z7fy~I14CYm#P)%mdJO2(TwdZ zHj+my3Sh7YnpZ?4I}&}q&OkK5Pk#+C`8l990oyCQsulN0lWY2K;1uc))I(Y;v! z`y{4J91nAwQ&&(Z$C!0-q)7PdIeL-qe6mf91-W2fauH@-M||htHX{a+#l?+siqVT{ zk%R$n1y@#j@dWF z03JI+qd0vBz?;f7qjZeHQ$gP!Nn-|fYUw?jPH!#d3r_!VS=fo(`Y+U4%X_+Ay6G84 z)QrS(zDB*G>t2z@q?syI*uFLh{zz-bM>6!RV3aavND(tJ#dluy!Ti~2*WRMfm*ppE7v2_6O!HmGiFZ4F^K?qH-{Cb`>^=3m z!xOQ)=llfGo(D+FEa#=fKyJ^{W(9gJ!fT@7^#XKZ-1nN7(-EVFIL1XDw~3$PslVIn z{ORXjbvyKnVbr*eWA%&tFP6J7tP=v~pVLboN{TmwbY9-+8f49YF95V6Tc^m@U zHbi>WYlGf%kuThu;nY$90+kSs{;R^KU1$}b_nH~RZWQ%Rr48#k>s{a zyL$E;gv|jj%Cwv$G~$NV0ZvkWZM@Qu=}*rTbD>t?!`;ZUeyjK=o~hREI!bRcqgln9 zw^1r$%yG3`0?3wa%g=RRJJKqOOd?&gPMi8n+0^RnszELNj9P52$IOtF9ohQxn?)Mc zrLeMALxBLN>#BGIAH`lwqgR-jC|2vD+utZm1#Y;m{sbr~O-m(*I0dv6xN#U<5a|xl zQ|Y*=rIfepdS!J^7tAeB_VVyZm&e=O_B&>Zs2Cm6UIp~RPBYr;yBAz6hnLZ*goq0Y z?>+v#=u(z}t;y*X{(fXSIKhCL{v=Oq4maQ8_&B;@qIlI7U~1O2r*GkP`;Crd*ntJD zzB}QeJkDgtZ9~#u$e}r|dW4WiJQe&3HiusyCJ#pWpI$h}yWX8bf{>Cj<@EEXelylujjRqdd zrj6Fgnk%&^hiQ_p1~hMJeE?oDgT9e!g_7W5t}P>V*`)l;F{#=SiKAxWg)|S9=WQYx zXngVujvq)nVs@N)W`uZ=VhH&i84JZ;xCj6&ipGp;XSlF_{*c2c*80Pc z>@Ks&J(+LT8jwgYnsB8mN~P3p=ggS?a#j8Vn?Tz$!;r#vOVs82wO_Mdw#Kr0J;e`m z*6h7?X)60UXDPCGw$t-yb-zrS=TXgL8VK5_#PhQg=O|0N35ilhmzK=ReRgv|=3k>( zNz*ECMa#bAWgUDmk(NB+aBHvYEa{&e24NFOZ*P3R=P}~>h(1qPDO>Txfb!fs!d?VR>d6mB$kd|=s87eD%KA7w^E2|?@EM=jqjh$t&EL4TfkARc6C zb~oZEdd{_%*D{3{F>~6i5uZm%Z_I0~F;67Lq?7hCo#8xv#<4t--%}I%ZXp$@(nC|Q zq4YMy9u8lkZX7uH{V8}Q@%smrFW7RPE*Dfhp0+JuW}B3&fk#u4OxiZDB-c^$`PYIL z$J284W2eQ`E+|Bc`y%w9Lx*73>>f~Kh4(5XNK;i))A{-bPAcSYH`%A~X@t@>D2!IF z;b{05?b;9Ti_w>Y8LP?~qJ#R83QGFR`qI{ZG_c{1&HwZ4s3xpX+rptF9 zfEi?6pD}b)m^JPver_)Ml&h(Cpc1HWc-h(Xg(VfI?r`j}tvG|ovpga1Vl@t<=FXpc z$)_-Z$DGiPrm(9a3k9QE`l1Kys@%@EQd8`R?@r%OlY6o}AD~$%mD?R5qI1lQz0lF) zkh8X6dT!*<)Mfi>@k{s4?oBr^3r|`;XjNutB?%WoM(k5)mKf&hc5@ig7(sbMvB9 zFjpyxacqdko2|McBuNHg%EQNVfU#=`?NzDj&G|i6{LAS)WwZ$<7KPVGd*u+7JA|%~ zG$9@0$Y5GpFq0*~M~|pYhj;WY$&sp4pi2=)_2cE+Y{o-^$Pq`8i+Q>@A&g0StRBJ) z8S%<=_&BQYDEh2-P2|`h+I2>chvs{<$=4JL|KHVmQ21TD@Bc$=+Ua^WA$IyxI zL8#^_0GD!E!`w#%s5x9h7U{o41xaZR-_g-M3y)%AjxE17qdtm(x0sLT-6)ZsAugLp z5zh7~JG`i!to1Iw@vIhJC%s5n7)`y6{-MTmWLhk|Get7&z)n(bnyM0Gb>i>_0TJZd zK0)-{bQD~P*RWP!o{(?@6PtVBD=XWuOXfowL<;qaKAj8EFzX(mrPfB-2Es?-nZbR^ z8USoeyP0_Kxd#7?j>>=uui8pgR+I$DFlc;Q!WI#*6==%i9J6V?Jf+@L9FnytgOIdfzzzGWN5n zqujiA_wrMyMu=;((`5y;Wo(GQC8y<0Er7PtBxPCZHyta4g!ju`4U;`8je5l!Bs=ML z!E>}v)n`0-*~W>4z=W zJ@U?p5Rh#(LAlo;TI|Xp9e|@D^S(LeDHCl`G%+<;mm<9}*wai8V3m#M(M{qPiSpGc zKV117o5k^Qt|4N0v?ZrowRmd^J9AQO3R}F#h^qIxRRZ7FfCB{5!{R(ZMz?dB8- z-BAN!U(;_2SmIgZT@Wl(J&)iDsZ{lxd9?ew8DW$(9dU!#WK&I(aHq0*PM*Y4i%TPH z>=LqAEZo61rgbL?qG7z4l5P@zy!pmBDDFJ$8oLDcdY%;Ne1B#=^^;!AOgQ!oANjTY zZ1hts#nrp=oDT$f9D;IouK2_TP0NaBO$oUN5(@orV5d(PT1%|g%ckzP4RYV^^r zbx7)MK9Z~P+?2HzpTni%Q&N@MFQ;>D(XChb;2-t)hmMT|^=*LTvZXs#YkzSnC>Bk!2qKLp=FmptEaT062oplcA)g-Fk zea%0usId>6I0`&=DPb2SM3N-OUhMA5M*$;=_wKNSeF-NNh?V65t^j{+I#)Y(Dc+r1 zWC8>3tMtut?Y&j4TiwLK@grnude_`R_mLBe>2Xd5Uw?__z&TlF{*UfjXs_*c|EVZ$ zOl)}W>5I#~MF0kf!%{h>TUKri?=!zt)`(&cC<*FH)ri*0W4H-Csu-BxZ zJcr@&aQomGF%iAOgZYjN7u)jSB1i3lp!wK{<4<^YZl&GSGn&c7@P_eupJZ`7+^|LD zR>7UJ!_+I@`q0o9WQcwkkHop6Oo(tZ$*Wu8II2}tdN27023VezK0Xk!&?Y;#g z>6Dh)8vwCDg&ds6;&@L6dvjR?!=u(pD3Gmx_nn~kC{crUJn8UO?~4VZD5zd=29d_? z>B2SC&u+sgE%1p#BkX#GvDh=7rBUN>oZT@87{s$_u29Q=(IZ&iA6X$egH}}$E{#hg%v^(qh$4kN%~=61I~)+ z@mYz1OWmukZ7}#a#LXPfMD)WCp*yi7Q z)a_PqhD^Y?yKcO)u?fLSAYMTM6pj+(ZNn(Abfu~pci@8{4Q$>-gi9C#RcjDHHj1b` z+cOn%D~;+M$GAU>9iE zdYU<>BJW?VxYtq>(nBKwE}TbR^Vc+NMMR9u3A~H1k{)hFw%<_hmop603<-39A}>!z zgBZFy)Agh?J=x(Ss1cdUaj$OQ{QSsA;RKrNV8*=@d+7Qp1tzY_)KCs?g)~J|I!0M| zpmO{4V9Ie^oVmm($?S?Jd#^HaC{VsVMG6}dXBSI;Lis>xfNhWQ9U&ykI*&cS)rgPw zEf%n+DO|tZdKi^*$i()GX!up5eGW+@XU(AaS?iF(t72G}pHTxbOL$6zrM-O`nu}~v z*esMjIA8LO4w)3KI-x%8`Wj08#-0-r)XJiOZkMji7%0OWw8kuL`Z@STZ%enDh+aJJ z@YqE(WS`r&kzCQhbB<6a_EGvJqZzr@JvhSqygkHkkjf4fT)0!wf&551^%FT*ua(Ae z|9FW1@w#WlRf&RIVl78!^!P}K`b@poe57FNCj98mU>{IXYNnhb;7vHD-PFo5b_X>? z_PnYOQyKsGsb}fHmf(F-+ceDSK8w+DDxU7L=}q%8l@DOohxhd7Hk*(Tl-={rBk0eMu+E3~jC+F$SQSK!uJGh3}$iK#$ zpw&e|0BYSr8g>ElM%f|4w_A*4@bQ#c7@WV^3hmy02~{rZaPu?|kfEmTV8*|BszR}f zqT$HA%DT9W=V*g*B~+r$v}+8p&(*vi9*d3WwA3QBKXwg?Yn3IsSOJ*Q%c#W}Jfy}& zTTuForcztOmL-7iG0#`iU=z>orWujQW|iNmLR~ae{Dp`8yaX~tkX^tEZwS?}^zpNLxT&skc zA!{jN8o*7xM^nMBec&t<(@dI1)FL{bz`mMEx@7wCaQ?8opwO>KwnR-jZG$b-+G7m* z`*YfDb~V>4XjBP3Gxtg)K3ug0>=>2+v6qZeUncJDRvMOksB% zZrW;96ZARj3NBHZ)-SgYrynL6l+#X|ELi5reTjB2UTSK$;9AO~bDTM)N8yjzJ*CYNOhR6!nOlQX|Vf|YVQ~Q;>T$D(K*Xl81%tz}U{2n?iikZ2`{@++&>5$6=C%Sp7;-59A@jXU z5gW2wd^&0q>3|$IOQY8-!*n@Z?rSNvpv($ozW35Uey-Zs#jwC$&z*#KNB~wjOehmoD+qbn6oPipkJPyko5E_AD3-XyUvu1Spfu9HrU|x` zr!z;%@5>csL+O_*- z$1+(Fj?P2BE?S&dUlf~0(pu=d6sS!OKR<*QrtEteBGS2^v?;BMZCRTGmsUhNzIEEU zPkG`N1ZmlA!@a51-?d+M9}owXEnh*G05eerh5<EnGARf$~3c(W_PSI{g=3!y2TE3==ac-mojS59J7q z6ZR+z*%I&b5jtaC z1u%}P7yw>)%`}^$Fs^+%D$aLif?534)enoJd zs6?;zWz{eT(IrFt^l#3C4Tfb+?C;O}?lt4bD_IAeh)>k`#+n`ui!}~O5pM~V4aoZX zIOa&#)5Ck?^zZH#Pcnn@x{oiPG0K|Ze{^ob{MoQ}vF+(G=?}rK@1q}slEUqVria3c z4>0?_!hisJXHwkVEmzAX4JZ14a$4>RPtj%HT1<1-FPNdThXt0fe&fdoP7-j3kbw({ zq(GyPq7i;C#UzXgN%(>+{iGdT=OC6*0Vj;#Q{~}nUR<+y%E>Q2soyL6DYvwL1`>8I zpQOA#>MMOm@_KYfnCw1s=S!2^_PaepasHHKA79!)iFxifYw!3F692~!p()%u9o`fN z=cvgR$C-P4{%#}t=Hm#IL$}yX;vxUbgWs3a>5A_clYL)a-6ER~1i@=OC%R1)_9*y= z?Q4B(x7Tu)hwN4}^R2WcasSrZx4tu_GL)|ffl#Ep8U5P%G2ef^qp0J_Dj&tuMFt`& zHDE(-*IAelI&h(kD$9zh(O$RjSL$B4$m)S2A`SFW1ni}m)~)!8(2A4%)PEP~?MhsV z??&FzeWiA6+jU$*N^Zxd+ynrNGhSnnbn~=zMAYUS6+Qos*VI?qTIdsq`1gEV*Ck7L zh`W}FxN2^xKf3v@{@I62v6`FPw{YlF(DRfZaFP7K`bSbGl48tAA-Qvx^RmOv%6VXEk!9ns67(qg^kT@&S}ax$P}T>EoFa@tB$+HZGs z8E5B22JI~`j2z?ANgwZp-|qRDSe}2{r@L!wd0Rq4b!FPz0{fHf8V_9InC$$ki=qqB z^Ay$wHfvNXEG?QEfLIQS!N!;Mo?kw|rVLNLD5@A{=fOb>T^bZSHmcla`SVoW-9-iR z0G$h-5pcTy_8=lW9TJf~y;`{R8}H+bI{Ut7{d^%I zC9kj?hMf0mAblD6j5uXcQhKwyuFx`wJoV+G48bAJL;H#4sR_NisRmz)x+V_DS)b=U zGS+sboEv70tilyewF*kfITag+$c_Xo<|5>BmC;s`yQ4o*!$*t7%V}O33w}h_rh#q-gG_H5)``r|)CF|#d zPWPq+TEZ4`F_>5H8@o3dbCGj-nz+mFBT9JtqqAiuex{Ur?z}q{vea*Uit-?(leewY zvev}$nk)e;)N3rdr<}J(Hx&U7FPOEbU7h%@vuO+4RZtP{kq}XeGp!CTl-r;+I zV8V1f5qh^?l5Sdb??o6?c)V(~|86W|lgmxQtDEEq$Q98&XJNUS4rWtDbz`#~1~cfQ zpCNKM4!G0MXHwQYP3=yr>D{1tmc^Nv5vRr^vXpwFdgwtb_yFfgsZ2M5lkEWdL!t|v za+1=OwIDYKf+!-h_eOo;H~3Z~a;F0e6`HXOoX>irqeLl`-gBJ3jC1(bnRi{9RWTRy z_U@R(fXkp2ThLVB#cfVes8PA^dT({D2?UMQ@{?0N2O*-9Rf+7q-jyRN<@e5XVL1GJ zj>(%jD*fBIHAC1|WIUp62cO%v*dp`ezSjm-w{&J@et+Fhl^X3aFwqFoUEZ!Fu$8}e zLTJ<_RN1OIFW@P*b&$W1Rd30xM>ZwQtfSH2ck=MUhePVG*a@37G1@g17d0RYvfIoS!CL@a&p>Y^l;?CnQ-|2uS zhmm%Bkj@>R(C714W=60~Z|mP(B{a$YM71Di&gKMvf8VCNZI7^9XAenaV2D?O3PeWL z6stcQKqq}nmulZt9J-vXhHScJiRavYxoGkEK-a8@_cT|E-H&1Q*`WdyPMNH&0y4ew zhw|UK5!roTsXw=ljE#en_}$9&vm*n4Bz0JBv1{5)*j_96^3+5;&&i*$)zM0KKjzq* zyw{S>yNL3(d2;IRSn`OETk@7&+z9}D3vZ=MDM_{Vl@RmlNaN?&J}YMM#^cOB7|+3j zC+xo0yC=+^fj3npBQlOGTWg*9(yobAo{ON3d`-`#s|E1Bt(C@fu^+<$ywmz3x~_vU zMzv2Qc5J)ixHh+v`+F(sKt=RIo@`|7@`I1drI&Bm)z-2eq?U%8^6k`(AIy38HEd2$ zk3~1i`OQGLlGey~tF4Ay&?T{I&RlSq;{cGkN$V8@sfMpMk*N`*E<>Y_k^~6aPDXQC z*x}c~QcZO{DOzK1vz>o_J1EbG|)XTMH|qbX0u3yC&g@zLF$!A35s zvNyhZ)EA9ho51W)vi-V%kA~?y4=uW$_3y5rYFU0r8D^^3G4yEmY_zZ!&1;Bw^is^+ zy}{D04t4wUU?8W<4}h+Zz3whHaKa35I4%3e3ykL%fO7eFH zqXR(m*D&z;Hwcj_8G{5BX<8tpSRs3M73;^i<*$!uqS9R9BIVxQ?vWx>lRS_yi|zHq zQ=~3UKIHK5?a%}8w^4@W#h_T~sj(~r6$*qr?|a<$jb|shmhby;HjlPjY3eA7_Vt+X zsA7-cjeA2C`=chLZD)sDzUQAC{`7_4K3`?TuDUyl-G)qA9`3hxBJZLQx09ENA$sDm?d=aK)q)dTXqQ#sAQu((wg4dh79}u z0v0`<(fbcER@%o*CI7BL+4DdNDLQ_(h2cP-)>=A+*%3! z^25W{b^~rOw@yL-$818&sqL8sY1c<`D}wjikb_wI>_)E8yN;PFEht)6I-Qtkv{{yM zuWQ3ox~ePj1L+ir7$wITqv%jEl=<7}Zf}+UkFDRw+ z5J3S^q97o>Ne#W1kcbF~D7{zdO7ES}oAh3V&;tY#dPpF-+242ex#ynm{#{R=tY@wD zzVDoKj4|g3Etu9Rx9B;}oirLaDup6O+>U?!8p$e#M!d3UB~ae<17lcn0%OVX#Uop{ zJsssHC8%cewYWxa8`aUwx($hi$@o!9+a#EdDHm>){HO*E3%yAv@IbkEYQ7yVCNpO( zZrXmSA9*Uj`xx2c0_nUqI3A+JP^3di44eesonYTavHCtq8f0!dxS&VJ?l#(|gHkGU z=qGGnTY8fpJ!Vm0J7`JyX|z``adrY(2zo^9T*0D=ntGG&AKQtt$Pr68POo(7`D;?g z)x%i|WKDhk8%ErPUj>!olo_8JVG8E2k1_Lqy8M058NIyI7JYOszDn-x zE7VGUUj05IZc%oPy1T~u^wwwCp1c&-E~;I-ExX@ti#v|y*PbG#6i0$EbEWmm?F@&U z($kvk++L@fae@a@gNge4>X~b@R&}#GrrX8r)5YIU6vDvPq?N}&xg{_OG;#eD00Jx#zW;8 zQ>q#WE35nfi)-Ur3JudX@~!MlMSP_WKVLnQT0#51>V>@+`4W*Ea+Y!h=DNfRDN->? z%An3YB;7V`G966zT6VpYi0=q#EI4U?sl;$w0q z`zvCTdc&nDIb`%!UfwN?Hypd&I7E7JiWU_r49nhq<+UN^b9L9^kzSvw=aHFbMJDsr zdoWh7O{edLO9+K1oE!(S$@GlgT33Jo{W1Od;tJYV}1n&wu3v+guRFWI>;rE`a*<~+&wcTcIlr% z1L4_5|r@LR0#=qlN zjgUmxv;P^&!v79-NnuYz`?!s-m$bj%H(M+i5CRRIFb)QhCIxrp%cuyvj_=Ny$E}PX zEkQtTlrQ{1^r1A%u2e>4p0-C4p8L@or* zYM6ET>hpXXATMbjQawR1gUCnshaZZ_V7ia>KEV?%a=w44bN&YfrNuI%QvAL(oae&J zZ=%SV4^%NWcJSK~4K0(d*26O`x2R3~v%);@9iyd_fP0geAZ5N8`CDPU9Pn@LqNFEQ`0KZ2i3&wIr{?6$<6AqAmqgxx5sn9R zYGdEncS=g9f9%9&aUxEA3_cw2u?wkI{O)wwX@lmMsq!9fRTtlr&Tr>V!wha$FBG8~ zkKZ(raeL&bxx!)zq_~GWXX;payS6{4@zv6{Iumy`ozmqiGJS-yO^by^%HZb}@9h!~ z(sCwsm~t+cLg0roo^Q(7@V`U{#?lPaOU>jS?7T{RVjbx;vM?#OlobJtGEq7hA2JIY zE6}rN_IUg8?yeNUfG$6yWGXUi-Daa~HFfOpgHhjXTpXdn1mX1z-rJix^fwy!z?Jk+ z!b=6NycsIF>nZb5C(DifN}|)Kf&0@j`L-YijptoeYrQ1S*`eRqmy0K7PtyQ4EszSH`l`W!M8yF=F zCZ|78ozE!N9<=#Y5(R3W_~Vph0}DEl1OQ9orVA!cc~iShZRlbY0}TO}YSYpRe0XuNRaC{*fn_ zdu4Ck1)9p4ynasu!T)T@%S%P!gg)VyT%Dpuah?90%G*7qqqc_f zvto*$e_@7TpnJDCk)|!1)5mHm803~F`NrlwO4*>^t@^|PksxQ8CM$0sU}xKJKSLq= zcuW@P;aGwmz21_r=?j8tDV#0qmV8J#bnz#0KIyYNp?4Vu1fG5T`nh`Byt1+~Z>F%G zc(`Ffr%b2XAv?zqXveVn#(4r~Kk?<5!`^8#Pd+Hk%1l&uA5=REuPTihYXb=8AJ3_d z=i~-$^X{kV@=Tn5IYvCC`cTaeQVY8eyV8ylC!BYA;DtumS<}W@QsA!#XQ3svgXvo( z%W6YI=?We4Xkz7{D%ids&~i{#7<42Kz!r|FMs5UbcWD`vUBV$I6%BCdsnHi+vp?eOpuSR(&WplmqC*SG^bC#eTC6ijS38s>K4xe{j(E&1Fc51QERXJ`a4;Z-bt+6 zcNmKA$nD`;cZWKXQ^;z&$(?A}Xg(VEiSD+f&x370G zg-vlW`1!%XA-jtAO}EabKEPTgEYJ~37thnB`rb0NMf;zFCKWz*;)DYt+&AscO5QUI zY%9nl+4NgPUrcCD2iZQL2VRkD<_S%Nhmi!e88QCjlTXRPxYysBs1@FUM zubqq%MX1~Oqs^JhkPSl|ig4}=*UP4))Tpk;_-{A!&&q8A^6iVt4WLx;(n(#~ZlFrKBPnGSc~i1!k-^0ipDq%#@g9DjTz2 zx$v5b*-V@C)5khiDd!+ecEiIi)-X~SRRv_k5c zuLOL`g8@p!7VendSIy$4LLq21mInn6FPYri(|@{WAlsOFP^XDx?=$>wymj<#w}`0-jZ`A1C(N+(?L-&-2I81giZ zX*dS!+>r4;?5r*24VLN-I{GHmk>X{mLUJJ-bo^?p41bHqp&ez6--CsJ2se zR4>zxmKz?mTH_poBHE=r4KZgoCL0XID9(4{%NH&Gh;}WgN=Y7+a$UU6&~`#PziVt?0(H=}7sr+aoM>U0 zg?Xt|yl;6`l@{t4dV*TGw{BMzcstc-%jNgh0X;H)vX5&m2nWT+jQmglj9{Xmj(lOX zf>cmfAO1P(u|g^n#dt}^<$U89m!~+{nf?3TlyiS_dWx4q3aq4iHgQ)(Nx}}#*V3$T z|2Ny?54WHnXhJxu($VQO9A}|N71tV#hv?-thjUB(1zkXbv~raSKJ5WgUmjiJaGUuV zBnShrS6l$e1v8&r<3NFDZM_9k51C$fSjet2AqRaVapM)pO*dF1_oOhIEF z*qn|*NT!>R-qLSjo<*=csq9{)iu86#Ibnexu9v7ZY{D&5;raUO3VTm+#-n3Pw@kSi zhQGbLgiwoid18k_tY4K_owNr}jOyD#2^SWE$tMAg;XD;>;y0rnR7PBfXN`!AMi_uURYAQ zS>sT@{Mft?t}xMXJO`$sqduHn&dN@|L;ktZ=%K}HZK{%d0@XE5R&?RXthryR(3&Jt zxnfE2b&nvWs8|$tH&xB!W||p*t4edbxnE^a;n^UqBRTZbllQC)JhuhqQ8weZgGKIN zPq6MYG(t)VBxvX-oPO%U4b!-QhO=4Iaw9;eH|D+>Rw@>BC}w-9o45;J+=yp{O*4R* z^xZd_zHy3Uf;w8f>>duooJ?&S2IzzS3S3l+rA~FS*`Yc9+9f=qy)%Liy`0N;ZMze{^5n?kJ14?+ETkVV90)L+SR-Qiqm_@?XYsh%at57RLb(ppoChylI zDdm-M@)X?$yRU_Q{NuG^Lp=czrQK_1;`b5FDl$%dp?_XWkSCT^zv&!n=73H~1!!iGXVyN9qKu?DE4vckgEC6}Jx*hyDmi>e)X*!_)H93>Q@g?z1EsR)|O5tNN)-OFDd_0C}?3f0RRr@978i!I%-WpNtkkzT=YHJ5(R>e+EX z?a@BXMqpf-KYOBL->~*qr_MudtV_#$3MR$r?BV9P3-oo<(Ilc|&81~2V<|Q43(o78 z+;h-p;98PDHUOc^CBtY?V(drt*+%TnOYUXq277iR+_7t08A@TmL<&K_7O=*<6;|^Y zWNRe;;Rt4@f%MLPU*BgG&uVa*f;~H{bla<8>r}mcOZ#AI>Wy0lZc)w-dy_y--2SrF zc-GDAP&Di(2n&srh8HcS3wYuqI#_4?!yc8GfbQ3ROA)HQ?_Sm*hbs zGs10GhLx5fM_jMTM}y!}K2;7aEiLbn-owZbcjQaWvh{^j3}kFIi2gYDA8nLZwz|dAUV>bW7FY=e6NZI${jVkRRN~ARK)M!Enn2_$U zjc(QDfd+VsQyJ!yl3c1QBG#v@ts4=yyhvx zZIsC1a@NQFZly|dhunwc>wTFUCI>im*1%=<0YCRBWp2`%rAmWP$~np&A_Tq z%2SK^{V@9I4?)J`vd87rIev*}K?}ic*}acQRa6A#!CJ5!%Hc%AMt|IJ&iwJr(mD~d zbzcu=-h-P)#`Eu>M??Q`+y|t)&rSDD6U|b~iwex~^7Yock?T)LMM0;?S@Zo~z^-J0 ziQyE1{T0M72;PqB zFPF{t7(Pbg6}G4I)AH)G-1JRMyCEruKQ-n_y*0H>BAx3 z^0&j4aWB4kradAZt%~E4Y=Pi{L|+DGmu}Ga-;(?HXu=etHu#l0q<;U=DNrF(xo`WJ z+kM;{uX5w_BW`F-53aF(AAX@tMn5pp#)42)(A&TM$HhDIXpUdu#X=Q@^!c8s9#{5U>db1>sW1} zFY#Dr#cWVCL8shy05#KgtbFu)u@c8*)&bcje=L9Bj)Ifys;yRk-&}I^+Ph~oS_4f< zS+p~J=WcJgpO?FW%eK?kKgh0OHUEaK{0*Scanyu8N^`FKa?hjSp+4c8!7NG{ImP<1 z3^Ft0C*x}dC@Hc-FhG9fQ-StuygkA%^;!muOwk2l3O-f~+GWQMf3u`~MeTDeC8Ab{Q2bKeBW@na|^5(PsYe=qS zW-3v?{a_c#wWy$}shKBxbZNZ57W(~K?-N{b%;wmy%*G(f`0_==QE}d2y?xw zkoMCosVR6!B}LrSPtZ9U&o#r>25_5c2>@?nUik*nnXt?4hSe<6fU%1-#Gz{pSL-6^ zGobqZ>9qb=t0ES0+O8vk(2ld{yq`)*kG~3G+96fXgr+7=tEC#fMsHVcS#6c^rLua_ zR*ha-`BQbNLgY7c|6gSD{{l(~u)EDcPLQmwATLPt{$jEsql${mko?2T$j_tI+KZ9g@%n2kuh8E8hh1rE4@sP!)KxO`Z zDQEWiqiQn!Za1!6=qEO9i7oaG>O~fmOU5U<&UYl9_J;`ai>rmi2_8B`gAfE0;$Cf< z`q{{O1-TNKv+EYD^xa89akt)eu0w29hSdrmkqM5aB9(~{-?|4gh2z~3IPL9{<+(Nf z+~f=>Jj1j5vyq5aj@wdua2}Pd%_DK-v?zoQU0{X(`Jkj5a%Nx31FtvekT{bH-={_0 z8>hP_^u4Z^$FJUR+F4h%iD-YKqr;S!M`HP&<`}D7wZ~U(nP!j`%IPt3 z`W$v6_~0#0MlA?LLR5=TU9(<;JsLJ_EVq*c7iTo6(+rO#_w7hqMNMz}RB^m!UsK%2 zA8@KIvZ&}S4W|o`Qd!!EQ_^$b@KhpDLE zs|A~gf}1W~lBr`n2`}^uAbX9xm=TJ!cGLhR);)hlBgWw2QRjMGDe7ol+h#00A5~ zJ=P+gc7|@4zDj`cbmkvR>&6)z$!%8Va+b{`zOeXR4Jn5B`s7nlsHIXDgi_nCe>s^Z zyx$U#`+krIi!WyjdJmmn-WXoaYAeh@{X|&$#MPN5glG3 zk4AM&e9ihtc!3Pk%heJmHGOGv3rim^TW;T}ENv6VcQ}pb!`dl+bxX*mi0SP)9VL)R zb7w|SJNNwrPy$`uL1aj~iCMY=Nb?xdeiS71%cgpVDM;_SW#^|4Ih-FV@Ld!A-(bkR zQupw!uuFoQ(c5-+(``zB6weuGP_aBJ{8)!RE&fUgQNRi%!BZa**KW2R4andMAil&8 zx8v)dd#z34%@`^g?HdCbIPa{@r^>?G{pxXM?WqbbJpF@YI^dFv`_I?ws-sK zLTQrXNUYX=*rDidFKZ2CO|7!yN0od0eZ-eeY;?OUE=|U6*j_Fz&ml1M7P^U2M7DK@^+*IkFU`Mec>oz1Cu(C6AmOp`dx>vA}iQ^qSdm?M#p;o&VY|5U;I4rBxiC$lyATH>@f+OnGIX} z*zqhWHb3v!_>aee?^0tHRp&{MBw8DRO;0@C2}KG&%P@jVdh^`;)Xrq4k| zC0XZhL7%io{&0Cn+$_gt{GW%`-Z}ca28SDsLB&e8VUX2EQ-tsWEC?tt|Ja7encq^? zWZ=Df0oAFafG;M@d%PR(Ke8Ux_{!j(;Irxy>1E^dt(b1&JfxMTNyEV~DY%K1pvOl* zhX?QeWi7%?_rlMQ&ad(qoF+5XEaBQBbbMKESg(lY7Cr^=%$Bcpz{!b)kG5+v?I&>wA(k1y zB^Y@L@ad0~crN;3ZDC9fphe;0pYG@EZkH%6>0#p7Hw>-XM_!J@FPcg@51SK1M_6k= z#l`)tW3rvRZdGn@48g(whJYyXmsYg?EpGZ3BP_#nK1pKF?$Fc{OH;+meDar?`bP$A z^wDCn!>Y^Ar5;lVh8;Q7OkUcx06(%kQw@ zg+ZWX0jR#NX+>F?I`VWG6cAdspksMgQE{BV!#z#l93O;o%r*xRD`7$BL()3Of6&zFHQ+SIAKhHiI|BoM z?P#fjb??Two1U8|xy*joK^B-~aZ01*-Zi1sG!H8k1()sZ%0!;C#yx6sDxED%@=Xfk zgu{>8xI^p$zFQj@q;!ohhXT7#yP(w$LHAYDPE-1GS)c5x8kH!PaWW=m$ij#r2>%4) zsK7U~Ch_ABbTsPnMkl|u=fw9N8J9Vo(kp5)DACR`P`Q-bZ>c<^vgWR7Nejyq@DUg? z2>*#(lV@*gR9y021F?86MRh6e#dkiE@z4shH+a_=RGG2for zo;YWYKo^75-;YFi4aR>e{#nu9bn+n&oSJ%PJv?t{K&K&^XnR(abq?rsjVUf(&Fj`e}DU)r!G*3PeNLxD!rVZThJ8Guk4`y;^#aZ<(> z!Yg>G#Dba#Cxq!$S#3g54{fa-h+_GY#%xrYBp3pQz4gB)RwlHP_mr5eAl`kk!*nCR* znCBmFvi9wgL#UOl-&uSv+p=)??_Y8ad`a9BI9r^KB~C_YmNei??36&6pbCy_zMX#T zn4E~}>R(ykIN-yH1L6FOf}^DI)CoO{*;Xg-U+AB16FA zeI>q%Pk=oq&CFJUXk({>v?5m}o#&eK~`OEm>eEWLlv zG)5sjg^GU`TwiWSN$j>t1Unk9d$q(EB6K0SVO$g^EwpgE=l@^2y4-xlVJ-ZkNX{!)` z6cyVA8KBqxEpY`>C9Yo79*gyV(E~7>2ajj) zP^NW!E!qx=_Hhnm*nx)!fo|*>qq(|*e15+J^;|B&H>=Y@_jtxHgOXOs;U7PS2|6>r zcjU1gq8I8u5#V7GLbo0}dmVhbT+0^$bu;y!8ZN#OM#1pJPNjzeHlrD<_*>pS!z*9S z_6=ef4on$?G+3#+E%PFVFM>cA=1KggAtgQhuoBq1YrUjH>~hl2Q2-13H<%Y_MC6*F zCpOEsO&pcM%4whLc}(LhPf#f+)wD`-QhA_}Yhm};!RHl~ufx4W4}5+W1kw59xeRg7 zgZGu513u&Z3?LBB>-bKUnB10j{aNFmNlk8M#N;M#Qk92Pk}i)5swB*F&InY#l{w%O zYqaU%cO8M+(hhqNFN5^Pf)()RE!_ZZS1xSP-SD-VR0#O$FJ?bi^VH&=)7q8BXvQP{ z(OuETRv>r_q_U`f{4xSYOY}65r06nVGTm6-N5o0zGEDjLRVRVhzhatFCv&#^8Miyri|F0xNpn{|)-qf&{enc>%0ThP2zifc44rbA5e=$WPkCLX^JBId)*D>@66&cv*hNF zzLhH3hgUB`&crFTuIE|72cq_vjI8f$0rOV8k3`~HL=b#!7u!9Dkt@TT)b*>82ip=TlC?_g;IrD<7oO48$CL}ku;^qm#{MU9IsN`VME zhDOXKpRr&WFfnmyC5>Tm;nrl56P_ z4|??rK)f8wG>dr#yhZq%&y;H@3Gn^EcPk3*|J=wX2z4i<=nj>VxeO#e(>5Na8$5+k z2Q<3Ox|bZon|>Spouzro+{NSaxaGo$9-Osvxc>x`J!xNpvQ!fBeh|pdefd;|KRmPq zVNmLBdP)}E`$E#cv1xqHnbn2%HyV!pFU?LzZu~j?a5}YD*$>h)c~TD*1H+LF?&ej?GQU( z(Cich9qslDx9SBR)E^<&y0RsD>C&!-$A1Po9QZnzRP%l040X7wOU*+T($Q7eL{vRk zs6C0C;|FWwBU z_pEX0m~;;))WMcoMi1_5hiswv)-Onhm|@)v(SF%NCTU)+VrQQi02zk%^n!b7A&sEYfR~+NGlO5#dFfcOEHq`6t0q_Y{p|B0)`OT(Ox>;c+~goN&jSX|DL?cu zWlX5Vnv=uXcsIzK@zfc3WG#WP#+AxmKu#9xt$G-ooMUvyUuf7+!%=;UV;p{@Cp5>S zEMDw>^)lV%EA*N&-WA|vCei@T()Y*9Dp#MhW0j{ya&MLJ=Gn5EMG_!cGc zd#69HC3P$4=k6W+N`|I=(fmr|Z~xNO@tR4~oH%t`Ui_rg17~Q&58ZT|3BG((t9JP5 z;c+n859-uC%u8thF8z4{qg*$Xu(UU<#AziinQ0O&m&46xN; zXdT?kI@fbyrnhwJnI(GoGJq5<-yPSx#CoJ5xQC%h+bdOow*tI3n4Qi;Ia`n40f6Fs z8XRu65j-!?T6t~LCB|tOyZ~}|8ZS@k6F=jHWFbsJkob4JzU|`P*8W2(3BQ*$IM%Y? zA1pBt?qH+MS{5xI&;uTbp5Jq>99I9bVSq+h$<%><)Nn+otM-`AnBBz38vR za8DHTRSM`tM;A!QK2y3Vhzjy8`6d!(%TeuC|zk4(8t?r+bYcUaU>>fpS(#W=S*?;wKdgw*4mTM7Ji^hDe z0qECK+Uw?oN>-1C<(pcy2tcY%4XvOb1las$&^|Gk_nPgt!^cwkl+bpEYnS|E>dPi7>skj-Zd5*-+d`cq*UpxBgobrv6I0X7^$JddO z=0yXNz0OW(zs<=gjgfg@P5k`&rr_0_W1BjKmd3Wll7T;^&ErOOXAbZ z!*N5b_oR14Hqm!n?Qwz2J2UJuxngA%?2q`9q~8*B(vbugPEn z=q^6v-DW(zv(l1FZfncE5dzsf5cr_LJB5BLI)K(K53`>c3NRpQ&hy%G(F1%Lsn~WK zaPQQv1^yJYO;Eh$Rz!V-Fn-YNuWOFo3K+o|e&6)`_jIxUQk2g+(C*Rv+V;Tir~a=j zvWnBM!93|YTH!FNTun3B{wz8**#(uF_38v~0k!QMc3+n`Sa}G7^(Yi^c#u40FZ+x8 zG5v!D#EP$cqK1f}0NdX*^bAYV_f|%wC#lq`#(AHv^(5bpiw!s0#K1E(fY#!7ED$!L zrak?MGtdp+eIdO#IS8^s!+cK?|F(KBq3-cvUqjkPLVeTyrW`~U5|cm@ntXh0*dT^0 z5|Y?&9+2W~bLOPI==B?&zV8RxOo&#R^<9|J1tO$aPR2mOj7Pntm{+9ohaA$lTE)SS z(6eVOlnDA(HL0*F=UI&PwaG;AQR(-_3WrtcQks~p4lOB`lyv$=3WMqr;WLl2+033=~fo z&>fgGFGvqy?{-O1hwTxfXD){M!ml*>AETpV5v`Qb=XpZa3yI+O@BrPZ$0saqA=ll3 z6*XMUH>5Lx_k4ZZmss6i+73~iDI3h&{FV2i$+}z2a9dgYYca65`3vaT%WF{mC<8j& zA}((+LGI_G3XFbUfl?S4(4^6}EN9)>>a#a#<2gg{^En`CrDYeowkjc74*U0}UT^&Z z%)E5lKfJzgJ#5flHg8dtTHE-|^8Insw)o)1YV>?$rp>DuDYaY#pWxJx4nzsq`-M9Xs-&1(}i1{9D} z*St*C2NZZ+1cl!&G22CBf}otXo2=lO4d(be!reLWlEMzJ{90J4*{fl8B40!oBCos> z;%E0dHNLMYXEo6=#oXlvaKB;>vS;2vwA#4zqa;M4TW#4Y)4Q zw2{tx_Z~xz%AJ8eZy$s;6)wIirr5Rmu;aAl8>;0S(&kS()|%dWBI&Q*!#PmvoFMdW zY8;B8HOLc`b#=+Nfx~jIkX+cipnKYHj2%#ceq7NnSF-;vx}RKooj|AS zu+LBtll1%lqknY(`8C3dhsfNB!%o*$F}+Ie^d}Jx1Aky92ga@ST8}^0x#jlTZ4jm)h3p@h@WMx2&Hy}@ zed$TvJB-sF%1&f?L-hzR{5;f}JjGIW2%(KS%RaTgLBLxrOMmed zEXt%Ey^YiZg_{dVMbh6gg^`S<#CC z4N`wni`yvNt2&VwYaXmVCFK?DAdfknN*e?zqyjMntif4oGff6(NwccM3WGD~2; z<&~qV_FpZg3|V_p{{mua}%qg!@8sYuxwi?R(Pj4)29p{A_v-(b z)%-7cBGAtRK|5a)bmBM0-o@+C|DU_$zu8a<@OpP+-+%r9FT_Dj#WsyR9i1otE|mz! zU!U&S{ZEDdsG>hUa@4D=-^^)E== zPO8q_-kxG7M}{_;3z77WMkMMjs{?%ma15(c9_U%~`Rsol&XZ2*j$F7m9fSRR<=@xH z-?_uLr2V>R2oK`6QKt#&N|ZqnEE`%Cl9oKPWo7Gr99Q_BEK41 zC>Qeiu2WDQazq+qY1F{rZtcH2)=QW~UaK-2e{*M*Eh$^A~L-jD5&YZ{>}%!O(&|8?DMU0GR?7+*X4!81<@|unEr%+zv`*b zzrUiLl7EVa*#FyS@Gs%(eCSM7zoHY>TlOjNB{YAN0-97@bBo=8e}y=rP(rHPvM+SLN6N^6blG9BR*>;$mrHHR#T z&3(PYv%>b(?EP5vAGUz#jkdqjLDN6dz#lO{g;{|TX&N9 zcOM>}2pK57TClBMd<_IP#z+xgAbBe%j^fCj!ABD1VGSv;dNHG3FNgb&91c7zD;g7= zJRifb{h>age>YzklR}#1_Mh62Lfq$<|5+XXy$g|~OVPYs%4&j*yFUv-54Bb*mD%Q; z>a@W4n=+0)z%Z?seX=K+g^{yK+Ct$PL^-|Rp0wq5K@Tebq(56#sR;c;XXpD@+1B;V zDc_+94|fhnwP5X+w%1N#^FoQ~89NI~z-8P_ePts_x;Qo{GE;0D;=n{j0TtK#D z(So9!@6}*EujF75^=dOUiY{TLBhINNsKee*ysuT;EbMta@C#BDk^AcMNyUIzgD5Hc zFlxZ3D1pQ{DPIt!!RpZH{(IufmhClqz!l%O_Vh`3-vz!RpQccmoGA%4yD9H5E4wgx z;1!*hD_lkXKQp65J)?sMM%b816P+Jw3VlsenWmU+(_VX_e?-&m;6e6{T~`HW(_ZIv zeH4m2f&*?PqdBKe4bk8o+RxC05wfL?0B7N1`1HD5Rh4GN=#ttm`=itgF$?mQA&m|( z`*j>YLsw7jda62=P_J?)#PG2*k%qH8@(*iAI&ZEfPk42s%8Kq)i2=W4P-k*+{VCI{ zJonOTw$9LbpZaMl@6!vl5L=#2FR%Nmn-Td~Z5D9k!T9d&(dyOZn1hav{Ni>^mjwJvqe#q~!Vx#`YJ1qJ z9X|0|!|wRnIJEjpc~{f7>c#JXEjA6<`cvx;e}~iTR0-*Cp;Zztg#HB-GPsK9bE55Q zD_hA#kpMvF6aOyDWrWXDLy3&zoBc`@N3nlJBR7Hy7%>@vLv0{ zABWLyT-HJk;>KZPvJaU9d9JGvbR$cm3o2F|#{G=6#+{mM)%v2|fbfU0VAY)CJJTKn z^M}j`I>cL90H3O0q#)KO(3^u?7#sFz%RQ?O>!zz}*iScpVTL02@-(y__$%}#sS3RE zX)?W9`smE~@dBVhx+-72YaZuf$nP{(RL`00ru9p0=?hQj%&+Ytf89Na#*8Y?hb^=@ z$}=_UFC$-g_eb8wYYCYsCt4e19<-W`0Jf97^doL425#N)M9;bbLKQhvWGY~5P8XVd zFK9oQo;QZr>Bj9YiMhHLT~P~Eva|`(I#q&H2At;H-!eQ=H4+SFR`%XtUv)gcz!V|F_+;#)Dor<60`Lu$+7p zB~F!Ms6JBt4EK5!L8oUAlAGn-KX=cVzxVRV1U7C<5mo8bIgvD;u!LdoT^L?du6@{9 zq?s0Z_z~RG<_>&GJettZod_Z)!Sax ztlaE2M6K_l97ribSlo8rc!ZGX5b)hFl(U{AE0jOBYVVlN6Ek;QVUdEbn{9~#T^x^{ zH>$%TKGk7QAZ}9lK8tyX_K>`?w~8}EKcM^7i(1y*Y}d+T){m3l zbZW2GMTI%XS1sslimXJ&!slIjy-2g?3{HF=?HO;&R(C8?hrIgMw9|C?xWCN>9k>Y3 z&-g%2X;8_&I-j@h0ETN79&AQ)U_rZ$YfW6}>4mGBxH1~bai7(iS?($BS_z)PfgfjN zD7m>Ai)_PJ)6=D|BD)-g4F@s}A2}d#2hYfjZV86Mrv_s)tj5i;`z_o?dgA4G&o_!DF6F=F88zI;#*H0Z`cgPd% zyua@Xkw~r~N99egRVO*Vxx69!UC;xyx=aE5E@wczom2%I-ctX;Pl?w~pYAPmJ8Ant zk}CMOM;raI&Xo3tf3ScX9&HVO2PR{m(5Fe;wfkNR|B-3!U22HAgg2?XkgCXYH8Li8 zOyy}Ld~$5*gFv+!wDOHO5Fc$7*l$ZZ zKiu|y2@h0*RADMm$q)v~I$Dg`QUq`i?+7!ZMkW%W1cv)xvP5g%rAnr=LL^VuK7OXj zLz~bbl}DC&&(70J%>(jA6n)VxpVC9^iu}98KEuB;OWJ0JeD;*iL1Qc*fCx8&2Gr9c zodT8g4uWAT8>|ga?oTa+-&^%t);s#;>&P=mMVeZeGT#9qB35?T-ZLd3!aJbRhFod- zwNLBPQMKz}nQ%Uk5BwR(_c_ZI+f6yA3>-g$V3pLXXLN@@Tz8sj=wfwMnX~W@)Rfy|rpno1lcIYQ$)5 zLhZdrYj0Y!R#1EI*b$LDSMT3*zxQ*z_kSG85r-?+`Mu8b{4C#J*_vIklMcXGd@FIn zlKevREW_A;Nty-ZPdSRa68z03$0>5VUH3lc}U)0pa@s62MFbLG%yK z2dtD;JuRib1Yvh8fe3wW$aD&TR3)J)s~WEJ5Yh+i?1DETP=ls^4SFfu2Qhzznr(JA zNPvJd4Fh4kLM`!4@?F)-2)n!D9k&i?)jicq+5wkuj)s^W;qc1Jmry3;WA~d(n+ulf zOf`FPsnI9Q(xt$v(->1RhuH0eUMv=)1S4vO;Z+eTp2P9^R+L>1xFj)<9sURFBKI+k z9>$`z&frSIz%LbxVP{Q27eJ=4`dm#su_0nI^VAg5zk z22+R^seNa2xI%DKL@ImC} zc{StDY;}8ajL{`Q>=%WiB8DTgHDJ56MhtdscPmjsKs-HoChg&oiL$ysgq_Fd`xE$l zsBM0O{!5lCN0I(`7?PlxT=O6%S75VM)3d8Jy)X%5oU7?iFEh+(p>>MK`!{5rp*v;U zH3%~|A)XyH6mxJh3V+?Rh;zLS#ci@0h}j`r-=KyD8WG#Xsv<$L#P}*c6%Yw13Ff?I|#lJ zA3sys>@Ei&2Ju$}%4*eb`0m^s)7Yw+uZcvZ0^7wH@uJ-z&siI3sV@FX*BG-TJlf-S zVmOG*l#h?L=?IV>n`4%4b-M?FvF|>SaJ6l2rOeK8{aiK1*JMGuo@_=eeEjG!Pi1%4 z9|Ct9Q1k9`rbjp)K(4@1X%J4xjm!LF@?HKIX&y-zv@DCu5Vq+lcJ+Rs0Ye!BVK$A) z;1^XZ%dGQkl^EYDr0luL-C!=+j!W34IrBUwxL5NNY=kS^Sba5e%3l>R7#j`%#KyZY zj^VV3PQX%&qtqc=XK_gGGP|(KnLM;*>=j=bpRy0)PIT{~a<4Et1sYiJ+RMmh_4b16`fp4anQLAW>ISx-JMJU*s^$+?kFhUyx zXQ(ZKV9rvL&;k$egP=43W5N%zI(>&q7LCW=;dCs`8l;DO@f8sq=Ip?Vw9f%+A9`-at@M>PV3mZ?X@vdGuJd^CGz6 zlWf&)dd-cR{SOM}963q5@`VY;>B`=5D#+U(*7kN^BwwLxQRq`h-CQ)eIC zh9>VVUQ{{9cqx_eybt67ABev4Uf}9Es4WAGq~aK%9suauAqPOIc;Ftn=S7Pz&N*`g z%EBWSX`wuDQ<(QKXB3zlJgbF{$kur2FCEBX+nODYZM%>h`P?6JmCtb&KvvV-)g0yOhd?;10oW<-_I>tB63Q)fi{S=+^Y|_myI(fZj(Hh8X z4vq@>;b0K?CFJ&-gJjETN+vJqdGkS-5+@y!pg zKLo2$K1lQcQ{c`4JsjTYk+Eko_^jaf$tn-E$*qpSs2?FP;1Glf%Nzf^L8TDf*YpAu zg2idLwpfDE{r)_>Cb`aG@b$GCE;W7J+)egU8t9&UD4p;ZX^&~qHJP6%444Q;c;IeV z-NgX*2%yB(bj4CO`qV)w6YVacqaYfc;^GwE7C1O6Sw9= z;h&|w?&|h8r-$=5d@=(5wy&_;@NTX7v28J}Ct>yN-wPqKfqi(|E6(CBIx>{#{DUCD z{FTOk3l!VNt6h+cs+rP$@Be2z^nV{X+D}bdhlY6IE}bBksB^;rIaFnj8@{@4nzSg7 z`vTctf-Ne5Xv4u%OB7Ms@oS(@f!Ao;N;te!E;gY8l>TrsUgZ`Jz+eeaK5*d)L1a7| z|K3;sxtqGGM3rE$OLS6dirJucAKl3?Sd#JST}XXbi;~2Lt8^2}F0(M<$Kh_oKy?OL z3lz+Q@eMu)CGW5f@VQXpE+}(#F3uZB$BIa9MG!!M1t+lzy;;s+c9uPlrEYlvLKv4KD|H{xOIf8-03(X}I=4^I>N`>D7g~oQFIwe#c z57^Y(uEKQ)b3(E-o+Ybc(m$JCwxCv-bdcYnfpRY2sn~j$h(_U5nwvU&Y0R=d_h3vV zxB$#_j@BovfdY<-8Q!FD!=MW&M;d-t8g}@{OelcTXaw_fP70Zp+@N@+spWokkY>dk(^gxo>u;Su#k0c3129=aXykFFhb8kCfgaKB zA2jfcmF(a8<_pg!95tGPqnGt*pUFYdCgU=pfkIcAJ-PydkCSqkj5mWckdMJ9dpg3@ zvv}5ITnBd8Z#|9b%Ej<91DMXH3FheQEQ$}>g=%*D(hRN&)|#MBapH6{dCc41)C%8P zO@%J*r%7hvKVRq3K>`~Xez%--A(CMAC4{4YyXN!^E3*fHnnJE2H?=T7u2B1gDy+*$ zx@1WrskOHys+w-Y8otNYM^rM%I^w>oyM7GnuNk-z2qA>YvTT~8+={GFnlgKXPi4wX zUP6&13)xnwKGy6*pJ# z#?it*w0*3)lC<#DwZ_;Lp8ze+eOo9& zYp%}SyyCSBfj0y}iI-sP;=w##&CPTz?gvi{emsrgW;pusH2k7Ej(+@oEAJRi{*&9m zCjpnJwieLDt*ED8m&`B5zz08R;@aTOya$8Srd05n2%vc>nPaZng%~%RUpagJ{6#Sb$8Lzh9Afr&QQY9q@bN#+-Y*#83J2@@H z=8mm@dhr2=OocoFb7c>A2_5zdBuOLF7LN-wLf)e+%jysz)ZfF67E3kdXy8@5=cn+$ z-_CK;iGNd~#%e)z7P)>Xb6fy5u+s@8kKs~xgzY)$5be#Dvdv{!z%9JiN3APccC*zI z9N<4dOKrx^!rVcVY=BY;)UZ$c|fe-$a1A0*qnEn_MMXnsBuTC?EiO9ZfPJ zVy`KyZXnorE$PvXT3q@S4ty}k!}gKMKqB!XmZgp57TVBpHJ#%zz{us_!0gg{uRa3v zFM6ETUXvXggz~U__BG8X$tBj(N77%}xac=i3>jt|Fgg*$vid5 zR|>~B%faLH|Gb|j@c>-uXuuxkvf-%(&)CywA?f0!bWNIbu)PHZ#82g_X0>Q#ZUKUc zrdce6*mWt=paen|U6IY1n9vgw6MHMbeMy=9R;&~Ekc>S^DuI-IpJ(wg^O#9`I3iMq zZoKI^-`Y89zaLp)7cbc{*bxk-K?H8O2w{UEHNQ7owVDGxy49+RQ26C~g2V0sWhf|g z?kskZ!S@dsVT+c%wZRv3&)X8vmQe4~AtKW`d{qg`x9=b!GA08GvZYIsYUljZHEP4C z+TT#LI7yrxtLHRTUP8_TG+7;@-Pp6@Yi)6TNm8QTPq@#hZK~qcD*3Q!St}`BU>f>< zL)T}|HeP$rpNgIAVVk;XlH0yEaABRwwI^yW!(EA?0t9)&Z?Ut?40~TkT`w1wZ-AR1 zA90rHbkAex6PR>?N>~tofsWTKybn(YOfl3lUm48Lff0jL;cm9i18Bb|*L;L{_js3- zW>Km6{m|IrGX82F3S8)SWG`!RD4|!g0;^P(retZup!(SNg@EH!@3zpr`Rd^8C?d-Whe*0ZcQ*2&UG5%5&b>F9Xc`FnY= zt}|t{={zXkX}mmv6usf&BNi`b;3$IS^*i-@ecGT^UfE6%L3|FO%JB57Q%rW7R0~SZ z*Pp09^W8i4E$I06EN^W@)TUZk&}B0%;9H$gPojk8^K1={*D|ChVs?TSKII-_g*tf? zTJT&ALw9%eg@Du7&P{3eZaJLYbJ<$(ibI6DE{H<%mz!D@3;v3QU_MKcZ>;FyzR1Ni zjDqDl4ril}o00c7aYmI~nnR+PztQ8|DGcI;o5S_lpOYu&+UElyzS$34**cwIl)#Q> zgqNc*kBHtzCoJVBUTQy6>z#KsO06_MH+NS!!M&ejeb~li)LtAgls511+2mr}2=E&lJjQdKTM1?%u4q-Nz;@Y=a4mHjh)?_R1o*{-??9-ofraC3?5nr)ex=8nT zy#1TqNtZZ#c1DW;)Y9vY4dc6=gxj!NT4 zbJP;1jmMe#eA>zp-R^m<{Z0sPTyn;_WXc78eC5yl7;$Z<68D)J&u)+bD4JclkM`%; z_dT1u-&xafm<(P;7Abu4o$v@|Jgc|_)+M2t&jLaIn#VrNvd9iD7cHe5KB2EC_>e7> zufSME3uW0{V73drD_t7IKh{=z$yaN`TNr*3EOn3ugTv=VdBiWEw^(F0KW2v>HKhyD z7`1)b7*JznIWU3AHKRLmoOD=tKsJ^je+3x7VhAd1I)vvHX-sEd@h`E>^2PPvfyI6r zeVB93|22^Q2SZ!SKuL8Q{|o$e5V%05*JPTD(=>4A|D|{e877y zDFu5y<4;>;$p~Cb&DLn%!-E2a~t|Ch?U;@pn z(-!l;_#78~!W|{LE8uXjAR4wx@3~$vsC4kck{=&ingI=DuWD8_m`$TYu0mwvZa04a z{umJH-t@F+w4DYX`z&fNHvlS8tB@JSN3|ozPU$(TDV;GFlvTnzv$2g2nF@~aefxRy zItO>4Ie$|irJ0KL=X*4U9Z8~R8%AA#o$bLZ`We0P5g1&MhQ&fcLzdTIEU!(enmYcv zPgm9x8M_`%!ejz7;k=?j3FeA4rv+-lt*hg5V?3RS6&LhP)Yqc?#KJVCI<<{A;?|3& zn!-hAf83+;6qB6Fj2BD&i$bI8TRLGAvk%<3g?B@wFMbjkQ9+Cf#!hT1tBIPzbj_mX z2h|LQB)Q&}=HW79*O&ihMsg{=1Ho6H#zX<5@ZUkR7FSghKnBiQ-VA?kxu5Kb@FODu zC1H?WBhiyyn#A%vrp=h|de2TTTjsCnc}u`nCr`kJt>W#7nca9b;vUKh``kAQ~ zYdz8LYg^{_T(dw&6t^{@4ft}7mG{H*FVWrT^6NooRpvk3fVf+=;mu!{cAg(MQZB7V}gCMZnIv}mW$O&%BT@ZtdTSj z;IZHk>2u;PG_)GY_r8NNHJ9`Sy!Fa`L2P1gowv8%ImYW~Gxvwc}k zSo?mv-aUq>fL-6%cs)|zvM^fj4e3pmsUDs#u?=2-H)gfG)SwtgWu|B!x-IQ-OEaEz+Q4gWl;gvMOCA{9& z)9^sv&BU_OHoP3}A;jO;$mn+%Wjp0wQi@UuWkHMh^;lW8hK!Rgg2wC89on83SOLRJ ze14ncK}lnf-2@&D%>5r2%2w}ni~U;Rfe)rII)h*@IjS1Y7!riR{E|hbzZ+P`>MU=w zRRv%m_}p}aZ92$vdIcX@)#R_}x0(UH_Mvo#$=pogN=?Xp$JN$1)>!dHU05!5^&@Pd zLyTqJV1du0XqQ>8qIKaW{OBcR&O8x!2@KVtfs4#>y`5c^IhAzs2SK4@X;iXjSzzHe zs}|4)hzR<#3We2M8mqL%gY@AFN(2JCS2GipMKfoJA;Dx$p(2ecv1rCpAOwd!GSH|^=GV8-D4=A5%{1i^jVSrM2Y_q=c z=9uk~L+?KQMtPs~?qB8sPF01{uAHRk?u%W20+4tTx;ruwgku*_c^JT()= zpGEhb>bb>!$+9 zy@md}@O)5myr`vDBH#X(s(0i) zkS`zgSsM14kl;w2_saWOVJ_8f9gLO`8x}FW+}-Jv?epL?rs2?U&(BW{snD=?cqA}? zkeHvHbL3%a=Gez+KtbsrPy56k{_>rCtsL`;?HFe1 zX&VmTRnV*VsuaT&^Y|ykz&CyNYWdY~aE#vmsNxQCRcsko331`(m5%1W4 zHxt+Un`1IRYb?e3FV2Wtvol#rg%67?ix9YGn-V75yAsc*{jASbXD|GRK41v#1~i2% znuSKUvDxChRCZUW!IHQYY4;RDGRrbx1Y}9zxK-K7p8I$VF4%AQFmF>}@wHVZTPgEU z_Iw&E(Z*{#ZQ?4-@dwMsFgkvW+{X0x03}0BcS%25j%f84utnK?u$?v9nJo6KB~03_ z#D-RSR4dKo!&6aTx#Ty&t<0?ti#*%SU$0vGCK}E6PTkE8lX?x<_YWm`!-mhJ*<*T{ z*$|pp(a-t&gue<~T7%(wl{=@7-F(Zo&-&N@kQ9X6?RpY@>ZP}QLi?mw?tk$OKkZ?@ zMr#j1s(^qXJV~4^I~+^XXg#xU$pLQtC4f+p&fvDA3ys>@jXa~9r_Lvh;^y~}-P1y2eXQ?v-Jv)b^yJI(9QY9O2e|U_!nR{28 zd~33}U>kj2X1nd%KmX_kpVRfDNr(Q?8YgCcMe|lH7mU!ZKn&j=WS=yPW&#XVRbw4xq z8RYv7&C8#uG86K)6B-HquSC6E&EM=L)Ez(2UdI~d^e4*z(Gq)~0kayc2qnM0PFqO^ zS%$h}`unU*rwp5lK7L3HcKC|F1yJ85<}Y+?g5d|sdZb6eQnbB3*73xHwRF?gPr)cj zjGC88_(k&$_9oc6{JZ?TDZLS>Pu%iY<08D@?_GQx7ho0>K7Y%G#HR^3CEM>GbMPR- z_IE}PHvEb=ijuSP66))M6o%Kv$oE1(&f_(P_iI<>S}{XX(S67DGdkmO6U}M&?D`IO zipi%(-DZ{x8S~y;HpPGo!OvV)o&2YCurji*{i$B_Gzz$zcPa}?0Rxj~7+GRYILC=_ zDYTi9OLvnrOE=Y5!Msshef9LSCRA0GvEW<&&mnn58+VZr#x zqf}eyB5^@gOCQ-OxgI_ETg}r zcpxr$<7CAc?=Tu2vDN5c+!zuNI9*{=?EH<6S#Ve{dZx@YNH9*eK78cAcL4{A$IgBg1^L^L3P(A@46dctK znv$4uiS7txU6?6b87MGayY1IDws$=SSNTN5y{JIH+Nf-!8O_eG>2;lnpM4MMMC9Tbbg_HRN9Z{eefeiWC~11oXyNMe!{1`B{EhFTm2P zrHsDTHso3U%-5|gG#E(NSPZ0c_y|5}lwu$VpnlVmBX*D)NW z22|qUT`ajwkg%?O79Kodx-)78BYuSO##?%=rlAjdP~~FB(=vxmRL3?)Q@snCGToyp z%t(cWPK-M&&YS&9U!7|>4@a$(#oXil)6<#PtxSHeFote}Km6H-d zF^*nt{Xz0cCf#ri-t`T?n}3Qm zhvPI;1fW;N9R^hmT?T_11mhBO_^u%ts^L zjncW%}qUc-t_l!%b&OX zYe~)&V`l{ak-h6JGJ8#*N-D$P^XKA{FJ>CK>FDhkWW?a^q7*w_Lpm@|7-7kOoj@EXb z;j*ot4KlM_Lu%=oA)eCr!C2~v{*@6 zg^bU}%9K70Wl7U*tQ}vT6s?LGT|1hzANlA`|Kj!W<<^_R*mDZ+jK3{A|0@5%Tb_>l z%~Jhk%C^{ZYSF!~mdb~n!oZ}^L z7iac$_Hc1Qk4LUl%(!H(J}+ETPNWCJuVzgcaIq zzAMNC!>?afuH@cisk+3ZpOx;^uD7uxd$?1c|7v`4r^1ZV1l~`m_nc=&WdE$)zPh0M z$8^sbYw-P7(YN(#V<(sx%(H*>?~n3-wb%M+5)nMN(&$kX(w8DPspiD;9)vG=wq3*K zIZS@})`1i9v9)2p|Fmq!`)`^Axu>7DQSwOwew@PKWQj19JDD&pc{Jolm?86o$?Vi= z`DU~4pT|B|riAQ(R`Cj%+h?jrstZicm^ezxN2UO5jtbIjRTi(Y)0F%6M^BGX{T2AjRai3Ts)+Ojeq zhe#tGyOd-B6}6Jk0tozANg6X}iqi2&OcwkkTlUZHlE3N#uz}Yt;bcQ5{w6P-10Ue8 z-s?<*APFjRmD*6D_1Qrbd=XCv7Y0O6zUIvA&X-_wo!;R6j~8L z9{u5L5a>Z8;hU}Hyp$y>gvyYhSV~;7m`%*nB*(gY6SjX+%{2}dCrF^3Z_+N&6Pu}5 z`Re|1>be^hRcy~V z!=562X#I>Q(uUAi+?WCPIj*{Y+O8C3TinP?ru;6Df&GdKx__NI$#>#ij#OJZ8FT-#z8Nu%Ceq)`;zZ3X*>QVI(Z~X)2T9nVb@QWH zR|IED#O92HP;brAinVnk`lriATC|3AH{-tM$$5ik#VL;c1*`D6dO| zh1^PXcFy)R5UUJiyhOKm4{*O#s@pmHli=0+b732}dycsi5*93QQ*cG&k88Sii*M{8PQ9=f5A7@h4x=?zf#F+ z2Mj~IM$e<+s1S_TLZmGO$Pbehp0*G9gvA>rVs8w^imw8epM#(1*~Exkma}2SDvb{& z;k=Q7WJTo5cnvuQl4G8_HP|pj9iZ0m1>DQE-RI&xw*ldw_W7jZuqqa`#zCU z0+e()6>B1wwUWllnei=`!p-lcmdHO4-##))N&t#R%PpS^?kz|7 z{DgX12ag+Ny!><1Frv^@Of1iZMJrU|nFhBSt2wEp-gcrt`;_AzFs1*q+khTWX*U(u z^Me^m^d(Lq37*;QGvJ?Amv8$1eGYUO-$x5|3$tP+Y(_Uv9}+DRVF%x&ZalL6ki3WA zI@HeO__9GAikZAG-e@$Iqov`B1sd?!%1&ru7KgwK%AWP^r9g$#owXX*;)$eEDmq>M zW44E{Hbs3MkA8V|&m*jd@5Bn}_~k<+C&rYI;@Wsm`on>2jH=H|K&9>a`3u*(s1 zN{wpAaC`|zoxoQ!l81{yA_q5#7J4=s({o(pqguK|(GV-t{Wc!_L)A~$yX$#E=3sW6 z@nOgfg6~94S@+J9y_0_FN~xb8mQ-h)F9$sy<-6aR?32R6$E2M*wfrj0tE8?Gdow@M z9IjZ7fJU|I++*jz{`2UCfcv{F3MjPlH}r|YW_{lni}`DV-`B5S&nvB;*b&sD1hCM# zI6e1Xj<@_Glizl}y6`QGE+q4w_kEXXijStQrPY?G= z>k|va;PIU6xxR}pPjLW&`-TqS){JU+o=rc6Q=o=#;l4wi{ISGvO_PDE(vhgo%w9jh zQ-yQ0m%=4k;E1@gTZ{h%vF8wC9ihQ~(KL(p|1HP*|H4E_Ok5=+=}+fd#bN6zj(C3r zX`%J()Ac{$GP<{aGuA4SGm4Y6yp})x`Zm_eEs}y}cK*#JkTf>UV_4b8F!PI|E>7 zd2BTyWci+aL5*>NsvE^I87#x3Y`y2Fb^{LA1m?xSPcVBBggqsWI#$A%PL9~Uw9+q8 zbe0fKKGkQOB(3bK{xx;l6z-`xuN%?CtDZdZ%8#Wu6HhbF*3o&;E z!b03C7v+RY7A+|C-l}cmL_Xq2TZUk!qUOWGsCxpQI;7=V{<|s{oOi!xFR46j&7@T! zmc3ipNH|^9(kp)#cC<6{ta?UbhUBS!c07?TQjDbkNsf-s<+?XXyw*t zRB;ewbS;wahBYCQD&u=2G&?z8@nw8%D*+}YXdy)2^4lnvJcs`oCgdX!9%);*jL;I= zIlg&c1&=crut}_JC@EiOo@9S)bY$*yj4DXxcT@C<9N=C1We`VMypTIoM7~qlw?(IG zpfLT4AVkZj_DZa-Mlb(O;n$iC6!~acbLywNJB@qMNs?kr8;7$`ItRC~gf82t`?gt5 zVd3=!&vti;l7!)Yy?Tr`vJ(FO_l+Tkh2^?uQ9rns%Q&8VX=U4fW1_xRzS-IF6{X$C zby2^^23f)@?64;BVK_|k>%YVi1a*;#vjzUDn@+h?`ZD5e_ldSdj(KPJHda2-Zw=)1 z$^7gMkF#}hFQ@)*(~znx>e8o3pBEA^wl@VM)I*51fDj;RwT%;Uv?8Quzu(+(DT$-a z^E>;@8kH;!CAYQO_0sQd0{!pxXFI=j5d&(7%?Ebm@?{i?8*#B?%*^lr)h6xASn`!t zjNeQB{+q%W*k+tYM{w;QfW>_J$SF)Fsq{8&;`;I|4a`pVvoM7zSgN~O`7lLneN;f7 zxy#vIZzX^u^?~Mu+gHghQ;oqmMv|ZyE1K#|*^Lp>L4}CZPtNB_1UMf~e3vQZkiFU( zYI;VH@x=|_`$h#iGD(EyM_r+VlgWa+?OR*Fp47`ftvdRrVon>a3Wf^#|9clu zxNc9bBzy?ycTHVxEJiH9Lf&m9j7&+dk7tJYn)IIPzkQoY-1@_-&o$ad>HOYUd{EVd|d=U9nvLSqQVP&CxHip~ObDUMV9aLsJ z!beI=-$i{E6#@7;(%&2LkD-1|tpHWctraZ$`B>PI!FBPm65(T5O$X*7d3epeBAqI^ z@FW-l5JY~)N8_zlO3MhW+wza<}1YSex)sUjm*6g*GRPEpm|4!2oDe`y;ZDb+_lnPlv)ff)`a zg@RCmjDf3L&()1u`J4!Q*7X%xD)jdSgzZ(%MKu9=F`#qY?OV^4VrcXmf6M|-r}!mj z#ekUBxU^2F9qnvy6d&K_qoCHyE;pxKriw{UPTLrcE9%M8uUkMwh544vi08)j>pW+3 z4V@$27(I{r=pHVh&+BDuuqc?;GP#E71AF! zN0k@nQJPu{9>PLY=!jL;OCftGhH@^oFJ4ilIH=c|Nq|mdl03|Z>*Rbf%W0pC{Tpn9 zw@SM-n)@(B!1H~aFBRrhY|+%=u$HD(f1cs1saotZVz*|y{#1kBZw9UUj^tzA0zxNnUr%!i-bK%fQSE6S@;gty@+zW8?201lVn9}Zj{s; zWJl-aW$3%i6K#kpD*U&um(_~1*tl%lq9ZGYELCMvnM9T`%6r1vbI~5l6Z48Dxc){Ap68LF(7CLrRCV-d;{z*bNDZt39BXAt>(kbtJv(S~8lr(>N5M)AVo> zDDBS*g3u8(N7P0^Wbld~RiKW9#;{IwT6Dv6G$dZ|GQy-n%CP?Uqk3_Bl#;9$f~CLM zVD`b7ikav^^IPLo&8ogX3eSP8HK!48jtr=r+aRvge5pZ1>DkJ#RioqQ#uBn5a-N}! z^~j8j%YP*snpU-YfYcxzzPTvgU5<<99`swIazF3y3zUtlaxtP1`1S#L4>;)5bb z$-s&;^-sr@N(L;bIQ`8RZZ$N+cRAQP3M45=TmGU}4Pt6*!|BwH*P2jPex6b$aDb?b zzMINqZ9J(Y_&s@jdgWmGk6NBkT-Ff86%TI`bjmY%AM)cb>c=b zv-?Htemo}6%rZo&%O7=Rqv2^|$%50k{6w9+2)S#bIj@l89<;hUg+3X%*F9gFONPE> zdDYH*o?z@t&XHRVT7w~bcJ=D#Jq2{$f!5E3v>8@oWDin5VQy+IUZV?{9A|>(-$f9D zx9g9MPJX6i+TR_IEnjJHa(wuHRoUs>FD@CSD~LBtyc5JKh0z<{+n=sCtPsq36(DB| z31|EViFPOQ{S6fsPt<{X9RgnG`rg%o6ZED+4q+MpDV%S)+QIyfF zvd*r+$-_1CZgbtJ4@+wI+YybRe)Cr1=w`0(*%6a_{*w`7lsoxq zjq>*siJHQW>K@Mw+}jy)QNn0lMu%8pyF~AapOUYmB4hI+eO}&WtEOhqdO1_ypQ{Qp zTo#;Bji~Nv&TeKWGPUNdec3j|`BTDI4 zFL?nw{AzVm`Ue#a;s=v?(?`_dczHuASGeR~*w~xFwB)Dk?q6m5igz+PnBTl}N|Du_ z7GnJ6L}>O;;Ly2>`5ZmdTw)HQpLUlJFd`n=Gb z*OLxA7P?{IW`mjf2+U*L6}{b_``Aj+mimYLG13mlR7HYfM z;((PeVMZXVzi;dt;>S~kX|YFVU@1NBtXM&Z;R>svGPv=H=m{5z8fLwpU;ihgf-huH z7p~N!(^!8>Sh#381@BxY-g`#w1F{!Nfe`4*9$0qg+*M451&6^Af?~(mZoGWWhbve0 zcC^%U+j%EXj;#or4p#{sh#kffKba?2=EnMtY(~#}444z?^<(F>bX>x**jS!j7Fnj8_@1V@`+GWOuc^da8 zFDK-S5Dg!?7)C}$f?a=|=W&-G>@g?)Tj$!*Z8K9Q^Glp7-;uX(B=BrHGg$AdhcX)= zdBm^39&Ve~s<)AFLSJ*IH1qc|K@Tk(_zkNJoMB$WB%5qjiaXlc^P=VJ7tr$u!q}Q+ z$80;Llk+09CLJGZA|&71j~fW`OgD4Uee(P*#zG)yZ2sKY@V?=_V^=-(&R0)XTQ0E> zwlTCigqU--?FgNzk_~(cIx3}qfiBUUI(T__7DHfQ-d=uh$(AL7qtjuN!D2&&%qJUO zLbYY;Sde&Muhmk%N{?MenSm$?` z{Ob2mg-1mw@uIJ0+m2!M2hS(qHy>bs;{*KZ*9p+Nx__=UCbbwq%?!UV>2z+d=zs0H zNv>CMfKYAX4acAeCp3@Hgym(KjV`E$EXrEvX64m-gc=<-cLqE7wNlN~sy3D|QsK?8S^icWS5oQZasYa>np!$har?ZOWkm7%tH36vPcm3j(Xp4ZCXXH~46q2l*Pu=7%{?5sDA>MBdHE_LozYa%;Gqd$9Xg z=rctqvns{|gqGb8R1lN-7_dZ9L!(qSs3_TMU+!wZ+a2G3#No3zm$y05YcBa=QF#O^ ztlX2UrE!)6-woERkrdM6{{-v<|Ltw4VTblku@^lGy*1i!o#$>@pxvQbA+g7kW_qtn z701q&^$cBhEwGy071_&k^mkPw&4&K~9d zaH#^Q*SKV$#4YDFPF?{GKCFF`cP#g$>>}yFm?G|7p<8>I0#p6XEQA+pm7jHOOexBf z+hXXQw`P(k-%IG#PQ(Cm-H{<}oBa3XDnouJbZWLjFIEg~NRjSiF0eaE-J|p%;i^P0M z6jepQ$*P69u>D_2n^NPW-sGPXibrZBRvYj@l0uT5@iO>1SQ^dlm7|ItIJe5jBdJPr zN??;cVuL1k!aLyp#j~Nf-+<)6L-w*bc%Wx;CyFw|i60&7+sXvOghT41Tbq|pN_FSM zOKa3Lj?6vgv{tM=$Hfx!?K{oF$&WF<#9fSEfhDs*MeC@$+FupZ@1#GPRv}r2XXYpJ zFE`~L0@uva*=HqX7>Nz`xFLOnr|>MH?;T8;#c;eIugiepmC4!@L{W}9C6|g;y=l!n zBE6f>ZrO*W%e#a}Bp1Kl9e{g2RL7UnJ%7OwT{Z5V-yTPmvb)f^p-!$I+Vgrw+Ei*| zmWiVP%g(Q2a`|G1U(_;H%Z+h^Ki}`1UoG98?aR1dRP(s!^l>vDu#Ks6&&blKrGE01 z-N0V25U0tTZ0fv^pAL?baZAJyEty;#7U3_}Bh&=sXJ*)Sf z!w&q)*Q^GSAUlFr|F7G( z#jfqXKmGq~v;90Sen9;rT-LSTZsIy!eW_m=&6ksNan}^c1?-sweE{38ztcFDd3$?Y z)^&YkVvm$gX2Ir;kr*qKS;nR$pK&2`S&C4qb{K~_WMP9v(kY&W| ziPAFG%NFI6(-1lz8nRF2Tk-*hZcK%Jek6=gLcsEvWEpwJXOVZt#CG$UX<_(ar7Mv* zF8K$CmWXeNJewgs)6QWDzJl9nCh#y)mnCWU3HQ9L)KjyzDCUDSZpu`#T1?w6d3fZ6 zvglaABd2^t+Wz;AktxT@dv4bE!D5oLEK-}#tEKibm_! zYe6gMJ8r^@Jb&f(9wPqU^FvO2xujcAuXxk*ap5|%+>(2Ma28Z0t#PhD1_5EVkiyPVWz5{bJa3Nmb)&E1-dxkZ&ZGGQ+ z+fWgZB1KA65JZ}YbVx+nrba>O5TuAm?=1-eLZpVGfV2pRi1glrKp-GZq=nvlODG`; z0p9F$?(>{;?(2Q7=PRtNl{GWR9CM60#_vx{R-IJ5d!deMSMnIZIWC`jvGrTW$7|>3 z!mBHvqr|75A02N>?=2Z(a3T91)$K?(U7k{I*M{-qZvMWn`k%IdBV%KmL9@qJD=p;q z%m5$1DM3Ci1Z+2@uOJ~8>}P(1j~(xu{{-L*XI8SYvlL8VLE}0{!+Z*e*`+98S8=p2PDo=raOs!d$AyJ` zf+ENJ6SZFcQRXXexOGNDTc?9<2H-1s9b6|f5;`;bnKF7?iq>I*@Qw4|gSYrz9D;r> zMwWbMOm}t_HKuRD^^`jun|3t-2)N=qNwBINeED;I23;)3zMll+xO=Q^VmmoIHQ(OJB8w%`rxU&!UQ^ph;vN|H4C z5t)g_5YM4%iCJ{P@x1XR5K0*@MYm`4x-LKC{@7W-Ge@M~G`G>~lZla=RS+=4P51a@ zaW_1=u%cGWuBF!z1(wmrW$S}7MQfU{tyol~1AJ=h^bYsrg8y|8iXYR3U(c}41S+ix zGTc;nDD?_fu-4GH;S3-KT##LG^)k(jE`NB6A=p;>yxv4tWyKC08%_HdlP$2CROuQ( z#n?2oc+?G-+6;Qo;+#d{k?3UP2@-*Mg#H=VE-)FbLt8S@&pI3aFsBvF3p2uT4nM+F z*_#VoM){#ObP)=B%L=M=zT3NR-?sov!dCHjI?JkoIozf}yHD-+O>#$%!HQ7rQ8;Po z?E<~i2lz|SHpk*SQAFP3)_D+A+kY)jO@f9~#>+0>26Ibhzl+zbsV0T>9Ie(I>V&FLaTv)cgZlosuiE-6!u;L;sr`>JpCF6bs!i|~L z9hdF4rpkU!6E!~=;WmeFB^IVOj`JT!+#%?HZF2j2y`_f#-DDVi`*DER{|`6v(K#CF z@L5V4)A#MHYGL!NRQ~!WId7Sks`mcOWLl!N6J8&n3j}8PtqtCJwtCihzW&E8`VjSG zHPH(EE#Q`B=p7bb+?~O0y&*u3h8OYKmD9(ch2N8PsQ40 z(bFGb4{O5tYYH@;39XHHxLyOGV31iAqMo&R3y)U?CQ7Am{c+dkPj;RY$Cr(_ql4;E zn1U%8g|*o%tw_hU$8iGI69D&vNclMQ>&3L1Z(a=9QWI^)&Y#bt+R(gmk&3!= zbaCh*eci#4e#VlyLBAo%-Sta88^c=RPj2UKw8CYdWIuiC;!+Iiq?ZIKWdokS7n9MQ zVf}%R`RgZ=&nHI^2CLXiedL%Zo_e4jY-hbah$#G34Sv<`&2jaW8R7Rqyw^u zP*fp=0q-buJJP?cT}E!p@TS{LQ^3Y>eZ}~tn!2RUEqAQ0VRrPp*@(k>&6vS3xO2P1 z4%^`~A0C*XRS4lykP7=TsiWy(@~*kXV(^*@=yVA6oUSHH2|7!Q-Z7ak+x2h8eocNC zcVQQv=Op|K*8#zQY?cy--{C%7)A_)qUBYVWJ!gzx=ZCAd=6(U=fAuK*0Qi5XqWg3b z;5S>M?`v&w50^?ODI+uX1_}7oC+U4%YV6sK-+S*AoUXNSN)g?mZN?jd7!)jGtU>!c?gb#LoTV>5&{K>jftB}n*#ZEok5wqJ#bSG%gX2b zsZ^L<=>)++?6Gg1Q{Tb~|Jid>;4Zil{H#-`W&r3(9HsyYj>LnU9r{Zh=-`#hz`i9d*n;p}h zoj)15-Xjm4Ho(E6J8!;!cg{D+nW6ktz$T-Ts^kNGs6lPYz%1J@a_!V@-FM>@~c44wJ%^C-TB*YI2CNxlAi*_{{c~EM^YnC zJ_6?bOV;l8k}wS(0l>-LD_7N0B;KZ}p?Iql-;Iu>-dJ4?c|!xY%cFR)_Z5zaWo z?&cU{_!GXY!)d1H^B37CM-eK!H;WDnozW~zqda~TSCe@sjIF^-I&9Z|IK6W=QyWc* z;wDW5Je_6QdOAgqIP*rKO?>-0jZ*<%v(wFO!LVNC?s`&u$lJK4cXWOOry}^i-T@{t z{U;lg>X0>x^j8r&@1BQmhuRgv2%3%N*n!5K2 zIp;&Y7o40tY=S+fYY=yk;AKgbQJ)X793HRYz6O><#F3(bh8DOSCj7b-r!%wdL1@U_ z%jnH22uY9EbmG?5kkCESWCc?H0gW8(6AMxLAWpR4^~qat&1RN2Rd`~uPy>w?dL9g$kab<_*DANuk!l=Q&29nvo*Boj%1(fyMQ4HXVp(z1znzI;t?X}+C4^Dg=>MN8q{Lf?kASEC}%Qw}RC@0EI1+(;goWv1e64!3*ePJHd5Q^lr ziGes$e+&Z3Zuc}TE9gbt(Rsoh)cf%VKN3ueyT%4jglfntZ5cAj`^QM0T)U=p*__rf z7?{9BOAZE>F^O+duH#D~*%;b&sBa@unQC3GCIA@odQKgFcP)^UIw8c-8A-Pa8CA4J?+7>b)t=)pJg=q2~KSR)Eb0}W-d7qkWJ3n|S zoN=q_6{u|O52n`kV2Ur0GR41x!RT)U0v9LXCm_olb8E z1kJfj&cBi6nR4F)Q2#_M{Rbxew1oL}#=eL0VbT)~kW*>CtAYtwZ&JRHJ9w}fR<(B2 zUbXhM)y(^()$Bl|@XUrutl{L{R$Acox>?@wL?ksc*V1?WjKMI{Rlz_9AlF#W=faec zm*)rnau+bQ3x2)w$$9bo>UY8*{fPf6R;po<{|yu`;t9v!+m*A71w*CUpCWz$U7K>K zQFIc+QFQ2>8(_=q8%U_36@qR$sKiK=`b>upw>(z86arg#SWMm*GpodJ#AHzuMwO0r ziVMzMjKE}x>7m4M<^2X9q1ZQXnA?W4`ET~*WMT?Or259-l$m~bN#Zq_y>z8)R6 z8CCP!Y?KV(e5moD;@~F#p8s5@*647@_`so}jQ>m0#Nra}_Wnv$*Nh`JvikJL8{K5} zsz@t;%53MY?b_4I;<6cv8Zh86$$G0A7{5!ovKTxj#5q0+EE19Ecb+hu5NNXORhX1s z2p=4un3rwZWGxbL%M93B(+NFh`C~eg&|O!3v9f<@FfqIu>fU6D!cRzTOEMe!b_pz} zO@!vl@Aa3LG}XIOn+WF@xzqhI?}|sqs%A&JjW$8345sPSw(-xP$bOHYQc9JZZb0K- z8Kh4YX2*6Jyr+Hv-L>eX0Vl0SefVB9GauC{{I-EIzfX~&!;a9TOk ziQFwqPH75lF{zbPv`L{?UgJI1rqsjm-67uS|Tq z=K``}=(_FiCXIU=!UMORl+*o7>i546O6kB{_j;>52ZvX0;rxJBsA7$QS6QtM1NK?1 zB~T~PMAqzJ4Tx5=IQ5WCP*qsQUv=WQ!o*fE}^LL9aa! zQ&!3eps13#J{w=b8@Rz=R-kEJsi*wM2}L&Z@`1=U|1ZtY$j5*X;5pa@%4MqDowBig zb}YQ568>1CA?VjiT~&~Bu*ViL2Hfe|LJY@k8;fUr*}+W>t~&paJ?cKY>4n-0`R1}d z5HQ7UqlP#eu>*PFUb+>NiZrv>J~H}~KlharpS5rd9Vv)I6@Anifnr~GPYd~-CwixI zf44JoRmqR;kWy144(LZrd|C8!pY&mrtJfah6ltsKX1Bf2=;otUV)5K@*~j4nt@_+H zPb~_r!`+U4K{7`hbeR$JjCJ)&K77#@G}h>-!!$J6L-!rxG84V)2}Q8Q&a?aKs^M+P zY^*nU%tfQ)#l9kq6~Cl?lXpTsW3G(5>jG@p{jl(=1N1-$J-)15$@Une;Z0WOmO5w-PsT)JK=dHAq_AF=a0x}G|iz8dP zd#pr{ONo{7-+5C+Er{!Bq8V+44<)^^MKi&d1MYcizh^Apwb-Yr)!!UujAKxfI` z!kFYQg1ob53riUu##Xoe)V1Pd6gC6I3O0CLwV2tfQ@wbqaT5kpyh5O7SL?-4Ky(T9 zdwPzbPaji(74ntescy~%ciCq#%%}@h@Wdsb^mu(JPpM6uozLFu@jJ762ki|to$DWd z?d}am8>eIX>!cIZyHmu-qe61CjZK~I`(j!%={hkm1@qnv$=_hl;IdO5C!Htou!Va~ zJ#?xr^R1~cV6{_dEP-}V=46TDTaz1_O#I#7TRlNgNSn707JX;%#}REh!<0Ls_XDBPoEO?xGCrgpaZVkzQxo zp20U~uo_3BXE--%O4O8F&q?)6;dFb#akYfVx)4{&anMQIZ@m2DvsaLN3N2m(qKi9T zy`D$;6{F8$v?U8V14zzQzTqy!H=k z{DG7@*IWWTmu_@n;tS4wx%%@Eo9jj$n|o)CE$Qok<vMB)9l4Smcx33);dDPVd3GQvn<&WjLh|scr{yXKm(hURElit`rEn2)nP4 z5If_RS?ED!<>jW-=G}XeGD?|zX8g}U8aaZVIyusl)hzjm-zJjTAQR=Ma@S_E(>Oo8 z{%9dKEXW|c1|f5lQ#bQSFIb|udwt3!p@vtzTs&W}NV9$)cfH1q$k&~Wn@>{X)wx=$ zm*XRfB-8aE{CI>ZzIfs8Ihj9;+|w87-T*`ayo>&K)BCC)F*>bIqI0|y`G}g`?iXzZ zeE&K_42CTqzYLrpBXYcWN^QqBfabWuF)$E~tSZ!A0SRUXZF=BtB=5gYQxrFdV{8#sL0Cs)+ zs`LbRH@C%4S)-@IF@`(*%K=Vd60_H$MNHkhgT_RU%AZUJSt;^On6S(HOpwi&7 z0Y)aGslBZ)D}YR13NBX9*{QP}>ZIhZVp)=&1*2xKo|GSv&Fah^G`RU8!#w1b!cdX} zJzu4 zTgUa}p1f&BMowb#YY+e9lz9_q&?YZlIqhi`g@?Qyc(MWn2)2Dfr2n{6A-3*}BGOiN9PL zuxJPw$r55!^KZ}azrG6L37V5>(c`nzh>kA)TUPrjamEAJ3`$k)=k4Z#O)muc{nvH>`R{)?Ce#_tO-((z^NZt)Ozh%RX~qBL zJ8?%pMjri-1^usaofF~#3k1<~8M=|MtcIy5L9M z-yipQwq6cA2Wq5UxBH*h!P}VYR+?7Te>2cS_?Xmm&<7JHd&{yek9A6+e!AHrr>`EN z4=I&4JjK)<_42(*RNP0|Ox|iYr7zVzE$mKiXPwU*mc7b}$B+_SgQ8n3bMfh@UCW%u znermP?@yPzy+@mH?BX`?<_AsPc3%K@*$;O&jAQbxzU%Usv0P1gNTBOCtRSXuvwLjQoQ z$+_{}2BsJo-`1X9K-Lxz;)`7l@S5^kKmU^HAVbkr9F@>0W`)Sh|EHZek0SnK_x3+V zm96o4b@n;`El2BZq{2#j$O`C`LGsK)0!c=o6*Nx5^uObpG?gWM1DEJ+@?lwupQp2~ zR{~B`Q=5B2Q_B}~g&bFilD3Ew+X)$_Lbc$(NAD7HYNG|!tEM(H>uL-?HF_+}HY z$uZjwKX%4%Yz3~(gp9;067tWF7c}(rE%xV4D6vN)PsZ*Ix_3J*MhrB+(q`UH12?-r zw@yz~s+)UZND^S_d?wZaJIpkUcD?EW;bL3(F7fGleG}VE$?59G(B-oUM<1oTt$TgJ zvF04X6-5pea>mgfv!}fgks0Yao~y^kNPOBOGhcX-rH86m=ZJ^awc_yNrhuzx{c~?d zhR<1c?9@FPoo=qcoQ`_3MSyHs%y0iGm{2pGtorpgr%sUz+J);8=;}@LUgx*JdGnK~ z$~yctY37^}phzE|3ec2@YQM^Drr^yaE!xD8Sz$dE1HeWfxnS92Z?&B66mVMAm>qX6 zj$U;lzs{+QPH6G-QHxG$-k&JoB*$p>qys;>bt}})z*t(brvfa87FE9GyBkZY(FtKb zNGVi2Ik~7%xqsz%GMuG|d$k1RzusPmx?t}0LXCo90xoP)Tu3tbqV<5-*aY3?bEqI08ae6G?gVR^mTpZHGT)ot7haz9S#Zb?%INkxY?JZLSa;OQ zz^Ozgft4i1j;G~X%}k+PX%nzZtV26MTdc)KnJ*2@;dKa{J9VF2wVkGL9a=ptm!6IF z*5?<8NAhSGbmQ%)rX+tpXtDd4YdKM-;P?{U=NIBj+&y5 zbAJBT(P+v&E^s(M3?Ix>;=h)~)t}ac_1&q@J>DPLEjU^o#5GSTA8+oaM+I+7*gCdq z4BRBEXXwnb>xiF37Yxur;9d^YudS7xDW`9K)j$r`l}e|q8HwStx-eJr!7fI6wZ;4X zN{{NY{Hvc{dbB9&@e845)tklHN)lrATQq>A(=~C4fd@9f3|d{AvoRH&0ekXWqb#d~ zUK4&l_s!(9M6@N}bX4eUNM)?nX)ohSRhH{rE%$qk=^gxvMH|b>9s?Uz{5zI>Lp519 z$kz&r2T~`M@4T4=y0!h>|GN70ISTBjEb2S4$Q8Ewe4sE$S;{x`qr<2Iq%43A{b*(T ztS$9yZSm4TLl$iCWoaZiD`20C(`fA(_;}TScyPC5pgZeh(x|+|&wZ219Q35-oWs3y z&4r-;%g)5b=cAh&|LF++=|%DTJb!kFIZkdpGFzw;lZkee@{MzpI*M^jpH^IR3pUZ6 z^xRB6#S3*D?6n3{wwi4zqvn`{?V~u;MgT{pFVOR`7xKO7 zAfU2u;&t3A>6_Lck#99mJ$r+OOH&8{w)>GGp(E!~)qc#oMNUX)y*1-h#DBpZzaQ(I z0mFl~n&&c;yh(tL8L3eNjJGZ(scg4~|#l*U^^Ty!LZz@E~Uxw)0 zX#Fk_%n=XRjL>=zDXCSO`1$7DN`ab&D-yQ;TAp_yBjk^Mx+4LtyB!Q=+5JDyi)dAI zKP_)ZXypNh$P zHl26nYbyj+zIH7(A2t*y^hjC-ZKl|6rzr|1HAF?%1~*$f36&=8uQ66Wo_0l1^OFVQisj33^CTZ7Q0ly)1 zS4Kapc*(YBBDz1&8N)m6=}}~99%m^l%mguXSJKh}F+1@l@hGD69zd4&<)-)QO@got z&sfjmUhI8!j{f9j(TGK_nFREV7#?N8(t->CTq#cdY*s<0^VR|5jsA&q_Gcx|9=L@{ z&DHjA?w@Zx?avop&Gi}}UmgRuH74wPe31xx z;BjJgD$R?8>?JeMvv4$v%aDGDI}yY4b@wO+mw;pY^VQI$cKfpjhh}g>AsyUzo0}hf z&9T6yzjH@@A$7@r&ZTtnqB&tY+D5&eFD~k){qFOvn)I(z`$j){vMfT-m1X_KUH~7k z%WiHbp56R7^IKEd6G^J^?!eB_kYA+Z&bU^ea^r%(^)gpi^Nez^8d6SRqwwAfuJ9C(tI9jDX`q#N-VTqFzo#66JV zJBv!41tNH2)$M|XTIaT)U&#j7c&ifn+kJ&HZ#Cw^3La#@vG?F%AU$m09&AA z0Qx47dna&hZoCXW==eUsD&y?Kd%ZdX;mdz%Ac0Sh zHEP|SA8Sx7Wz}KlRc32T_rv7)yht@OFoREScI|}RjMiQA8DSgO~?3S5bE3N>}o}XeQek%8tK^y&9tz0uC)ftJLl%By~4R zUPS*iW`Rj*hj?j3HVasv_#QkEHi^#tq%1PTVS1*XdBy zdTHxDEp*B4#zyj5tH%b4HuR2&B9WYcDt+mGAkQB4t zTb_)@tRZ0AD4Q!vK5r6a*>P<{20iro`5iILMItCUdAp)z!*WyG+0GZxMuC}WThDfK zm~CRy%H1927=2jXqAaE<^XXAvC-JZ7Nm+Yj)s+*o3uO}9nqQDxra>I90F$+yF7toH z)kbsZzJ&EnNFPqQ&lGt9?Grv{fPFGjjRwIg2D1T(T4lh?{y`((F7w%)NgMt8WRUaY zl8ff9tV1>2&@0wyn<1g0n=19}!;;rp1=12E+Y~>b(CxWBCv$_6jTX@wW5u4+1Diuz znS5N;oC&Ep7p?_I(~?h}YkcuzeK6FEL_*j*Xmb`Hej!zM$1Dm$SQwNLrAov$aSXSq z9NjGTpPMLtKR;2d{$TOTe03B11Gf8Fr|54-SKVOVyIkDuKb!4#hf`eYLAv#zYPnfY zr7BR~=Vv=E`%EI?oazh^pw>+UXysk%-ZHb~6Pw{FuNAJw+WS$dAbj4u+}0PoWUxPy zZw4iT-VZ4^^cpCEWVFgs&-Hx>LWg#>GjT%aU#-4W`JzPEJKXjx#s_~JsTwBrqz29H5rc!&71IuyT%7f=#HITD+g?LIfmq|tL%!8dJziko`9GCa z1CC2!Goe97_m42IQMK0JmZiV)quO!!ue?gA_O*SPZ220qO!=|BwyvO8&H?6%{?~yd zxd5VB05wa@>tv-nuw*GE;6f#FDJ(L*<}lJ_Al|$`ogyR46WsZZhb86Z*60+G;V%pM#)sF$U?KuNq*tY6`-1SC1FYp#&0Ix@bU1+!+dFzQKOH9yf1Id<_3)350= zW7V+T>`3=r`>-|Vv^TH5)qdR+J=e}e>yB>p3s7@H71gs#={Lpx8PNUFNGQm;nlnH^ zzgQ`AbkZi=%}g)wW80MP@y9Q&v_M(FM9X*NNmkDE%BD@)l8aVoY~q`%erVW~;H0&b zL&`bG{`O*`ce?ay!m}iwyQb!>L4WV$6nm%3e&pkg6YFNa7eeiulGI+ZQ{g7(o~iso zt#)H_+)Ox}v+m5n2MV>O)BZ>`MitxoD-1F1?9^rBmaL!OMzU(n+%q$5Q}660W6$dml*@j?Bk?Y01>S^v}uY&UR)Y#6<)>KrDk!e*MG z*q<$s+7==+Af@iSDx^f-s5iPQyVyTy@H*8%$vvxfwR!X1leVc78UIEbp{AhIiM5_{ z)tn(M7D5dQ4)yHyMtx@^t>zrdfsYE=vNjHdOYId?Lf>t2&k)7t5g$XhV1(m;)dhs- zbl`QahJ_QVP&^sVvj_>!v7{D80x8Vuk42DIq6+iV7tEvwU1T;s-X_c3+y>cX4$o@{c$;Ca*?`?Ut=sONzkRfY9||1zeg%* zP(?pPqoulC98j#;RP7j@S%Nw!8`Wd;1){YXeqH$z|aFs(O*ST^VHk3 z6A=mgNmL4{tIBt7t7;Q_zCFd)TKSNcyesS0{5t`;Xa1S#f93FKi96NEx98MI4|Y{+ zndX2+I&y0|a-?6yyQ+2YX-0$u?$e{z4DFXp6^IjWYNh3T5tT&dD3;E*t-bi4zVElG7c0VRp^opx21 zQPS%|jO~*fA3jLV9!By&%re9jIwG#|*}0HdZ0X9AIDs`%^{-;Oen*dSw&JTd-wmyk zzUbSk_vrf0s~ixNeHL1_Gtj=sPpU-DV{a4wbW#OU=n7NtxL)r>1McML+UacJQu|_^qM&1Ru=8L6 z3jVvtx3ga^l5bP5*}aDFyEJ@IOS6Y=pn8L!f?v=%Uh0i3XP-C3ESKW&#yT1(CBC4Z zo2>ll7_iqm|CD6*d9M9%QDBX=7PSpw-3h(b4vZ01v^4dis;;d4pm&H?Qhu=S{RL%L zB(j(;Gj8mhADty2H_l$$FP=<2SWw^E@8D5}4yFwuga|zr(d{Hg{-h>S5z~wf^r600 z1<9-zhjz5@t5kQ37J7wTR9QL!S(H_s$M#s2a6~*bqG;I+?*w_|q@XR%TPbWC*2FM# z`xE97w-tDLFkNOy+c#*Pm5J)#T7HG+1&#W71G`_F&l*Y`6ZtZo&G}o_e+ePRcobJ+ zn6DwFxr7ECOSla=wKEvtE2&Bla?bX`79No?D%q9`mg!c4^tB#z*}e6pIVKaAH$}j z&G=`AV-di`B~}R3?zi7V?|IpLWDI$Nl2wC&vIxUX3W<@@R#7|&kAOG!~ zK-HHIBL1Bh|1qF*VaJ>|0f2$Y9jQ#x`2uEOsRK+aEOg}Cz_B<$oP=Zruq(eGbJwaj zj}Zp7TmM0q^j6FCAypEAYex?7#GsQAEh_>nX3chlc_9bU)-NuSO4As2I0k~YcV<~k z0@|R06K{LBURNwj<^Whym*|0e4^FxHBjuHfMS12m>o0MdkNz;WRTJX#GJ81gc7mZ5 zMTa9x*OP$2K+^QjA(7L_yQLn5tm0V(Nj#Z};DbG``AVUpmJTvTCWjl)o3^fJULd>u z>(e_XeLovBB1r|H?7d%HjbzfNOYaQG@VurftOKyA>^L-k6Tp%mCLupsp_g)Ko?q8a zAoMBxPN_j6fSb3SGSvDlezubN`wx;r=51Td?byPWTCoyM#OD)t=-S6RvqT$7U7v=q zD5r(6{mT(4u>udNj|B7c40Wucix42OOSP`>)@nypKmjEr74>$|)YqtJtko>j$!i2@%=@$&cAl@^ZRP8FfJI=7o$vaxB2j#(%wI zmJ*GGL*vqcy*Jmh!$d2Ld=HZF@98t*Fyo=isnZ@E_vC_#!FHG^pj`}Jo)7xym1+-s zQwW8xArj13PmP4EJ}4X#T#GpEUXkRr`?@wji>oj}lI3(w@plxhU=92GyCYf|!CScC%S7%Bva($-7~PvxgkG)ZxSf*`whU){je6epymP0Ujje@E3AJ>O-;S*Li({2MiV5v zyUcO=L9Cs!sM^GIddYj44qnmeu2zE^WO(?=`j0XP{XNwuk1Mi8z!JQq#=!N3^BJ?0 zPXggL>PHI4Frg3yL3X9ZTARZSgKxP)k(R`6w&Q4au_!L7uUL#s5GM2@{Uz#wVDNmm zt7Wu2P#_ik+cA#GY}0uWE6xR-i0D5V2u4PV)kJ%Ng0=;;XZ~`odOuTDEZvF7$5khX zF#+Y0rw=~j&m=Bp@Z9$xT$C)We8vy(V>m-rR-vH&Tj>jv zj`^AFLYbttZ%r;r(Is93%C$R-T#CFr(cx7q=Ln8qTGIEbL&DEX4E0|w-`@5 zAV~Y3ASyyZc7Fft5vkYnQ8<5Er`ZrZj==@%VmX92oRY+%24vv{bH6|4Ox2lmo& zg~o8VAJ3c_6xwIE6gApkx>%E*Ui>Lx;<$mDzmFcaUC6i|WGl(Sq(r$Z*zhq5+^SJn zs{aA9U`Z_s>?u2KQSPMJRk%Uo9R_$7k|d{60VJFb#$+_XT7g;jE(xJ$DL?I z(<$En=BTt89e`luP7-nXsO+&L1mf6V9YEU8Ql1R^R5jV$)>o&UHrjT^G;n0-6dF(Q z3{YZ0abVcqqGa%q@@9EPYU=yQ;?);f7d3&|JgeMaPr3TD$&^*q;_S6znejnc2|?#Q za~{&q0k9|!X}u}wy35!^7nrbb3rG!&`<=(xp6q`kh_p#|ZuxDxU^(DOBpdi^lzT&e zQgaH?y!>4I1;$$uftpTL5g7dM`P6x7jdc(Zh*MPE`Z5dG${_jyI;20xOj%pT}Fmr?Ur5TBM@LqJTQ|s2sUii`KKb z|6tD&LO!x~-}8=Q&PghgTTcgD#L%ST-7%NW5)rzIK8s)=+?*+f5!$)Zs6cv~KhneA zg0GcRUN^5fxFgdylz%rpRWgU8pU(C}h1uA7U}dB+0L+qZ*XMSPHCi0Jm&PA$4Gkw4 zUbCk5IE8ZlAlt-&Iw@3XFo!ZR11+zBr4_aF`af0t8s;K5Z3cHt!dI&JQC>2}r8J?H zwE=uo|Fxka<+77>GIIGlLl22j{<1e=Xw`BYH%}m5lT>nn=fZ{GwMK@@t z<)CQI;-mWieR?_<3_J&XfBbk?70tAzl<52`nQ1kVKx215iVEj|%Os1P?983Lxn(%G zSNM^(NbukOtlGkKYNqu^YB_s#g>X{CT_*e+vS6!q#{Lp>GYhkS%(D$?%?MavZ?zVC zH=R~a-`Dg{+TLaA*r!Ki)M=?nBe7qOMn`^P@yfLl_{OtpzChbkY-#O}z1w5~d2MK+ zs3OJxDMeoJiUE079Qy}^C17*Fr*)h|!oMa?L{T-EQGO}Dj;aHQu2A?=}zagq!T z&UHJO1$Yb`(J8q*+3zX$d7yB(&m9oJwFPFy^BfNiq>~gO5t-yupE;Jf(gxseI*P5~Oa3R5tJ) zn0ko{v_Qf(Y<~v&eHS%^f2`*9#IY{!^_0fcGo^DC!re77ugtN(^K=$tr209__OOyo zXFKmB#c~B3+gn($HvA;FR6E|$2s7Q5I~U^bG!@}l z9iMpmf}tJaEUeAxTBA2^c%Cv57pH07-KKHF&BgEcFYYM($#fE z%7vRLG}hG8vQTeo+|Nf;dr(_1L-ycd(cwqNcXXg&l89BO`#WsIL@J&Y0O%xq<%J%s zq@t61_fvK-;}u5erJL6N_)4tMktkF+-@Ye{KN?%^>J#6AFmUK!1ouK&((!}M)^Z^& zhtr&>P58uZ5q;;X^3Z)|{mlJy8sd`vEY0;sff&}{!uC|A?X>RpA(~BlgyvhxwSh~4 zHprQSKBamLUSN=Z6=DMwDhw}oi1j}t(`!0A_Bk;J`LpnALgEMlc?|mx&_75?)Cay> zLSj5jkfO;{XQve13F+0k%7mr|?+{s-Mq3ZtFiR@*mms12)Lz0TRVQfS@n|6a%jpuq z;pX^iV*oHM*BFvcBvi}v4o=$Gjd#*cJVhv*sAl~lw_{rI6V8smY+SH$4?h2oO2liL zpZ%KuZSd3qHS6ijS!8u>Ff9Js;h)n80`=E+dz0PR15@{L`;?A8*;yVTH2z&3d2%+k zr{sP|{5-;UwhJX`o9;ilZ^cLMYz>_^SDm?FimzJ5v?4)Y;pc;G_KR&g8G|}#RGVttd>H9~@57}0%^H00E?+;0#$i4Uou-J87Vt;XnO_yP1RWHF1ijW+iqLLvU z#0qmG3>C&$==6!^5-Bo8YlfSB7pqi4R`4XHR~Hcwt)6T~Dh*^LL+Gp%K$-UGT+YWla|IqUsm5;cgy zsP)d;o|zC&MqWb&B% zTYroc$lVtwVPoi1)FKPt_p>seZTPgS#}`zGFR}8`^cQim$tkM*RSysgFV1lolyyZ; z`%V`Bh6XL0L$0MZV|K~Myf=DwaNoa|bi{k|Y<%9LO$)+!Adirs!75$bsh{rJm)xwv zS_okEbc}phr?0Dv3FSwz&6rgJczR-v>H*|cITGF>`DE|FI=$&6Y@l9a41@VKDg-i) z3N38UXf}i;>58sWOdT?B(DZFQduLs&GRQN*JOnvk`YcL@|7c~F=}A`0(c+udH-{5G z<#TJci{P^@%Uq}4ds#%%N7-w{<oaidh@Ts;PRjI6I_1Cte-agPZ||!5r21hq#$%-lfRP-K6GRjGxC`}L)@b|f zZ&;63Nr{rcT++iRRe4P&I92K=pFC|#+WSH+XJ?ETU}=xJm4kaGCnm|YE)oQ#{X(>{ zeHTj=4cbqBu}l>k8;y*#skJx{KHdy2z*gP|oO(9P(M-o?ytb4!h^K_^#TO)p-c-J5 zagniDpIY^Mb~l1ErLeqy78V{IH#|u5h%u+Z9FdkAQm=N6PBPq(fs;jBgNxRoPA0>eRmYw|!Ptd*$%{6C9n* zxL*q|R{5xQF-oKbh|kXe%zV#~3>q^S%VZrvg^s{XOPK3rH3RQP$I-WFIo2Kb&d4!-X87VqXz}| zDau}bKEUd`!<(h}Kmpgvw8{_Xv{Jf^8nvjAcVh(caHXquezQQl5jEg@#elb9y+-Jm z6oBTpa{^4uwfTHw`tleRiogDK#Rc=sM<M*MHZjB?re{v6z+g zEUBfEaC58>vdNC)A&(ZYf{PB;bg(g5xs;S5HhkDuXUQ*bOC(el>)G&+X$3DV;hy+= z#_<8?ZF`h5DP_a=6qFvflv8UIpUiJ5 z?*@R&{Vd#wQ8FOW7%U6I8`O2~$( zzKz~?oq+>m52=@%p|0kSoC8l)iZ=aufR;LwcCDN5yt(SvylFX-U)Db_sYqGNwO!us zu{r!cX}i96Y(v}{Crs}qFW5iZ?Gw;CS(nmDcrdC7oBTdk-a&*j{S~rNl1`Hi(+|5V z3oGOc^XiAYjR&E=`KHjqUb&rs7C`CQkl`C8cAmoz4S+ZW8|2MNRZD(&}$XU2kx$_Pr4CITYTM1~H@SP+pC0hLaas0;>(^p-M; zh!hj01qF$Uh)Of`5{Q%#DT1_w9w79Z0D&Z=yzzPVob&9nulK__AGsi8LGE?0|NX0| z@$ehSg(*GqlzLT5oQ${Zr0{0a_l{RpDyBA5cZF9zj}a^Tr6{E42!ua;!B%ugK5TYc zw}Cy?=;6|Am>t}%8RWxkX8yp$1STZuHmG^?revt}JD}HGGRk$iC=vUbxAn?@`*N45 zG6H2GG!~waM({HpWW(xKDt}*7r)DeTzvl#vhUCa7M+@s9{6#I%CTUF#8`%{mahc;n zsh#iDYw7T2TC-e>h-<5nBU#{M`NWNj6+awJ9ythfdCN6HD`Cd19RkLo^oW6Wr^3^1 z6l^^=z%iBXv4%$c(6f3bO0PL1SneK|WFI!)d=p8o8<$TR)QcIKuy73<33KG@&3*Ry z>sX~&y`f{MjuvS*?6M-adKW#NxD5?z%cCBI2Ok>tiFY3~pOQJOZJ5Wxzc~taKzzsb zvrRDL+YQ_4{D_MzXA9}{kkFx`0$U4Y?sBa2QqV!z`+!O-!=>z~p$_lkSg&AjhSn(~ z4E7ihW4SeGlAm2?4On0@nPoI717FLNU0lY_6njn>j=yo>*Z1{)c{)hVr?0mcl6-{x z?f8Dab@yGcg~rXOn8a~x_I3K#l4@b`?>J!0nATKcL4(`fi~&A3hRLN88;W(7l@0C+ z^|-?~NF`Hqb3n=BfLmN^B!KyY&DPL;D!Sx)eH2y^!nf>zEIh+uHUJxW?rLOUT()(w zrHD~6^CopSA4d2S=&w=AVK|dh>MxhcH<7aMlt*~a2UW$rbW^AXNt89sBK z8|tV@Xkc3rj}$a)=M6$itXn(4g7s*-F4HC4)R=HB+5PB;-Lxa|z`*zPeyY$uIq}kr ztPZ!`idpK?@IlEoW;|?blZcG)HxF^o=Ho-e1Zf+I#dNee0I>O1LLyWIL^N&WC>Br) z(Y5Hm1nH0)){-3+qJvb(TWL&tj?$3c0|eyik{O*4?vQHyL}P|k)N=0aX9?w((h=h$ zb7!ghqivI|gU8U2AJXEC)EVpIUt7^JGM8_NLkV=l)+BOfVbpGD0&eO~S8cDiiS240l{FJ+B1x?9kJ(;LM-2({O{TRe<_{2;kzkgQP4sU=fpjL2UW_gh=~ksS>!G7P04SfQ+@It5Zu#ZH7K6+ zC>N=jJZY$3HIyuwlwdNO3Az84{xKJwsEUC*Wk-zX`+Vm{-PIKZ2`WkL0A@dC*7bEp zE_8S+VZEdMJViNhjx!sv^%5M({ZIS3G4wANC7vO+b!;Z4IygAa_467RdM=)it0&QF z^#j?IUf0MTb&;uwcr9{U^kaj%qTBa*lLs=`D-?U1|1iC)R2}P5L&t4oyI&fNpdsjA znh$Fk%;c--bqV`T^PHD0XA%r=BRUOoq?2F-m78N;k}-Eb2-1A6@ylG{x`3OVdN#*a zC>#9Z=x6SYr1}F z>=u8Co{nNX`aCDfrqgq8@2(x84?~L+ZDqWU`J6Pe2G+D_pStR}ly!u~F2q2PGecpD zRz3{XQ&|CWLA2qg3Fm(k%G21F37Zi(nUDzJABRnsZGw0q=z;|97j()U)Pgb($#;$s zXlK&l0u7<+QPp#9rm(KrpuUtW`mlNg=_ghoA()W3fBrtJ2%Kcwz^gFfE_5ezyK(&+ z8Uw>X+KmIxE3T-c(J!P!CM{H)HBNac1>g5 zMf21^G2i_TJ;%eiJobEW5|2`6-MYtf1^cKgux>>jmuuz#_%%RGOQ%i- zxeXP1U%O^O6eR3A6Jh_rZk2x?;cTn#H+wUJtPet6eOkees@K}RcnN5yIw0)39|dq(KIJuG0NEa7{`mr$OW%yGtBbw6^4PX53xTK||)Y zu=+Y~-k+aj*W0l&p=-^(P{eSNLfu7)LCt#r9@Fp?D>Lmu_ecP35E-wSbca*3y)_tstqGcf z2~m^BVHoeA8RNaAvPn+ZHz~CIa`(l<`{!B(7{Hf(85cy^7xXzjjKbJ?D`hazdYtbsCJTPnY~PJ5G0W)(Cxd_o^z45!2n&KwlUxq=9j=>7#CcA^mxhh3mB6;4=6h$y3d(0a z6e9=YMfTi|c#$-mJ1;49PN%0sqBJzH4Ax)uBt_;jrM6dQ{Q>zYzh&VTSUlf+ZqtLv z_|a&Dcv|u$qeLrSB5bB@7zMu{)|>y!{T$s5^vfWqn6jw}K(N$II!SFbI_FzdbT~LP zlS+0j?q)?jJRIq?IAd#W-8DQvaBp-3pvW#H5$LIc$y_OkY>DCQc$hzM!VnAn3VpU* zE=%~Ak->QB|0tqIxe=VAQK?@O>%?` ztWz_v_4%`RlwDc^FFqv?Npg!T*lHwIB>W&hfruTUvFh zS=}NL{pw)CI>xeX?4*o3$Mg9oAW2|cEJWfXd=;Ww^!4=}m+NOM@Vu?`iXW1BSDf|? zrZ9foe8YR^aqeTw=j6S>ska}UFVm_QwRY9#oz~Zf&`ekrF!W3ZR$?JZCk1bIxrpqQ>q3_-{ z7(yv6DWo{?i*M2Gqk^6ENA8;ME+xcY+7gIagnxYV$2Lvz1>x>#6^p8)?+=@?gTi!h zg3Wvgk-wFcC4*8et`mH4#V?L$8pFIF4~pO!yyd4~k9nx50E+HZs;3Vscc#&{NrIh> zJHe}m;2xdDMyu6Dg#iU#;-JSqgNH}3^(LO6Jsa4^>Bcg_LYqXaC4L_2LQYoPmOv>5 zjSPh>jhH$ffIABwpHPeF`+azgO>mY*N= z$orO<5ye@u6|U}&2FC-B_n;NSGZi;xGk)78Dx@<;?U?Js*7ExipB_xuUx2+Is1Gdi zz(yWx6jh38^874!mS1*9d!IjrBD}6xF=`eP3)qX#G6H;8WYf2)Ia1@&1+b+wlL4O> z2ygfZ1jBKhQP6-*ru`{&JdSgXj>s7r2~X9dWlY-^#yuUNP4=?eiI!tbLZ0E!I;pVibSo!endh zN?ZFiJ?EGNBm6!*7Vjsjr zo3F}#Grzoz{VjJ%;&DSas-if?=iPkFtieTmJ-242`GB=eSlZH@W`2WFOF#0ph)6~C zme@H6uDm&(eiA(D#T)dAAIv>EUHSS3Wu~^FK>@ab_A;HVO~0Dmq(r;dZ5E1%D0VnX zl+d9PYl&AM4oxd<%#kWXl=93^>vp-Klw$d+ zVXXIV8ml6}uBBJ@rFE2&IkL-2)+b9ML9VJ{!mo`GwBBc{p*?eJHH$}HQwSc@cTnQ7 zznzS%oZYNjdMA#HdK`b4`z)2wSi1xZg3d|-qK8>uI$ZD$oAni0nOP_j7*-4Co^UJtpfIURHF%i+q~l#8vdN#7bd&rK8`K=2gm zDWEy`X-Q}uP^4Kor-KVxJc|pZef|j3%(9KrpLDK6u2a5)g1%z9k9(NI8+{MWhUm_G z&kN82zyD&R{{|4q>kO zUieeY>9}Jy_mmmAyj1_^k*CLAJdP;=TU z>mnh@Npdb7%zT&NLYfZ@=*c(nE$v(p&c`N>2TYZAquwuW&VBwo9;@t@Qsr^U|Jn(y zI759Myoih|c7M8GxGE^7sWagZ%_an zrysz2^YjZR)bs7Fcl%svvcb#k%+B8ufD%>HvstQyqK8?!HXln@>A08+a83spt<0Mc z!>_IOLF!(5v7Qc)+mav>`!{YM`GB@{Ji$w_Xp5;lH+f0!^8sd;nS&@ zr`&rsS|z7_mcPptRb+%+W;Q-ZX%A}h*m7VQY7CZXaB!Ij=WPMvNb?>3R$k^TsAEV} zO`d~rqr2;(jNIh*0iHtBD@rykWnK}w?-h_UJv%{6wBLYXzRxO z71jS}`_>1+r#f*x3!-!sZ^(41d{zRl2F>%WVce+Yt*fT9iy3Z<+?|e z>)(d9+}D^(JxO1!jf>4Rur&FS=zf<>+9&p4~O6e!?`=#UdS@2jb1 z>M%pFq$h7v==4J^)cVyh!w+^*9UoWCqdS;|r~`d0YSfHp$g>LSa=wuR8Zs9^RK@XH zxEh?#dZhel#QLdJC>jA$&J8_AEAWEE&DIg6@B>}3kA~YvniarM{jSk#6roHdqp6#+ zG{g-q8oq_TGSG4XyFvBXt6tUE2U#4Tj*7SSbZ1`Fx6pZ6%S^!UJe^XX1~l;y{{ zTjy^F*v)Hwp_hDmoZmvUB%kr#^0Ul|LIl+Z6)sL^f%Fd-Aqo)d~8il4>m12MI!KZo49XkKq;GpkB) zrV`LLi`S9!=fs@Vept7Bm z$+Zs~;wJ{Y8UrVbUxXVvfa`)Ie;HH@;;P9`hf(ZjL>Pa~Ih2f;FqW{E2 zb_yGWsW^36%oO@=*WoPDL~xd_RvJ15!J=jv9wCc26nf*$Pu*d>plg{ns1<{ZG?H62Eb{2S27H7bi zJs@*{C}c1WA7JxD&t$zVXyIsju` z!`KR>x*92$x1qthDkab!Ta1~z?#(N35vAW#R_sW7N#Y4v?_$z^xpD2qS5gs}61>@C zo!-MW5pGc9TP$CTOIw)U+!5xfC3h@Ock>-nU+*#DX3k+RP&OG)5@0`Nyxm7b8P3Ad ziML|pC|vEwc~7bQ`V}44cbt_A!nkz8(Uj!!=tI_*M4$YlqLz(`ACXn`$UYaJo&npW zBB7`!bSM4kZAlJcqpLs&9;k3Z={Hoc%~{6n8sx~HqKq~J!-@Q6@zb`1|=OQktW&R57fYG+)?K!kG_^0AE! zjk&^HwQ;mJqq>^$PLCv_gb$e!YDA94c=BRhmD0XZj{}u)B@J=Yk&OD~nZ`RUoqh#A z0^N|QO=1jc2vraZE0(ub-WV;7sI;$))?HZu?OJ6$ed;%piM<4|L|4(Iv0hx#)8Op- z*C9h#fY%%!946jw%6)2mRPgR@xiFCDJ%lnQ^$<7H1|zoR^ka4ur#dCFy-eN-aL|aO z2*<6mkW4*+p61Tn`&94W#gUu{AC_WX=6zy%h%wn2L9I1)|C0QPBgNLO zTSZ1eMXV-IaCUs;vQ@}aZCbnjIfWONupjGFu|pb99co#VUhhVt(kv+ZMxwWCBhHv> z2QZ&0f0-_@YX(y;C@VGmfL^5yy8~~X))#4?Jc{*;f88>?kvh3*!tu*vWgOy>F0l5j+>5sHM*9Pc(C53J^~GZjDjB5~ z=Ci~whRqf5zws)o->TKO7(Ivb%`s(}a(yK04V@QxeFUR02KIZuEr!#-Ctpmt*E&H% zIqQ>r2A7!vQN{$>F3Tl7lg80^fq=Kh*=II99B7|-tEhh+&ih3&jSotHBl-UxG}%Fy z-%tns*?+Sx@8*G>VyCCZQf=^)z1_lo>t+wr@o2dVhMXu~2+oC35i3-^XTt1+U=o^f z>~d~C_UIP}PA>Ks!T=iFYrIO^$k}coL~XRQ1?-7<;Bf=rj2hl^V86cuu%y#H=XSq(mZUr{4{bbOJH``Z2pbUT*JP& zM@9=Lj;K8R!_I-Qya2CtkgEW#SBcUgpKcEu5nA86-6<&1d+}H8833wUWm@ zbKvw*P-PC7w(pC;pxdHMP*Wz`8rRYpKp~3(8RM=5_6IHs#N=FD6YemC`(7$Ly1A^_ ze(87MsCP9RKOnp)oA{>DLaa#9HZ!t^*o6yn+ z)L>6r{H zEqvPuk{_ZHtrscbm9%l472MSc1oj*0uW=u2QB*JSGxCgC)+=nQ{< zr6j#3?|JmrkS|$u%I1CDnFRuMx&%MWA*;MtEf4+;^oAn{ptdy_QWD)|$(xiY;c4A0 ze0rmb=IQo`I6iuY5U&vIN?2NR5aG7Myy!w{??@z^K^vD506%^y$xokhWa<;*rg@q> zL2uskDpBq3G}bCgJNDyTBJkPvTa>wK_Qhk`M3L$Sk-fd3*P;)S=i&TWubel#bV${E zKxTh92uD!Q>Q-wDvg&A4-VT8U^9C&EPRz2&ow3Ntn9o=SDMsElXgAF{x7nCHl#4x_Y1s7a#d}L+HQw-LzD%)%b!%w( zQm~~Vb=^%PDMDUl6g*H)$|V;VAa-_s=Bs$F~TqLTrmnO++{X|CKe zN(qy;h=doQbD%B-fKewNiy;`DkoLaSN*rW~#Sn;5rPE2Ng)^`6%O}AeZ!#b?K`$`!CKe zOlOHUY}~jfRv)Ral8Ud?t(=%$_;IRBtYJjiui2sr(iP=9D*3u~^CaAV{o*M5rNsj- zbGu)(Eq^_w{P4R7VBP%n-|1ri;j3c1C4CiE6Z_WR4bpU8>8~4e0g+_+*6}V)7+;iY=m%!rYs<23iCgnl0t}v zV~(SMdH&>EwZHE<;H1TY8$C{U*gp2t8AIHVWj(!*W}kDih+avOa3G6?@=E!-O#uq0 zbrQ{2t1V~ON)dLN>x|dWbTpFoiaNQHEIRu$s zOwPJ(TU>v7c9r|_c!9HEx?2mNt~*wdrqpQ#m0jRZ|4!&8|45k*<-A_Xm% zy#Dk3sk-_-PC__N_K&3!kqs;Nd*x@Z=q`4CUo*~0N^0qkMnRJ}KoKRVrAeT43x+E+ z&Y9AyZ)^V%k~D*7lnIaJ6SyL=93rp+#lZq@q6dmo=^c8QnvhD7%r+91y&kW30Cnbj z(C;#U{*%;u9+#idn&N_%`wkcFwBerDi4?#EU8hB~)5H<{EJbS(|E>C%37VgtPv$Cn zO8XMFabh+PM9@WvPm<|vd*3vZGUx4}gml)|=a!;OL$lNC22` zHY4QS&Srwt9l=vB+i=a#lUDhJsiCm%0}fKay|&!7U00`qsF-dw`mxdG9*Vw+wUQ`H7X{Q6UpZtUMu@&{%wXY|V zWc=CNOh!~Hq93xv3ey{)-(8c4+Bp7Qy56>UC-}vP-qF$(xo`?^A|~kD!x$>xos~|X zR;Z#xH>6oEABT$?MIJBZZ@BAsk9de4sB_Io*U0f0F>1dL6&Z1OI7VELef`-Uk1&)O zu05=gqqY~m?hBBJT0W1T-IBK^uw;*VEw5j!a!LsDayIqevLda^*wGrasI#vkvFs-* zr71r=(h;}7P1lC%UR_PVZWOdVJB#y0YS(Aq`09VF1LWP(8XARXP>#0_1R#N-7|o%C&9p zyV7!ZUC!|2zLn_Kmk%H;oXQ5F**S+VmCDOd5nH+?JXWiK^ffk(l#4- zn#@iaj8C;mMnXGp>L&tCaRFUr171Mja$EnB$k!RUc%lF_?*5$B%pt)-&O!(_%Mtn^ zcgkx@+`Tp#3ED~kIjo>OAp8t*=ij(2OBB@eW7iMce*_NyvG)%Cp?Ve(KPkkX+)uGr zU_V%Gj@W5bJazso?*t%mm=CpeX$o=h>yW7FpZhK&T(f()G;%d%_p~?$2sFf#!dV_{ zku+By|6M6o(7B5~Dl{XWs!4eE6k+pQ09XB?W6G`)cpFnBYCC4gTpAJJ1r3oSVE#q6 zl&fHKsO3^m_syOadlBiC;QHm(dA9RC!%uFZ{g1O+cE$Sq)@SX1ybK48l`&h0i#5Js z|zxFOjT`Aq|y)cB$z;YRVVC1Jx5cw?&ClYL#q$l5sn5d?O|$!l$ryztiVGxY0HF1l?$|xr*cv12U1NkaYj9)5PS%T1?fyvA z`kg`0%kqo9OJdo?%|V#Epw}ix2ayJmT^#;fWsBL@tcNINl)qh*H4k5ayjT%;Qs2i1 z{iHtxnUTtI|F9rzu-wD(PyhQXnTK-bt3Za$Z*Mt&dnKs>s&{SyCS(Z-C(ch^jP@B` zSK&Xm23imd=_A{Qj-uAF>;jmTHo=78@Zh(`Aadu%l@E?D*FgOc z7wLn(mY63tDW^|Z15%|(;a3i)O*g6#zr10ejlAf?e+A8LnADF5M5MM>tx;pd_ZESH zU)CoAs929YoMyl*LL=f+_P)1A@=V&p4HB?E2f7I!KVAJYD*q^JRd ze%+EKdqBy+jJ#wx77Wa2JyNowyp!?Ogy zAAalYpFTLNh4{scbQPzUAm8_}usM3(R(`ha6}5N2ZLscQ)fQ)e?nQ0(yMTaK)}rEI zC7^e>JAyqi?UAj((S1U7|E99^0o<`yTa(`%hJWV%x;6Bagd6mIss!)Rq?AVt2JEH0 z4oKtO;oPxP9x7IhK7!anQT5hWUMT=c-4qvOPmj+gex!Zs{A@XetY^erKSTu3NV;3kgB3aDBkr;kp@@;sS z-GbreFBD~2kx|*nI2l<@aUbfMli9GDeNjP&1}*tu0!;t_X6_8hHwOW&($&dY0D@Dj zvo2}ml)s2fV$#wStmuVY-FanLk2Kwc007Rk2K$yq|t_)Ul)CFO2PiQ(p8s@uB|t1 zaz>`$7mE1Pd1W(qi8Hwp%o|xjc_2w$^6a+L^Oc%)JtdK>yAiq}|72pSpOr)i3rqT)-Inyyu+*|u}i&qrPd5`h}m;6{S zto+?j=GFlIlq!HJ%GeZH?I(T3O8^Mo{V+4-)OdxarOZ?V2cINwh}rx}acVccUAdAm zvGQLo_1F9SDRRQ{`1=1ATl`<^9B6Qak$?Gz;hd%3<;_nrt%f*ZNA+wUQOZ~w&iPxH zU0*$Ei!z+o_)tGTT|KF_mYFDunltlW&!e`cXmveR2P2)eIal9y2sK4>YNgFVv~)c} zlDK#gzu8X2S0x zg%%85QFRQyGW=@;4~h>b9m0ptREa_0aTNZD$J8 z`^6rzbX17VmtqU=%5%RymZ4Qo_f{KD#`i(%TSLdgoY}y-gIbN(mh$&kGEzXO+mc15 zt?>+-&b$(egTqPP06Ihr)~d;3qz0NvHWz&TEBRY0i8`QpD$+^M+pI#AL9sZs`Qqn% zR#%ZIK6Zz>u0=k3Nuw}*PV4n->aD4F-%auFjLDHh50;t7H5xhUNB$zZ%QqAUz#nWB zY0If~Er_(snGl{Pjl0`7ONX!4(9EgjRPIU(h=kG(c6ugN;lIX8LJ?E>VG($X%Q{_q zfpwW-70`37Biu6m9r#-7{!(1)n@8^3yv%4O9mKVk&GiG6$sF(64%lr|C1x@mcWvlb1QS8oTj@*9he@A-_i=w^yypcrizo$L_Ge_pBL;uhc zA|E;XME&-oh}K**=RsaQhMhXNvmbUq_LY!r+`@}R8P5uvKD`C5eZ^kRA+v-pU%oB;Ph4(wh@1D2A)gbI>Onhp2b1sCt72}f4_v?%J znYQ}WPm~1LDpI)i^xi)QVNE%`sR_CE@de}0Jst&&ST(}~McRAbrPI7z6K#^NNn2mGC{14W;ip$stblF5^WP_d>(A>mj$1V+dCO2e8K2uOOvUB582*)cT29@8 zkv`>vF0hPiF&b);t{a;=sem)(U#R%{mpwN9f{&ZEq87k?gL>7%-BV@#vjp1bI)qu1XNaoR*rneMvXQzM~gW z(-G?fr6Ao`?@7Mo_7SZ8)K=f+11}C_K+7gvx5kHOz)2NVaNQ5L7#_O136)N7Ji~1L zh?b|XOr>VZpPF1$atYPmUl%6Qi!HNm>r37%!DzW(Wmi~rIDI*|zr_eYubuKdt?*Lo zl~PG%Bt-4u8AsX_TTQ{kjqs@~J-#C0DqJ#idf}~120i8L9|}iubT13Kppeb{1@^B= z>m?OlPgugNh4u}b^_SqbZZzN|cK2Gl?L=j1`m`(Y*yXV;_b*iU7^i*F>9?Xp_Jcft=S=vmyo9cqUBvMGhGg_N?HudL6)znSbG zUvK2Bn#@JtVzbBFH_D)5dQos6pJX<`p&@B9Po@~x0`MqufPY*fLQ>|u7bl&u7;WOw zT#<+4wJvPcYw4PM-YM0;~&9h zK|0{AA=emceJr$Ee|=AyavHP_=~UBD!t;*WTa!C#d(0?^cG~B7hFPo zdktXBe$@LjiFrM$IpoRQ^HnU?f_i%VsN1^gnQo(8z0-$QsJ`**@E^qs6e#d40Rz6a zIwExRv#OCPIj!#%G-5vka9_=@LRnJAw<^x}ECIm%n&hP}m2|psz--Qp+xqR)KLX0u zlTE*Vz|MiX^U9Qiu5=>VkavLm4BvqsgXKWb($bN(M_xyAIpG_@b)Lt>=zl`Sph>4@V$ZkG8BSI)%3 z`F|6g?P#}!zgjLc_4$*aZ%YQgEn|w^H;$>!Ec&ipM#!-`yhKk#L%wi7ygmuZ>hB{8r10fO<*eKEML^ZVUQb&F z6rf=EG9$q-?i=q(n2)mQ zS+~?A`q?=9OCPSWY>t#JIX7tf=>}k$JMWe{pYN-_hHx%bJCWbwpzo|=KsUHgQ?M-t znBNbsAgOs+(~aX=mYiTVmE92_!D~EQdNJCoA_$g*!3=)gUSG>r@`i0*NgTM`=>F@ z6Bz}NnNr(RNnaw{V&u;QKdJqlPk8-H$zaMMn)`{7hP#Q|b>g`@m;LGk3%v|jwUfsu zYkTZBvF`+;(xzGx9Q*NF*u zm@183(-zVi6=F46SLrEkKIed-M~!j{**%{0>|EPgYGd$W+H>mD;DC=*g%y*+kY}H$ z_~6%{sf1OQeNT&F7u@z=k;jrxp?=%P=V}W+HeoO1LT^O8CJAd#fm?on&QQ0xapFPH zHrD%5^T2umuszJ@&x!{()?#MhL0u>2=)ZeFyVcVXwT+0Ok^UK_u~Cw$5zaI+d_x@< z@T5D)qdSqC_r@vt<@k>Y(}43)H|4MKFE}D@#so%r%U|2^G?jw)M zH>F<{e2rqjIp~fnvdE3TIaXs)C;e*G^gfvn;S#{;g%BnAFwe8ycB~o`h}-aSShwxi zJKxvysDGsme!4s{_xVJ{C#4qT*ni4q|Ao^EcO7B;S_hS;AXRJ3X=sGATty~8QPdK_ zte_D@`F37AUxE_;4l=tL1Eej(t?qnlP+!lspbBe4V29 z$p+W9+s52jz0oN#SY94v)0KAThlD&P1t19oK0fETd2KlU9kFAl8BQDIwfbyaV~RPS zhM%Nk`_&S1jOJKPA1>Kqb6-kb48ZW@b;SIM`y``arZyHcsb@`w7+4@ zAlDZ~q{DHKFE}UuM$_5zBSS4_1dp^AgtI5;k+yGd$6okL)8ZNF>aVwU5sn@+`g)al z0xxzSiZ53yx8Bod7mjH@Ivx!0kZljDl#r~eC=|)C`cA#|CL7x~w4J?LNe|#$B}zXc zbIcCQyWDDt?}h2mL=_%;LlVg8*&QhXwAY=R)xNO57pX5c(kH&V+mp}!;Uh!?Rrbd9AoxdA0 z40)^KM-L&a^n0_9=ZjpQ@uz_Ylp*+YH0ev>>luc_F3elk4i3p~{Yl+^Hm0R?aJ*j6 zGLw0+#(D~I)+}|dk)1sEaLRG+aP&ahqfQ5TQ2_z@;@f+w-3HF9z=pIx;CruHtjSl5 z-E%$Im*HE`g-(Q@f=jR*RWc{eQ%cNnlfA4pb9r?4Yf31%! zr=KCzA41D_Dz^UBJ2OF+dgyKYpAu|_>g8LpYW+s`7Z&3;4c@Y@jVR}R?Q0LzKEHj9 z`HC0w_sJ>DIXx!3?}J^kK)WKP!Saw`_Y%h7Fwi#U?-Jlj$c#09JJ(YN`KnZc70tfJ z9$8{ydQl&@pJxb(Ard=BU|IESq37=Cj{nl);D7e_@LijSwEpXf_TR3Cp7>`;{{OUH z{x4R7$Gh}1eRJ7$EAl^I_dmpyj1A?7$hiM;!vNEN7S-UcX2z)g^Sv+leVhLmzf0VS zuk?RBcYjhR|3B~d&zteR8VnAiT|xX_MDWZd)?jz^v@4{swwRw4k3 zP^=Dba%N3P#VGTsS@nG0C?>&M#g!=~p*Tl(gDeNqJZ6C}Iq*=?_EhqT(# zyTF~HK>ycX*$u^tW=dDHMQv(6p{22#se{U7aZrxGs9;M_{93H3m5DCXJGEK<2O855 zM82a?Vi*<#=^ySP7XGY91Gu%BQH0seRHIg^7qBLX!01oc0i@CW$;@@lxiUqv zn0ys7vi)vHxXc=w+0;6u0c%hvcqu$_2O!MqB3-1Fl?Ys&XY`72p2!D*#Wsu-Lu>XT z)?>$~VWn)17mY0GaUZU~pNFWK2NqRI8v`+dO1}g(G_{pKE)o4v?AwtnkK^%fQujIE za;5uZ>U77`N%5k*&OsAzz)Mx^V~H6P55iD)xB_J$UdG5lV-#*Hkx}xk*dx4cZ@2VP zt7ZIX_>IDg&AusO@Yblm=iVI3$uH)RmAT1=(cX2NAR~Bq%rDF5EoG3i?4Aw!E&wcN zBS{{iggD(@5lOaG*0|STnFh>2mc0MD0C4{*tHQN4Q_RQyIYIuj*ciYgzqaiS-`;u6 zdbkxWm>OJqi6wax*GWH{5fyqyn}xcwfUP2ZQ8l8O4Tv3|Sw`!CbkWDJ`B79siEeeg z`<<_L$}<5Gjn{{n&oe7$(E0%*V=v`@)JIs4)}Nsiogux>xan!$q~>@NZS%Lba8cg& z9ps{WxT9vc(b%Se8U95$`*Ory zhsR&K$^0(Sodml-^6o@~%Bdm8ge$9g5BRyed-f3Zul{D=s2fFk!k$B+4pe%cXQluXXIzN&3S}Rv*?Er!=!3UXZn!iU zW`LP8&XF$OL0nOr#$XD@j#Ps5EhMZaizHV+j=?bLIwh7?0uQ0lnRCNXXvxm4!g@r{ zlfhDl#&c4UTE)j1XYQxe7thxPI5kM?9Mf3S+TnHOr6#nj=_TTAE}4wqmJE8$EY)kR zt$0>wy8lXOcR;GyWMmR=Vz;~?!-80n4He}Os@+)A+0$btD@Zx&<#-^idGezQ@c2RV z>G(0t@54@SMuSH*ul=auWH{?repstkx!hni^dfCMJTU1p!*BG}x2npkzZu`udG?q7 zD|SCCwJQUIp%D*6TJK6E!++<2B?C

*NG`NG(7)50_^(9Vy?IeYy~J*yI6AR;!ValL)#gXVVT z-Nd6V=ti^cchPH)5VzNrZmh+vGDU@#PqxXkIlwlT965&Zb=-}bh!#*t}|!SS)!ex{?=%LONE)(Kz=f8`|uSVN6~6G(U7Z;+K2_?;Zp z@N6}%^Tu`7>-#D}@KQ({X{2+Z$I*L@8=}|J0-K;ECu=zN;*I%XOHKNL>7Sz| z&4O zIpqguufEkkt_i)RRzfN(M%8ETbqp{2$K*aTJSo=b7dqF3=xwMF9FRJ%`=x0I#Vtv! z?LVAD#!O^-3|*aRim28*aPHEg6=zj&Tu=u|^mJmPR74o@h;(%@O7x@BLv&YwLzF(h z%W!dcO+?~7?vENM$BL4>(LVawt~h*B_8TANt8@AuS59~RLi49cH&>a&eMu`3i;ym^ z=zDNl|4T}?Yo1h&NtWQi+VjrFJ%SlxACh$R>8tGoUHy`irja$24AJ!)tR-yQNj$;e z@TE4*;Op&oH98$~ld6fPcRjw9$!x#VuT8tSaN(L~LAbX-f}h1h9PgBn%|Ft4<^qY) z3H%At!Dhr!Po7v&PO@oOirbhKkp`R@4~E#ieAu$+;n<$HgWEux@Rpz zW>U^l3d4ci`iFT6X! zf^jl8(*~(Ez%N7&31PUj^e`wkBLE$p0DWb=B-;#ry7LBI?w3xsw@?3vXSp3y^t4E^ z$UtF!JGVKhnozA<;U*<_A6{r>AfTvwlhl2=fh|`YJ8ycRQ^4*aLciMIix0UMBa-ci zoqX%IQuy_@NenBrMMvSu)ltLor!g1GU*@jo+|Hc_%iB!npE!%x|Dd9I;wS5YT~M0Z z#k}yCCudVkqJ;(|eA*ZMzk>5!$^su^Z8J@MGURRkwYrlHCVmJZ@I)Lhm@KR9l^2zJ zsrg-VOl)V!;`g^+mv>fozMF=C7MfB3XydStHaBS^VNhhLomSKW-k$Vw&C9oW)*%h_5Ef4E}CK)^CNN{ zoBaZlqP1FO%qRS`lE#=l+P@5rnF}Z@et=G4wIL(>RdJz{!W<#7TPP5u#W{qjLhnA{ zHqSnrHQ~M&fdwmNa@DC*#2B)nP8~r%J^%g1(`d$RT&4+?si3;G6^LWL)rC6c+oBdN zdlYo?bA>!;5xr)-a#hg7b^+bq67R!;sq0IVsr&8C2iJOQ23D&B->xuHj!;DW(){_N zS0`pGLISVl6e@&Q)m&rQ{>d0ze*IB>rftov3H`5fQWSF=GVniX#W~5F^PxY&FmAY> z$)I>XO4`i(hdj*Zt7CJmZc%C9-R>|5jcz@%wi#FdsfMKgw-(R{f9dA^i#o3O|iRjMDN7-Sn;)YMECnJl?81~MTVtDHcS^sIh`<)a<`qsFzit9fhFbFe{GlYoP4!5(i1HXhDJObV@$c764IM#oz3|G>*8<+6!)O>@>9sg^6q8l>Y|~eZN+fV>$ED+ zq?mK3cR-_UwacTo?yklGlfQ6oFMm5XRL^b75a}^0bxu{u>#h5e9m4ZwF}*=qjK?I~ zp?}9__Vae^hS*_7Ge-}8d)ilKgKzos%J0@&l&CJmKE^yomvc2-rqt+Nw-MHO!Bv-Y z;p(K>x1idu7p~RCPE2}E#O$&QE$Y7EF9&`b4_ywL`D9AEf`5rw@o@Rg(!HkntgHSY zPBt-6_u9*|xB&2Qqh(LbVql_EsP)H?cgqzay3|y=nikRx<6=A};9&1=ZxCpkTdSur zkef2@93$q(jUT>3OiFNb3^=(-$)w7-ovi6;(#%h^zdz^sp_1M88P>}Qe}Di@x&D!# z3EC73?0VqSxD2>*t7(v#V1H2lFGW$CfM2#1@tAZqbxom<1A3J9${hhx0O7Yx)Ddw4 zd&{bWXd=;L!%?%iTxJtyOTvp!ngu5M()L+#x>!Zuwl1vj_1w z?Y><^oxegoV+O8NL8vv=NpV|wZ->Py@Fd&}NifbzMh`U%Cm32jtB@*>X8(xa`bZI- z<9D<;y`l_CI<4q;`E=5Qc-Q!DSN=D|C-3R!98PO=)ZQ(VVm)G2q~B94kV?yiXN!P$ zUuCN-zrOO={_!oo#Cx9jcPARook52ChJMKjqwpbxo}LMgPs0Q)|9#{-U&d+U(~nNS z-qgQ&`c;SH>1Y4)e~~Nb;)L2bc7Z(v1 zN>(z;s_k^Xsw@*z<{1o+{{bD-I=4YDn!KK?S6-YQ44}b%X$J?I~|R;xQR+j62Du z&f-UR-QGKoE}9hHdh@Z()TYg}dU5+S;&>}LbD-X7T5dPwKDh2?VPKSC9G++thzxEW zhptyAS5v*#dS3y64PRipF6!O6``0;*992r&hb!kS{l=7L9I+6W>WJ%?`tWY56Q_!Y zYNm$_h5Sn?@eF+~j&n<9z*-Kw?(i^n@k?!@tO1u8q*SJk?6sldzfIqqC_7e9>*J#4 zC!Omx%QSs%|Cb)+Ic1vm%PqZutUUyjp$V61H`D@V*e~}ZuAS<4HA+){g7t1?H0S=o z9CP$e?g3BViJisS_0*IP=>Tde2@5}ROavRta}*FWL~SgBAKXAOOH+@jU;O%M%@yMQ z-RoLtBq1z@Rz$stbg5I4)srsW}h#{ z7|V%FYd0d|c~|(a&Dm{T(~ScH=I$geJlYDGx$KmcX7-jY0Jx>g=qP7?x+|E6kuELvcu!N4%VjWOPe^FBsDJj}gRWpq9~a?A_(PRnweX0r zEjIVqGx_BBLe=uU^#oJ*wggYF_qklWSDbmx688U+>_n!(_^lqI5=^o1{DFYs@=O={ z*PxD}55zR$1>Oe_U22I~dF()DN_>aXk}0HeFpaINJ_gt8NJDITHi_F!*#9o)mQCjD zmz;E-U;Ip>gE)4Apyk&qmRz>AFeCA~Ek1MDuUrFz%KAL4*~SP|L~=7~S=MdT4HQ(R z^kau;=SQEnh{a1|6vPO|HEnXBs`AaaH?ZKZSo_zmi*n*<19U=i4+@|);mjxMqJb!M zt)sM8sL!}GkKBgaM@T?7hwk$R1FP(-0t`RABqBkcIcJhc`?l( zTxQ|V2C6}=i7IETuh!jXS}jM8)Nyib(P^V4jx&_!%3uG=7As~Gut4M@4*Z-AhR{I4 zU-`QN7{51W6`iaTaqL?@@&)0AM+Xm+Isa!_=)I&HJF%Ab+&xaJ88?Yx5AC-!h&6#f zY_O}Db*t;r^NZ4$g?YVY$T;0WV>><~mP{6a`~t$G%3-T}Z9FWtu{o4ub#&E z2MJrk-wxoJq(KrXmu2BZ1vX`=uOHYNV~-U?miT|z%6cMrJHG_e|Ce=!R4fVRNJFT#YOrf zCxNN( zcGenm9q*-`Pzw&lgK?@_t}dt!1Le5Oa*vNm8UsaL$SWLQ8KwQz2+l@*tI*t%RO0ns z(-0+J7t1yIBYDT}D*Xc|y!gA0&Kc|sOwE-`)I75H+Z?V?)rfy+p)UP%xFvM>#|zE8 zWXx8-p;20(RFJ;8=L+t5>qk6z8y5_a&cQqZYS1W}^*cZ@z$~lEL41EMK3V?ZQ4t%* zWS#v1UxlQGQ5E~XL|GMSllfr-<%(rIw0<^zB74$;{BIQwgIbaHV2EoI zn=A?@G$Gp^k|`ClmO3TDwsJj`^WYi|Fa-oJGR?sIaJOy4{d&Pc>=7Lhe9Q#xJ%b1}pr2 zhMP4M$%S)Oux|9^foRdb8r4`Jh{xUmVL=tOI$m{NX#-aRnEiXal{RpP1;d6NzgYvz ziJ-d`_~8->`b$?F^acp3Uh6aLYa|UAyGowg$OSJ`H8SQ#D96-}`}X`Oy;;Yws%^75 zixNNFVMrJI`~13bNKbI&YF@9b;FlfSm?a;pA2I+2E%K_}lLc!MBIOSR4aAVnF$gFp z^3kR%O<33c7sAUC(OL*$F#T(Uv9*ozf>QBU3<_bt=dh{PQynMlPlAI=lp9`;5L%Px zDtGx+^l~?OksD2X^+TVqp74Rnlb(JwM??$#ihAq}XApEp5ka$XPR7{WQs}8rjaP|a z6i@u~&DQZxlE!;JxLe|R5%&Mbj?C#41YenMEqAz!=-aiMh;aAgu+S}jTz%1%^7vK1 z5K#E#l8o?r>xOm%j(1lFTmzUJc_12Z96#xL^!anUbwiUnFDc#Jw(I+OrnXr}jW;$j zfT@2eMo7sFKQK4bPfrgV;k;(vCWE~ZZS5NXwUeOqw*v{zM}=HQR`Q_-*}O3cHXBR9#dA^? z>%T1hLL`>m93wWijaBLSZImxlLH_9+mav`~5V65{59@c1?#+G52c9ttlFbjX6E<+S!F1))=Ov4vd4ppdVclfRvLdMbu4tqZa`SI5

MjV2^v}F0J0N)3mKwkNrfx<{M2>c{(yPi?cj5Q%LNCO_M2e_VUj%ZM~%n$6&8Y{&2jE0tfQ=_=EF=Ahxv|H$AVpO_6aN z=hL~((YE$4hj1w;g^R!ww|ww_Gq)FMs{%WAF7b}qpl~Co#}>urKG@TbUQPt8THtMi z?s3_UfNp?z_S!FP8k}XGX~pPGY2G5p=C}tb{}_tqn#mS5=R&RA0Eo@>dJB(nN4|kd zRS;;GBt5(~G)T{gmOKZvmnuf~r<}d}T9qgE^x1*U++5JaRot7=4jM{)Gb&;3yc&4aKMNcZQ<{jg|Owyh8AX@ zz1R9Vp!nVnbo8wx+^f`>>nuGVKm3U+2Oqkilw7(Mw=&wC2Dg8XS|3Q=*ospj^a@IR zf8Rm-Z!JJot%0IG(_tVKGUJ`TFqp&ni9j)?C~>6EI?fqe>GK4L*d*xR+~3j%=mL>X zi-Wkyyb@u?ow*+(=xO2`lsKho-Tvgdp&ny}^a2n<{kg?n2YEmR!%?lCo(=kAe`=Bu32ykiTwh`b z+m{ts{(oh1h6+cSGgDka92V@U-5IOKwpsROFQ(j?WTJkkh`n*2`AXmjL;BPitAvHU zIcbUoc+;gnhR79U#x@ z;%nGZ-idhOy%#ZP$C%=%a4hSI7_&|C4YnhuRCPIk%@wgH}h4EnUv;tCDRGt{M3+Mm;`^a$WggOOf_{ zLag`IxvxnNhTG*qW;uXk8JX#cqegqb_dfU9cE~OF#XCVH3Y}1fdSD7ByY-!tV-lgDj(_c5f{)o5{DA zq$y-d)mXCFUWj;u%nCd<9N|O%m7zI{j&O^ja}8~jLNiyANCDFh7Gk>Z3I&O40Zq2I z;H7V<=TYh2C#>|~gEWQ)BIW!(qRqC4 z1nHvyes~Fyfzz#_!@(S)bCgjSgc>{YYQFF?_32Ow&^5jAoEypL=hlD96=_z-f9B#0 z-*oSX{1L4DPnV*pm*cvn{&~!>W` zTBq8S#@-_ru_;#(h`CUFZ#8bA)WDf$LytCXV^8=L`WUM-fx0>|I?0G@FS-zZ+q+>x zjq+DyuB^)GWZ7lS|M10%a8@r91;sXLWO5&f$!g70x^-Q zi)}(}uaJjsGu>z%Q){8gur$uwV{)-)=SGLd`EmFmdKyp@*Px?}JYb*4e%G_t`<276 ztLO0OExS^{S{yGOX%+%zm76`HZEDAbV*Uu;`XGygZc$GAql%%%=!uYAO(l~l#mAc0 zA^b0OY#Yo~Mj>O}F#|>#g^Zf(i~x6YQix<{<}d}}a(3&30Ul!P6X~%h6&nQ1C4_E#^h!Mm$V@4o0u%$GpJxlAZSOL*OR^}%z za8Ok8Ct^Eiu$l84Dfa7#;eu;G)Su5(8IR>G09kgF@y9e-8JUyBIa7qzn`&Q^^mVo^ zMy{ytdM4YTSGfJ6t?{MR%|Eo&mENiImvgGIKh@dkTP;(OrggNhpukkUh! zM!obM(%Q*$JG%PABY^?}$EJD}Si_cHP6uP=uq9FBv^npLzn|^YJ znbK^6&ys^bJAZ%Z^^m~G*7#&=L=-Y!_S7eKmfEIKaIseIx zXpJ$2B=GFK2D6lRN$tLIkvMv|N!HvoeQq2d!=D`ipwZJ@OD%-KoA7qfS^7n{d`$ZO zS63Lge`>F-g7GmMbB^`RIb#3DJFKS2KI)jY*M~skSd5y3ZItcOWuhLqyw1g3K+B@E zeK_Df=z+M)fTO}SH}EwkYVo1C*T$fhgl-bD`i8KG16qQUcsIb}p zp`X2_fLkv;H1{0(WSG@CYMG8jgVf+0%jZu)NP9}C*NSiE!@!b zz9_&86qYxt|FjArhBr#ldPl8()=&`ZztbhU*+GwxHP@JM5Ey6~*4t`c2#FD}Rs#Ey zSlb^q(OAZ!gDD_RiHnrWWn|hJ5n%{dKo?tUfR~ZO(3V5II%k^V)$-zH-A_(yoWUW% z;BLgE;P3yxv8N_u&7%WGz0Zh@U?vt5e4D9^)v)SX|} z{GSZ}y3nv!wp3dl)t!Ui+g>JHyX})zXLgOy9M;99{5O_@e{3YP%wD98VI8oB9qqR{ zcA##gNpJfpI(g|z{Yp>hy&nxyM>SI4U+p-JytUGOxSJvR)3w^#a2UHj?!=+bYLzye z*siE-kj_N5xDm=@8U+CGLlK?AJ>u`Oc<&7BUN6fCYTq_dYy ziRl?W(O*eH3PuNzT*4pW7VL2UneNQm@2AGGp5UE0v%PMP!6U6v6v%3Qx@2{)ZJT5Z zSo3EsL%Y9UgtbE@)KuTh;%0Lq6>{{(Yb!3?y22sWR{8DcA%&NbCI1MDmH%y|O2^~& z0!eyZetuS7{W>rX6k9Nx?#;RVfdKW zF%8|KJt4l&QlmVQ`xL>2svK$<1Vk4-232VtbjYF#Ua3)yTj1LUJe z?RWPW%}eFDgH=G=f6zzF!QxA7kvebRBrTP526Z$?AUFPRoYmfTFL8EloZpuD({wiU z&@bTXstufuhqWKnMqHVQcLNr(W=ahXkUNtnCwsoDuiFUSznU!Sp?}5W>HVX8clqWd zgQK(V#at=>TzqNlJEyhiHClOa-7Q!B+|Up&LeWISTOE&Knma3*oXfaF2m$DbpBm!T z#q&mA_`$~yG6kJKs((J@#SjwWgk_B3Jncwemo-JHs-TO_H z2tu;;ieqAI;g7A@zXqG3ZrvZ19-p}j|JhKMjCV!HR-mgidL`SQ#YZXycl@=d&Uy$d zw300E-sJD)71Mtb?;w4r-Sa6J(dm7wMvC+gmvOB7O5fB@vmfnJ%qja>^a=PN_1IpC z_UQ5*TXG>dm9@%okeYj=Bp-JNJik3YoOpbx^>OstFuC}QkCZpA&~ttKjwocXXopWl zod9wxhU~q+#7#Zr%DY|3y+HBpVv^*$0ZA5X%7R0~o-p z)0*&8*M7+r=7{lVNnu5sp%|)_w?WOxrP!F##>I-@h2x9}0^H>HuOlRo<;8n*?d3L9 zLMHKoIzjU0^suPxL7!{{wnk?K zH1>o;&pXjGigOp1&0L}q4r*R_p`66-rhC_;T8lQWyh$MoSdKDUI7=BX&O-BT;kS4L zjR8kPNLe>0>tdcqWO{}vg9dSVX-e_M5dW>l<5D-vPK*6hmb|tk{dX|& z;cYJd_iv9&{T(cF_pNPK?Nm@NkSuj>3An*-nRp{B&`<>Ag&jOKj@m@h!_##K6X#Y7 zW>lT=sW{s2GJ6*X-@1*dhJp)1H9GlXxV;vq;~kV3Tl$)PKZu+BGaoW?;nq^!tATxE z59<5Y2MJk!**f{MeAi-u$oZVA^%iPkv8cvjU9Jud{p2rJmr%pwh6g3Xly8H*fdL0y zhvEQfSVs4%IDY|1;3BqqwTw@j3oo5)#m~0fH=uf6^fzfNhwT#r+VTP}IlmF?(-BZH}!mY_6?Z``PVH?B7m5 zOs7)ReTWqCvR|M>U*c>w1{J51OYF$ad_1IcnX;2s#Cs;VnB8rQ!Ql;{_@_n>&`^=q z&v>A(FU;vzG$$NGxiPVcy#YRtyhdJ6J2=U}W|kA94%a12^YW*A~+wZTE=PBBJF_G}j9CAbj9KSB@X&`1#S zntKaPvfqa@biUQ}^5z!rT?|lL`{}fQ7bU*)NIvlA2#mvH*U|KI6Z}^qdS~iY%{HaD zO%6$|gu1vCTo^EETsVBS`hwL_lmZF3mySFDe$cwg!qy1u-;heC%z-whLd_%XdOdn8 z0i~bDSA1vuo$&1fg7^(5O`x#vvMB^`;U~q3M#WB9as6@dEm|JT$>LjNGXys&-7y<( zEJ_N*?VlUWLu7FQeKvXDV%)bkscPSH3O&t}_c=6XWVGBFx)L}U=RC^qDVSjr{XPzU zb@gJXwT1!TassxdsJiFH8hXAOnQzOUt=b;4jL~N{gB@wGg>jk8`A(?I!Tq|8$^%0y z|3N*^o>u5VTsF?Vqtw2E88{UyN#&r5hN{+js^*?0&w(fTD&6+%7O6xutCAoKIlxt| zhfqHd2R62<_jG_L%f=o2Fc@xScU;IN8H;!g->;E_(oSddT>w7b5PmQP`**YIHs0#% z{b%08O&Lb~$cSzXXHy%uwB#KOEW(fGV2?{j84no)Tz4+mN;T@gu2{*p9<+<#(O^Hj z{Q2QydoTULIi+NsY($0hh#LQdvk3M=_PRN>nVTfK>|7k`AK6HVwZ2GO$Jcx>su)Aq z-OsutPWr3jrYjL$bY_QP7sPlW;UW@xZht%7uip+M&bkZm{4DzheUoZ-!m=k{oO-HY z6o21~@lLMd)TV$?gI(mggTBWvzkxmzlW;4{{%4lx;mgA?m$6^(M0~pL05(0o6rTaB z$%4_Y&6)Iq&|%e5(!z{|Ykq?bYavx2E#MxPBpYTuo!g^-w%|x0?L2&lq4p~RI9tq7 z07Y^&x?&ezDGC*c#GG^Op@*JpVOu$+lhe%ZZ+LQEFI`bW@EI|Vi7NzoDD>xGhMn)k zs?iO_wl1R_*A}y_DJc`0l;=&C)^$)@ib=}U`_=|pw4*zf!>6{d>tT$PY5Ay8 zF^_tpdb@Hza0(UA&Yf<`0FIx~KKk764w}Y9F7O$KT#Gq>A^1UDNOfS>gD0XBcAE^5 zxbXAQl4(R@nd;_qtGybP55<=)VLghRb~`VjaB9MY_AVjM+x@RS|wbG)oM1RKa09&q;llL z>D~6($((bz?S{g~4U_UzGrzWGklTSk7Cd~Wh>KiBP9~(Qd1j3--sSA4x3vo}NBU#+ z#FT+(4Ig=Dr19&mf;5exs*3kmwbFh8Jr6CWKJjGu&s(y1DF?5tM@Fn zaZ%eC{$R$Z^_D4I-GD&m%Fh~?)*u9_42A3yPJyx zF4#?Se&?Jk+!|Z`g4&SqNxP%`ILT*2oYH3Qo$e%`via=&oui_*&xmU3XU!xVQbUzP z&`OFb#PQ|2ZwpZyV!=hOy>Kc0d}DbL#lw3&GGSG(=Am)grh6l$GW;1U{w1R3!V^a? z@|DCqg92zbIe+vPaDSiCe4=s7v{_|ya9__hh@4RVivXk#(tkvqIQfid z#>TxM^y~KVX&jVL&9Qp08Z$Kp^sLX0l$pEJ93IsCPV07Wo!A&+p}1n} zGNbLJmS?u)K@E|S=Xpzl<-^vgtoDE7(9L)o2+owX4rDm;%(}1&0*ZuYpUdkJpd<|; z&)-3pjlL9#*q%~%c|E>x-oAMAJU4ErKF_u2+!o{SAs+}hGFEawkaC0Vx=qGLf zQI~Mqjzkk(0>#viPkCon@NSJIcsm{qWcO3jw?PtlICu0-!R938yJ?aY1>v6X=>)Se z&jG!H^-x})^Udub!7P(R@3Cx}i9sU!b$W=G!dJys=6uh^NPv82S&70@51p?MwceHAF z(O(4puH`ywC$>Bn>E^=cw^I>0t~qp$`y{i3{1D~gvYa7k-Ku$+uL*dl#Fy~>$=MDEe9H;FHg~}c#!pDY-kaSX zg=SM?Vlj__Mzm&H*{3I~O{#TIr9QtO*wKp*2{SP~_ze?%^NJria^ui*yhX<#jcIfE zaiiR&t6YOrO{p}|6Z40cP5Rh}`>$)%CLF(p-rzbBs(IWNkfIkY?kJvIUj$dMp{MD5 zQz~Ze@=4)KZ=SE7{3nCaYpuf7`%}%ASXk-yl7Axta53&gbURW~QnJ~!RYCnl0X)%i zTR(2;-0dKotNQqb+{v{Zig?xE^x{qAHW zU_uBsT=(fdz3CY52d2tYkef@;b6Lr>dpkkCy4Qr?m!vCcenwv9Rju!Zz;cXyVZ1+?u~SYtOrM4@&$DP+|?QH7YME1tVe>k%l?L~4YAHF zx2q{nZQk}6<-en;hB_P%-ddVEtoPdo)K_#JtK96-k_{mq!9D3J*je0NYV>Edc_-0f zzR#}JP0N$4pd9(Cm>{9B60Wf8EhuqkoN_=8uK*~Qu;{{ zlE6wYs1>W2d_3!5BSD6Lb+FK7epllU(R?$Ui&FACTglB^ZA`x{-0>qm_nhApA$rZ1yP3=7fel6g~)}Idy&<#XR|U?i1VS;vKQ5BbIQF9^uigXLgzxQ?p;mn0{gFCxNm65sa2HBj4Fx zEU7K(9JQ^Jj8@((%T$KATwCAj30GK_m0|zeoY_zjwB4=AAPS1k=J}wYYm1UE(D%h- z#CJHspiKy|pl#}^fm}QPj1S&=R7T0>%tcqEu!rj)V)R)`X!&fTP5wfpP#%Iaa^o6C z?}|O=N%H3W9YxtCJfo+4R)oJB6FkrgWE?pi^sGubGNIvT72fe^ILB+@8K|%C=4G;>FbZUiLPE~J!yTBFjMAR zjC16nE%|HxxsNxMRhaWqSC~-EVp?_4of| zO38oODwcY}gWPGl`Yld^$Sce!x1d(-iZS6?G`ZcmMeBFnxu& z_f5SGj!x;KPK9O?ST;im7qk%Rw>*!$7y&@|G1I_Qjzsd&T=j+<3BRK)bkg%8@j~nO z#w|XnYqz4#XB$4L6iHsLqKrQAdS2*gC5TB%QMLj1Us482HP7bo?6$ILd0g+4&Q`i;NzJGf4b!u8i z!pFAD=ADQHhs6sbf(JAZ&Ul09EE5?#jm>?%!g1ZsS>a8-7YFT0ZQHI=2g?W0)uQ$vua)vhxDony&HB zMF74h>X*fnlN({)1IcbExkDlnGuXJ156XLc97!lOI+Kl(?Fes}#FuYIfIRvzvn{rJ z^B%D-Q$B8}!_96S;~JG$$~fY#>>RkQ;luJ zi9oH(!(%SW%d?mCYdyNZZtNMurLHa|9hLJP##Q{rh$(B_fD;z!6~4$SE2oym)AE7YL&3>oLV%vwxI8?Me-Hk-X-LNFEA<1Z_uUif#O z-j{avYxZ9FF;E{x?R4Xcl3gdfEBqYh*%yybjS48D#G6wfP?B0nHg>y@(r&dN9NPPn z+w#kjT5$+E>!>T+{az&RJ4W8#QkrwRUkQ|_Cn`o+xvfKusGj5#1u6#3eu=#aA*W&_ z)sJg=DhjP!XS`JifDaUTnu^Z8VSNv_T5#FM&Nb0feV$LB_bN6(K0q5aROYd|6 zU;sh#$FkYz@P=QDtSbdrLw|{ky=FZ>N>wY`9evBE&b}oJ@-U)_bx!y2?wQGwUXNZI_S^@eRLhh3+i1Fs<*4z2Ka!d>7DW!|PNv=b?v7j!sv05<2lZW0`<@v7g z%L^i*3a(*QQ3=B(x#w6lMtdpQr4A1**UepHz{s0m-g&j!Ju+;7B^G>+tvZ`MOpz$# z^~Suey8wv{6~^f;WUmv3zek*)ZsCl2aP#WaNCmyHVO*ZGo++u*U)%uIW-=lG*t{vI z5r#mcn%x2=wir#BoM;~@U~9!?MJ*^JXv=%|Oya!75CJS2=+?O(pU$}O3?2-M<}Bn;)UU}lmrUUc`#AU_J2HUF zgjm6j{jSFC>^ytlBlJo~m`timls9s}s0uo;E5mNs1==&!Y>M>Ng=|#X*(iPIupPDeqU>itMj~yIoy%w)hsR!CcF+XJ)%grJ_N%qhySQy0or!iH zDmWbt^wgy$7pu;#nSEaNi{Tg5*Id2w{Xz8oaYvoFYuZD2nb1LRcNllAAgjiPC^uzWrr-VFA%8nl))KqT;V-IlOp2^T7Jli}ZFVEi!=KIeebzf}E642d zZ78#1$zl;c<^tjQEm1)LYyKm?Cx5*FKF3IiV>IHKuGSNh{Bgs7`-}U=xDOhyP}QM4 znUWl}Y$$_qZP^sYBW;zltg*x~Z+t~;XFbeW;_1!r2`b&@K*f?5=Yzj8*Vy zJ0nVIedmAQhom$skmobpgd*w(^0<8eSJa*?_8Ft5dWDncXzeUbhwuaoRIJe3^v6+7 z614XDxW>~9E^5p^KbJm07;HaWNMK zzw$J)jgl=Er|3qA?_FOohq`*+BgUDHV1E{_BqPEQ$1O*PZ*hTs=`LN0N(FnJBAY#} zm%J!W=jRP7_&S=M+t|~p2J?uWZ5*A!?7zm~g)rgjg0?8_$kVo`Z~`h|RZc~VwXFcVkW-o>5kav4EpjAQI1;x5;&my>ql%>-6K}C{f3^H_fHVY) z#ZCTR32;}e=OlF{kW&xK)ObI>pbMipor3>lbbkU~j>3`XC(^2jV5$%t5e1Bn0!US; zys6R}npz)`8R-G8jA-5RwJA7UU_r^gjY4{WgM_Q$A3leYIn?;)%+cc5DW=a&WnJ0$ zmk=Z&0ZOD;8Xye=a`UfZHz;3ZgG-ilfKd~>Zs=!?8y*3nS-rzBSv8w^i8M&mkM_Nz?hv5C@~tPNr;1L!an1C zRz|0nc~mCZy!4&U!z)7v^J9{?PM7Z#PYF09s9(bu+v~ESK{igPHJSS&v1=&bmB1=S?y)-$HCAhDQTopc zf{2!vkQmK>XSKNwi;?@w-Q{WeRZ7MCtvSGiM^K^!!1j>PJwd(OSc9!RVbk`3sDMydM353x{Yo~#b)#FO9PIZ5NM>ldPW)h!!S;9*NK{Bw#jw)St=nJLiT?=D|2#3QNX*i*D{e+l})=y z(yvZXDE^!hbdQX|zN+2PTDz{?)eZiPcF;fKwLgXYF;=-GlBuEU13j0`scjrF~b zsBY&nQQ4%f2uy{7kM|qm2dIv`^MK^Jxv}Ca<$=anun; z_j0{^6$=nbjk$$xuxSPs_^*D!q@-faUP*SP^=fM(H^$4ISsA$HqHWwVk+-d=)-Bpv zn^&j*bK{yNZh0o~(&yOc1HDNNwQnM=`q?LsGQ)}SUYG5JLtj8KfSx8W&#m?8Zn`j2e5_M#qbi| z9GZ}+MaD|RigW7rSKgr!W0g~Yv4ce*bWG*Iz@?TNgL5V8Wi9kgZJ=}3SR|u`lKSP3 z!#f$g#={7*EAHIHJMX}y5~6HKuRRNQ8fk64*LHGo=@;7nrRn9KwK+|QRbwuOZmhH^ zuxqWBR6T~v#bmgL00QaL8IMQZXX;uV?2F?aEhEHPVMVtJSdaF96`_`Ga6pvZ)aG#g z4x#aUElIyhwJ`3Yu;@^AYZ6upa!;qduoW)97kXIUw8WP_xHB9X7d2 zrkkEg>fZFRdEUM3=l?KPg*v=$_6F0+i@7EN)tr1Osrg22!;iq#Tb@onX1Vvh&)ABg zYl@v3_1W62B2;KWT=z!tSH1AcwwP2ETT7T|Ti9>^N53e{Py3p|jX%F>B@K^l-pt&+ zH2BVEJA#*;m4DYZFtb8pYTeFtm`?@tDdLzqoO7jyeZl&hDMI@%-z?}Qc9}!lxORqz zJoi%5I0x78Q&qbNG_#Wf;?*BLrJm{LDNhprzRG%e>HoS!-;tf(`#&KUqY*rVON=!} zG*g7=IZE^dTeklBAILk!`k{N=TFR2p|EI7kkB9R6{*j7QMCg+>WGTCkvW{$_EJaze z%!DXQD(e`Gr4m_+tYMJIlF6E#3S-OI${5Cy?0dtE-R~JuqxyV*f4Q%D?sM+@ywADk zp65R2dX5?g2N4t(co`;)R+esp;Lk>nm5isoXf&KT%& znsuQdCHq*iuXu5bD<-O(PH#5TK0g$0;aRL?PM_ONOA~0=MeDn zbVlLHz9m&`(HxgCrc5_G`#-(8PwBIB7I5vB=RSd}W#!8j8E$BWKZ6=aM^| z!rX6y2MQx#*X&CsGe_b<-!9!o451WPgT@-u4N*(NaEZ1wI@~A6xNH?TuE}D}4vzWk zxU!UQ(P3jyo5l*4Ym}KjJqGe>vr;Ikke#aX)U;pf8RGjEB){3bk8v6su#ul-`Ek?p zVh1r95po#5nT=^h5sK*8j;i&!-RdRGGu`XiG-ZCb{`oTe{Yc}`*CIOMTXkYZU5RI1 z6NdI%+?Y8NUHd3ZQTQS&d%LdNVi4b*I!H&P;6doK%dj# zF}+~-&PBMar??I(v;Z>LaJ|v{XzuuJ;*A%2998lhiqFFKaUhpQ+1SH$N6v#^z3lIt zT~2CpylLOK=uo(+9a(!&uQs z>e^aZl~@UNY-z%=J$RYQV;)eE(d8F@ko18bVcCACOZd_@!97taOh^@tNhy9XTjPOK zDH}sB!Cgg!NhYsy(SiC&gH@@BE-`cb1v%tL4A-@OZ%02ma~^2IunQZjIjxM17L&z* zf*@g0fy!&FX%%y`fbW63o#|3)fQ&o(J(Q{L`?Yc@Z~IsTre~IJ!Xwix6d{#v{`^h7zO z;-0;N;OPs>oQMQ4w#kg!;YYlfc`v^At9-5(vt+^dP%^el_^c_h z{W!mGkI2PVaClzyb3awhJnNLZ9iwm_#xE?|I*(uSKYRRCTCLTEA>$`i&nuLnNpT@)!>NJN~;+N}MEQP}24P{Po>JI02 zy;LVURk1qhHlrLG=NMZ82Q3k-!x)g_=|#&kFHmrW788#A5us2`x7y3~N)xcXEzrD% zE}|J-D5{0iJ6CW4A5XyPN{#khccV>dB1D-8wAxTiWU(jHKXk_8ECi4@%hL)^xh%?5 zf)LTJS!37DCEXPUr1lQVzbfyWEA{_o2UHL^788uMb7jD@jdnfPjJ49wMmNh$;&hK95E3)32XuW(&7^wS@Sq|^^ zw$WED3y`~jmq{Rcid}Gr*TMAqIOjp3XJ--a=o(mRsVHVm8uh)@m)-JcpGsl>qyal< z0+Vyc{%ekzGbwrEqT*PU5YkX^edMuK*!JUbHRdE$DPqcgv;7lxc^|L~H|w2nq`^nX z{5cZh!Ns_Dafa4;I^}>X(JM>P%6TSR0KBZ?%fl!F<9$_Cn3L-@*Anh7fY`$)^NeQ7 zVH(Mz>_$Ays74p`kH*A1tM6rIsWDf3xhRyw15{<*%v)#br5nOUt7PkzQzcMwS`FV| z?~0WqIF@@IQPMOB1<=uo`)D~Kjn=2%W^-|^$jk5fk39Q+ z%aRj2E-a}oeb((BY6;XUPcnK2;yaaG{37{PkMsJ3KzQwA;MjGuns}e_naOFsC;^=U zT6wVsD@>GQxlAddF9&$#_z?Lx#cVjr?O(86ZP%sTcNzT+{3ZF+SAuL7vtnn;DwYrO z0pI0)#px)j_V|0D1%_)(UtsQ&tUIrDh{zLTbzxDO561bUsO-UqjML9fU9lhec5fpi zM0?`VsA$;W%fv6o=yW`xe0*|wOKRf8rRcg|eGP*mXa1+f_SnLDG1n2eZ4ec@*+MB& zvadWnl*)GEQu{f^ykVn=Qs@-ljHGz^LU&h<9z$)s;mP+}lqYUaCOSEY5mNhm+0Bf- zP&LrcU{82_XO=qAuZQ^7%)@&{)q642==v~Ae)Zhf9zN_CaAJbA6@FZ9I#1`z>H={# zwv#V;eQh!Kv89BOa~n&XUt>Oq0q0c59lOLqI#-!3F%lz>N?cEFC`~u-J9c->k(Oq9 z)KsRMP;jo!&uPQ{zW8Ha7fma-gPO;}1WqKwZNsL&M%Vb|_sK&&`HFi$mwbaJM_xyM zTI>|O%Cv68oTCGC5j2fF>uL(4_A&+j$0YvJC#j2YlTc$Or!ZO35etO(qp$|6m8pl~ z<=>)H;=^3R#{z@{2TRh=%G$SvS;Kgr+~cpbx}U<{^GY->o7I^4W2_J7OyNz>S<~jt z2kEuhxlR@aUfI*2f6=<*pc##?Xl2yUbCi8y6Y5)b414|&))#A-+L-~F3`d1Dx+c6v z6;L;I8Z(apHa+AO#dHe?>(|DxPA^d#F_96LUc7Ombe;yVskexVBx5@n^k@QSx&spI zyzmZ{pVr10(UigHl>E^VwxMpqFMgEPX^>U0sDX~%Yiuqs>8062D1Dvt%A;-@LZvy6 zDgW}C5ux&p3fe{aeUUk%sqQtn=j6(J5oExPAlWDQ(i%Rmmf_Oj<;cm^2cG7`*UeYL zV_*yW#LppzueSQ0wW_d&x@-10|?i@v)^e+MrgR5P!F3Xd+zqMKmVLUj&9A8E=Jmp0PlJ{+5^ ztz0M_*~?pw8c;Cs64mQdd2^bRdAu^9LHW2_WMADXci*P7)!v7BlcI3~iDnhts5i~T zv!X%Ty@Ao7CRi+MKBNw3%Z69b5_~?d6z#aCspV1Cuo~%!$jcv9u!n9o#kp@jvs(nx zGA`eGf*6hptsj+G-KkWY-DFRXZ0+T#|@ra z;uKKkip`sj3M})I(0qF)f@^p-zkt@BPO+89_$FVY@g~D7UR@b$y_@bcg5sv(HlsH! zig%+JoUP1NRV5G9qSQ22Vhp^~~1S5HK^k!TMC{}p?#+oDS~8zdO4 z(WSCc1ZK{dfaP(dp9rH}MEKp1o$>=cd&wti&qdGt7CrVQn!$FJoky-V^jn`z^qIY)SIb(;@& zk+W9NSqgf6QTOPVYUN;cy}y-)BnR-Gx58SEJB)vV!1-8NL}axw(6vHNxJ7Uf-6 zaPLt|1pdzXCpikt>_fSeUh?e$3im!&uFeE2Z0Il`?((M*(!1vymSjVnsNl`D(8#a( zi$ulNe4Q!m(0pJLD(xU>`FaL+L_-Txi24{SCjgWNG_6@iAmGQ*abw62y_ohplF2x8 zIfpvNuC<$t(N21FNfE1%mN zGxW|OOt`eDe}YkAgMI|1-g87@h-lO?^4Of+vOAq1+a}I}FIfK`XL^dDoiI#Cz0EFKuSW zR0664q4dDk2~cXGigL%A>K7c99?VyuTk)3t@)0vZCzqOoz@N?zEU|sTv)U;~Cw>ZT zTsTl~w_guTD_dLp-6LGBX|M(N;#gQvGniu-BX~lmyz-6hL0|s0y3`N-9Q)kN9ia)@ z)iONqZjSL;U{G49o^x_(<$ zgg~5BZMI=;+7}qO?+kt;hKLqJA-E}Mn(Zl>Q>_UbWgb7vxx^BvwN{Om)Z$WVIBE|b zWM((M*zUu7UiiXVfn}JP=84PC$IeW?DC726m3Y>s?SA=b(&Z~pmRu-u*p4ZoC zIwkH)nOSDdgLFP~>IdEH8%Mwzv-rHem-0Gj*cLoF#(PKL>fG%+EpqMJgau~ar3dbg zN)|%2ClYGthcE%g$2c`kU%7Q$A@>+-P$IsM_n6xnYR0fP7zf-^p>RmiSjY#!iG zFR9p;ajb^eXcty-{PU#qEUGbfvHgNZ5n)m0Y)Op2@ccVeii!uyQvEyI{sqGuA?p?s zIQ}VEs?AdK&R&35%EwWTA>ia#?Y|>7o=D+b;>4WXXD@4S3kT#AygA}--I0EW8yl`K zoSZ9$+K8;^xR|hw)1KPgE7+0xwF*$T4RA2TBqrcER(9z|&*a`7^D?c@XgcnK#hMPe zsEs~o>y4&d;kz?7J&xYa6;6Zsh6fmPstEG&l8Kf~U;1X}sfgx?;Boe_9Pm_gh_Lrt zC28OxgK30+13U8g2%@40sOK(t)!%oci&YWS_6{Y%a*r?pX?K(dVTG@{= z95?y1)Rx$Gw9Ty1)O@$2yvIsb=Q8m8a-&x+(JtLEKmng05N~)R(H!^k)8{h2sbWGF z(xb+vYPrqug~wF7rO;&N*+q<;;!0>v`DO!AdKgpLB<51?cwz?Xc&!gP@0~qb@sFE= zsQW|@Uq%0!S#kWjg-lVOzKDl+rRXPO(ljxrTzw|;d!oz}n=u;WXTZUd*3%j^DnIKG zNhOKy%~TrpT>AI^y(Z_W{n<6sb!m!!J)8@(h+j2w2HIg~;YeX8o{AOk=?tuRlqx+6 zu1TmHt$aH@%80u+|_9h)OrD-Z>g8H$J>bjlV8LK9vL5fY1oCfL25e?@iN!U7TaL)6Kl=> z3!$@JiJ9z{#5!geP+org7BJcOd%(=%f#(=un&Z#O-?nY<0{iP$GhHMVc2@HOo%sge zp8WYfj#o^8$~53F_jCkQSDx0;MlhWS{L9tVARlkCogQAXQ-I*K2bw%vHSUo<_cVXi zk?xd~iY-x@CX-T29S6A{S!GDG;vOOe5-@PeZi!lackiT3#LQT0} zkW80iDj|)-Gkv8Jq`t6A=V5=jXFtN{%yYoW-h!!(o?=BY30Hr48f1jw?(mNk>6cV4 z-&l3X2ve$cA4i$R9|EhJc?QTgz0#N{X!Sa5)HWL6I_-hKGK5{L$1#G(n#{obx$AQPCWP1QWlW>Z{i=+bi zuhqUf0298I{+lP}<^DE(eoncqb8=o9KyT24EW00rSNGAk8tfLh)$Kk1z`;{u`hfA` zGUR4UcTF%!nqu4Ep=0O((@h^3lZ0I~{9)?dAa~nC0&vU-C?@;|_lNdV82+aXrF}@e z`J8RKOpbxecU-Od*6#*^*phrWB*LI0!sp8WnD$$S-FqG&Vvl{wrEM~&Oej5o3iLl@ z&e0Sr(*PHlQwhT0ir?6eIYIH}w>u7r0m|(yFA;H=rM@Lh=0n@fU9LIBNs>$FW)t~i zhbjJ-20E6ws6f)n~vLQ?bklfeW{>Su5qod#<?U+kTT1bA4 z2xtzE^ZnZJH5CDA#&htU0c7Zrg2Y9B!d3#`R|MS&S|1)l~X#kGNu2FU~`=`^i19YkcK?MGA(6)4l{g)wU2{*+1BN8v@TErcHMpA!$voDTyFVDhBMe-Q-7C~}i-GKzafns8* zObd70Z;R=lvAa5v6ki)uLw!cQ`2Iiv`~l|k z91-&)quy3<0L%>`ZMW?v1A+ijphDewozErz6;w%C;E&wGS_Du|(rwnPgfv*JdNtF3 z$8I3e2Xcg{9*fI6ypn`S-=#TDy7{Nbt!7d*U^sK`1KvGMEeS~gYqCj2 zO^_`sLssa;7 z3m2{4YTVn6ZC~4zpb#b-ey4fdlQQQrrDPnS-#-=WBIe|tBN^ypx4pdo)Y^BQl)i3o zD>9C{eENSutN{7;=L12ff9PNBt5uC5bA14V&dJw^J$oo#X{st+-RkrM&w&#F_ygF` ze~`OOA;wO9+<(_EfSsoHksO-hQpdPzwb71RWby5X#2-+7l}-W_UUqGF*;eTH_`1uY z8%!ksH}si0_M4*KCn#2b8|jcNfWp7>V~*o#s)F6N-0Jx=29QJ%cu2N6TB*P7oz~10 zhJVHY;K_&dEHI{9?ROO}V!N9-8nzg!;Opep0_Nbqq-FBNouHHK7^>i1rrcJ4CkT+V z$B=RcLTL1t9p$v>C@=gO1b(2{t2(f`XV0F^P3?2f2|!~%$zXf;K($Y)^ow8T1Af_~ MsivcvqYUx;Kli60Q2+n{ literal 0 HcmV?d00001 From 0f5e8917e51e61518b875734b68ad806ea0c05ac Mon Sep 17 00:00:00 2001 From: Dias <120464230+dysmon@users.noreply.github.com> Date: Wed, 4 Feb 2026 20:42:41 +0500 Subject: [PATCH 004/161] feat: open source publication of process_sd job (#975) --- .../scripts/appregdef_render_job.py | 2 +- build_pipegene/scripts/gitlab_ci.py | 20 +- build_pipegene/scripts/process_sd_job.py | 51 +++ scripts/build_env/handle_sd.py | 7 - scripts/build_env/process_sd.py | 331 ++++++++++++++++++ .../tests/app_reg_defs/test_appregdefs.py | 45 +-- ...rtifact.py => test_process_sd_artifact.py} | 6 +- ...e_sd_local.py => test_process_sd_local.py} | 2 +- scripts/utils/pipeline_parameters.py | 8 +- .../{application-1.yaml => application-1.yml} | 0 .../{registry-1.yaml => registry-1.yml} | 0 .../{application-1.yaml => application-1.yml} | 0 .../{registry-1.yaml => registry-1.yml} | 2 +- .../{application-1.yaml => application-1.yml} | 0 .../{application-1.yaml => application-1.yml} | 0 .../{application-1.yaml => application-1.yml} | 0 ...application-2.yml.j2 => application-2.yml} | 0 .../expected/appdefs/application-3.yml | 2 +- 18 files changed, 413 insertions(+), 63 deletions(-) create mode 100644 build_pipegene/scripts/process_sd_job.py create mode 100644 scripts/build_env/process_sd.py rename scripts/build_env/tests/sd/{test_handle_sd_artifact.py => test_process_sd_artifact.py} (95%) rename scripts/build_env/tests/sd/{test_handle_sd_local.py => test_process_sd_local.py} (98%) rename test_data/test_app_reg_defs/TC-001-001/expected/appdefs/{application-1.yaml => application-1.yml} (100%) rename test_data/test_app_reg_defs/TC-001-002/expected/regdefs/{registry-1.yaml => registry-1.yml} (100%) rename test_data/test_app_reg_defs/TC-001-003/expected/appdefs/{application-1.yaml => application-1.yml} (100%) rename test_data/test_app_reg_defs/TC-001-004/expected/regdefs/{registry-1.yaml => registry-1.yml} (95%) rename test_data/test_app_reg_defs/TC-001-005/expected/appdefs/{application-1.yaml => application-1.yml} (100%) rename test_data/test_app_reg_defs/TC-001-006/expected/appdefs/{application-1.yaml => application-1.yml} (100%) rename test_data/test_app_reg_defs/TC-001-008/expected/appdefs/{application-1.yaml => application-1.yml} (100%) rename test_data/test_app_reg_defs/TC-001-008/expected/appdefs/{application-2.yml.j2 => application-2.yml} (100%) diff --git a/build_pipegene/scripts/appregdef_render_job.py b/build_pipegene/scripts/appregdef_render_job.py index 1537b7df1..1c97a7068 100644 --- a/build_pipegene/scripts/appregdef_render_job.py +++ b/build_pipegene/scripts/appregdef_render_job.py @@ -39,7 +39,7 @@ def prepare_appregdef_render_job(pipeline, is_template_test, env_template_versio appregdef_render_job = job_instance(params=appregdef_render_params, vars=appregdef_render_vars) - appregdef_render_job.artifacts.add_paths("${CI_PROJECT_DIR}/environments/{full_env}") + appregdef_render_job.artifacts.add_paths("${CI_PROJECT_DIR}/environments/" + full_env) appregdef_render_job.artifacts.add_paths("${CI_PROJECT_DIR}/configuration") appregdef_render_job.artifacts.add_paths("${CI_PROJECT_DIR}/tmp") appregdef_render_job.artifacts.when = WhenStatement.ALWAYS diff --git a/build_pipegene/scripts/gitlab_ci.py b/build_pipegene/scripts/gitlab_ci.py index 5bd7290ca..df060fe64 100644 --- a/build_pipegene/scripts/gitlab_ci.py +++ b/build_pipegene/scripts/gitlab_ci.py @@ -12,6 +12,7 @@ from env_build_jobs import prepare_env_build_job, prepare_generate_effective_set_job, prepare_git_commit_job from inventory_generation_job import prepare_inventory_generation_job, is_inventory_generation_needed from passport_jobs import prepare_trigger_passport_job, prepare_passport_job +from process_sd_job import prepare_process_sd from pipeline_helper import get_gav_coordinates_from_build, find_predecessor_job PROJECT_DIR = os.getenv('CI_PROJECT_DIR') or os.getenv('GITHUB_WORKSPACE') @@ -83,6 +84,7 @@ def build_pipeline(params: dict) -> None: "env_inventory_generation_job", "credential_rotation_job", "appregdef_render_job", + "process_sd_job", "env_build_job", "generate_effective_set_job", "git_commit_job" @@ -117,8 +119,8 @@ def build_pipeline(params: dict) -> None: cluster_name, tags) jobs_map["credential_rotation_job"] = credential_rotation_job else: - logger.info( - f'Credential rotation job for {full_env_name} is skipped because CRED_ROTATION_PAYLOAD is empty.') + logger.info(f'Credential rotation job for {full_env_name} is skipped because CRED_ROTATION_PAYLOAD is empty.') + if params['ENV_BUILD']: jobs_map["appregdef_render_job"] = prepare_appregdef_render_job(pipeline, params['IS_TEMPLATE_TEST'], @@ -129,6 +131,13 @@ def build_pipeline(params: dict) -> None: else: logger.info(f'Preparing of appregdef_render_job {full_env_name} is skipped.') + if (params["SD_SOURCE_TYPE"].lower() == "json" and params["SD_DATA"]) or \ + (params["SD_SOURCE_TYPE"].lower() == "artifact" and params["SD_VERSION"]): + jobs_map["process_sd_job"] = prepare_process_sd(pipeline, full_env_name, environment_name, cluster_name, + params["APP_DEFS_PATH"], params["REG_DEFS_PATH"], tags) + else: + logger.info(f'Preparing of process_sd_job for {full_env_name} is skipped') + if params['ENV_BUILD']: jobs_map["env_build_job"] = prepare_env_build_job(pipeline, params['IS_TEMPLATE_TEST'], full_env_name, environment_name, cluster_name, group_id, artifact_id, @@ -140,10 +149,11 @@ def build_pipeline(params: dict) -> None: jobs_map["generate_effective_set_job"] = prepare_generate_effective_set_job(pipeline, environment_name, cluster_name, tags) else: - logger.info(f'Preparing of generate_effective_set job for {cluster_name}/{environment_name} is skipped.') + logger.info(f'Preparing of generate_effective_set job for {full_env_name} is skipped.') - jobs_requiring_git_commit = ["appregdef_render_job", "env_build_job", "generate_effective_set_job", - "env_inventory_generation_job", "credential_rotation_job", "bg_manage_job"] + jobs_requiring_git_commit = ["appregdef_render_job", "process_sd_job", "env_build_job", + "generate_effective_set_job", "env_inventory_generation_job", + "credential_rotation_job", "bg_manage_job"] plugin_params = params plugin_params['jobs_map'] = jobs_map diff --git a/build_pipegene/scripts/process_sd_job.py b/build_pipegene/scripts/process_sd_job.py new file mode 100644 index 000000000..d9268a101 --- /dev/null +++ b/build_pipegene/scripts/process_sd_job.py @@ -0,0 +1,51 @@ +from os import getenv + +from gcip import WhenStatement + +from envgenehelper import logger +from pipeline_helper import job_instance + + +def prepare_process_sd(pipeline, full_env, environment_name, cluster_name, artifact_app_defs_path, artifact_reg_defs_path, tags): + logger.info(f'Prepare process_sd job for {full_env}') + + base_dir = getenv('CI_PROJECT_DIR') + base_env_path = f"{base_dir}/environments/{full_env}" + app_defs_path = f"{base_env_path}/AppDefs" + reg_defs_path = f"{base_env_path}/RegDefs" + + script = [ + 'bash /module/scripts/handle_certs.sh', + 'source ~/.bashrc', + f'[ -n "$APP_REG_DEFS_JOB" ] && [ -n "$APP_DEFS_PATH" ] && mkdir -p {app_defs_path} && cp -rf {artifact_app_defs_path}/* {app_defs_path}', + f'[ -n "$APP_REG_DEFS_JOB" ] && [ -n "$REG_DEFS_PATH" ] && mkdir -p {reg_defs_path} && cp -fr {artifact_reg_defs_path}/* {reg_defs_path}', + 'python3 /module/scripts/process_sd.py', + ] + + process_sd_set_params = { + "name": f'process_sd.{full_env}', + "image": '${effective_set_generator_image}', + "stage": 'process_sd', + "script": script + } + + process_sd_set_vars = { + "CLUSTER_NAME": cluster_name, + "ENVIRONMENT_NAME": environment_name, + "ENV_NAME": environment_name, + "INSTANCES_DIR": "${CI_PROJECT_DIR}/environments", + "effective_set_generator_image": "$effective_set_generator_image", + "envgen_args": " -vv", + "envgen_debug": "true", + "GITLAB_RUNNER_TAG_NAME": tags, + "GIT_STRATEGY": "clone" + } + + process_sd_job = job_instance(params=process_sd_set_params, vars=process_sd_set_vars) + + process_sd_job.artifacts.add_paths("${CI_PROJECT_DIR}/environments/" + full_env) + process_sd_job.artifacts.when = WhenStatement.ALWAYS + + pipeline.add_children(process_sd_job) + + return process_sd_job \ No newline at end of file diff --git a/scripts/build_env/handle_sd.py b/scripts/build_env/handle_sd.py index 6602663d6..8abcd73f6 100644 --- a/scripts/build_env/handle_sd.py +++ b/scripts/build_env/handle_sd.py @@ -188,13 +188,6 @@ def multiply_sds_to_single(sds_data, effective_merge_mode): def handle_sd(env, sd_source_type, sd_version, sd_data, sd_delta, sd_merge_mode): base_sd_path = Path(f'{env.env_path}/Inventory/solution-descriptor/') - handle_sd_skip_msg = "SD_SOURCE_TYPE is not specified, skipping SD file creation" - if not sd_source_type: - if not sd_version and not sd_data: - logger.info(handle_sd_skip_msg) - else: - logger.warning(handle_sd_skip_msg) - return sd_delta = calculate_sd_delta(sd_delta) effective_merge_mode = calculate_merge_mode(sd_merge_mode, sd_delta) diff --git a/scripts/build_env/process_sd.py b/scripts/build_env/process_sd.py new file mode 100644 index 000000000..6602663d6 --- /dev/null +++ b/scripts/build_env/process_sd.py @@ -0,0 +1,331 @@ +import asyncio +import json +import os +from enum import Enum +from os import path, getenv +from pathlib import Path + +import envgenehelper as helper +import yaml +from artifact_searcher import artifact +from artifact_searcher.utils import models as artifact_models +from envgenehelper.business_helper import getenv_and_log, getenv_with_error +from envgenehelper.env_helper import Environment +from envgenehelper.file_helper import identify_yaml_extension +from envgenehelper.logger import logger +from envgenehelper.plugin_engine import PluginEngine +from envgenehelper.sd_merge_helper import basic_merge_multiple + + +class MergeType(Enum): + EXTENDED = "extended-merge" + REPLACE = "replace" + BASIC = "basic-merge" + BASIC_EXCLUSION = "basic-exclusion-merge" + + @classmethod + def from_value(cls, value: str): + if not isinstance(value, str): + raise ValueError(f"SD_REPO_MERGE_MODE value: '{value}' cannot be non-string") + value_lower = value.strip().lower() + for member in cls: + if member.value == value_lower: + return member + valid_values = [member.value for member in cls] + raise ValueError( + f"Invalid SD_REPO_MERGE_MODE: '{value}'. Valid values are: {valid_values}" + ) + + +MERGE_METHODS = { + MergeType.BASIC: helper.basic_merge, + MergeType.BASIC_EXCLUSION: helper.basic_exclusion_merge, + MergeType.EXTENDED: helper.extended_merge +} + +ENVIRONMENT_NAME = getenv_with_error('ENVIRONMENT_NAME') +CLUSTER_NAME = getenv_with_error('CLUSTER_NAME') +WORK_DIR = getenv_with_error('CI_PROJECT_DIR') +BASE_ENV_PATH = f"{WORK_DIR}/environments/{CLUSTER_NAME}/{ENVIRONMENT_NAME}" +APP_DEFS_PATH = f"{BASE_ENV_PATH}/AppDefs" +REG_DEFS_PATH = f"{BASE_ENV_PATH}/RegDefs" + + +def handle_deploy_postfix_namespace_transformation(sd_data: dict, namespace_dict: dict) -> dict: + """ + Transforms the SD data before writing: + - If userData.useDeployPostfixAsNamespace == True: + - Replace deployPostfix with corresponding folder name from namespace_dict if exists. + - If userData contains ONLY field useDeployPostfixAsNamespace, remove userData. + - If other keys exist, remove only useDeployPostfixAsNamespace. + """ + logger.info( + f"[Pre handle_deploy_postfix_namespace_transformation] Original SD data: {json.dumps(sd_data, indent=2)}") + user_data = sd_data.get("userData", {}) + + if isinstance(user_data, dict) and user_data.get("useDeployPostfixAsNamespace") is True: + for app in sd_data.get("applications", []): + if "deployPostfix" in app and isinstance(app["deployPostfix"], str): + current_postfix = app["deployPostfix"] + replacement = namespace_dict.get(current_postfix) + if replacement: + logger.info(f"Replacing deployPostfix '{current_postfix}' with '{replacement}'") + app["deployPostfix"] = replacement + else: + logger.error(f"No replacement found for deployPostfix '{current_postfix}', cannot continue.") + exit(1) + # Remove entire userData if it has only one key + if len(user_data) == 1: + sd_data.pop("userData", None) + else: + user_data.pop("useDeployPostfixAsNamespace", None) + sd_data["userData"] = user_data # Reassign to make sure it's updated + + return sd_data + + +def prepare_vars_and_run_sd_handling(): + base_dir = getenv_and_log('CI_PROJECT_DIR') + env_name = getenv_and_log('ENV_NAME') + cluster = getenv_and_log('CLUSTER_NAME') + + env = Environment(base_dir, cluster, env_name) + + sd_source_type = getenv('SD_SOURCE_TYPE') + sd_version = getenv('SD_VERSION') + sd_data = getenv('SD_DATA') + sd_delta = getenv('SD_DELTA') + sd_merge_mode = getenv("SD_REPO_MERGE_MODE") + handle_sd(env, sd_source_type, sd_version, sd_data, sd_delta, sd_merge_mode) + + +def build_namespace_dict(env) -> dict: + namespaces_dir = f'{env.env_path}/Namespaces/' + result = {} + + if not os.path.exists(namespaces_dir): + logger.warning(f"Namespaces directory does not exist: {namespaces_dir}") + return result # Return empty dict instead of throwing an error + + # Iterate over all items in Namespaces directory + for folder_name in os.listdir(namespaces_dir): + folder_path = os.path.join(namespaces_dir, folder_name) + if os.path.isdir(folder_path): + namespace_file = os.path.join(folder_path, "namespace.yml") + if os.path.isfile(namespace_file): + with open(namespace_file, 'r') as f: + data = yaml.safe_load(f) + logger.info(f"Parsed content of {namespace_file}: {data}") + # Extract 'name' property + ns_name = data.get("name") + logger.info(f"ns_name = {ns_name}") + if ns_name and isinstance(ns_name, str): + result[ns_name] = folder_name + else: + logger.warning(f"Warning: 'name' property missing or invalid in {namespace_file}") + else: + continue + logger.info(f"Namespace dict built: {result}") + return result + + +def merge_sd(sd_path: Path, sd_data, merge_func): + logger.info(f"Final destination! - {sd_path}") + full_sd_yaml = helper.openYaml(sd_path) + logger.info(f"full_sd.yaml before merge: {full_sd_yaml}") + helper.check_dir_exist_and_create(sd_path.parent) + result = merge_func(full_sd_yaml, sd_data) + helper.writeYamlToFile(sd_path, result) + logger.info(f"Merged data into Target Path! - {result}") + + +def calculate_merge_mode(sd_merge_mode, sd_delta) -> MergeType: + if sd_merge_mode is not None: + effective_merge_mode = MergeType.from_value(sd_merge_mode) + elif sd_delta == "true": + effective_merge_mode = MergeType.EXTENDED + logger.info( + f"SD_REPO_MERGE_MODE not passed. Calculated based on SD_DELTA={sd_delta}: {effective_merge_mode.value}") + elif sd_delta == "false": + effective_merge_mode = MergeType.REPLACE + logger.info( + f"SD_REPO_MERGE_MODE not passed. Calculated based on SD_DELTA={sd_delta}: {effective_merge_mode.value}") + else: + effective_merge_mode = MergeType.BASIC + logger.info(f"SD_REPO_MERGE_MODE not passed. Default value: {effective_merge_mode.value}") + return effective_merge_mode + + +def calculate_sd_delta(sd_delta): + logger.info(f"printing sd_delta before {sd_delta}") + if sd_delta is not None and str(sd_delta).strip() != "": + sd_delta = str(sd_delta).strip().lower() + else: + sd_delta = None + logger.info(f"printing sd_delta after {sd_delta}") + return sd_delta + + +def multiply_sds_to_single(sds_data, effective_merge_mode): + if effective_merge_mode == MergeType.EXTENDED: + if isinstance(sds_data, list): + if len(sds_data) > 1: + raise ValueError("Multiple SDs not supported in extended merge mode") + full_sd_from_pipe = sds_data[0] + elif isinstance(sds_data, dict): + full_sd_from_pipe = sds_data + else: + sds_data = sds_data if isinstance(sds_data, list) else [sds_data] + cropped_sds = [] + for sd in sds_data: + cropped_sds.append({"applications": sd["applications"]}) + + full_sd_from_pipe = basic_merge_multiple(cropped_sds) + + logger.info(f"Merged data after performing basic-merge for multiple SDs: {full_sd_from_pipe}") + return full_sd_from_pipe + + +def handle_sd(env, sd_source_type, sd_version, sd_data, sd_delta, sd_merge_mode): + base_sd_path = Path(f'{env.env_path}/Inventory/solution-descriptor/') + handle_sd_skip_msg = "SD_SOURCE_TYPE is not specified, skipping SD file creation" + if not sd_source_type: + if not sd_version and not sd_data: + logger.info(handle_sd_skip_msg) + else: + logger.warning(handle_sd_skip_msg) + return + + sd_delta = calculate_sd_delta(sd_delta) + effective_merge_mode = calculate_merge_mode(sd_merge_mode, sd_delta) + + helper.check_dir_exist_and_create(base_sd_path) + if sd_source_type == "artifact": + download_sds_with_version(env, base_sd_path, sd_version, effective_merge_mode) + elif sd_source_type == "json": + extract_sds_from_json(env, base_sd_path, sd_data, effective_merge_mode) + else: + logger.error('SD_SOURCE_TYPE must be set either to "artifact" or "json"') + exit(1) + + +def validate_applications(sd, effective_merge_mode: MergeType): + applications = sd.get("applications") + for app in applications: + if effective_merge_mode != MergeType.EXTENDED and (not isinstance(app, dict) or not app.get("deployPostfix")): + raise ValueError( + f"Application {app} doesn't have deployPostfix. : notation is supported only for " + f"extended merge. Current merge mode: {effective_merge_mode.value}") + + +def extract_sds_from_json(env, base_sd_path: Path, sd_data, effective_merge_mode: MergeType): + if not sd_data: + logger.error("SD_SOURCE_TYPE is set to 'json', but SD_DATA was not given in pipeline variables") + exit(1) + sds_from_pipe = json.loads(sd_data) + + logger.info(f"printing data inside extract_sd_from_json {sds_from_pipe}") + if not isinstance(sds_from_pipe, (list, dict)) or not sds_from_pipe: + logger.error("SD_DATA must be a non-empty list of SD dictionaries or a single SD.") + exit(1) + + # Build namespace mapping and transform each SD before any operations + namespace_dict = build_namespace_dict(env) + + # Transform each SD item before processing + if isinstance(sds_from_pipe, list): + transformed_data = [] + for item in sds_from_pipe: + transformed_item = handle_deploy_postfix_namespace_transformation(item, namespace_dict) + transformed_data.append(transformed_item) + else: + transformed_data = handle_deploy_postfix_namespace_transformation(sds_from_pipe, namespace_dict) + full_sd_from_pipe = multiply_sds_to_single(transformed_data, effective_merge_mode) + validate_applications(full_sd_from_pipe, effective_merge_mode) + + sd_path = base_sd_path.joinpath("sd.yaml") + sd_delta_path = base_sd_path.joinpath("delta_sd.yaml") + if effective_merge_mode == MergeType.REPLACE: + logger.info("Inside replace") + if helper.check_file_exists(sd_path): + full_sd_yaml = helper.openYaml(sd_path) + logger.info(f"full_sd.yaml before replacement: {json.dumps(full_sd_yaml, indent=2)}") + else: + logger.info("No existing SD found at destination. Proceeding to write new SD.") + helper.check_dir_exist_and_create(path.dirname(sd_path)) + helper.writeYamlToFile(sd_path, full_sd_from_pipe) + if helper.check_file_exists(sd_delta_path): + helper.deleteFile(sd_delta_path) + logger.info(f"Replaced existing SD with new data at: {sd_path}") + else: + if not helper.check_file_exists(sd_path): + helper.writeYamlToFile(sd_path, full_sd_from_pipe) + else: + helper.writeYamlToFile(sd_delta_path, full_sd_from_pipe) + # Call merge_sd with correct merge function + selected_merge_function = MERGE_METHODS.get(effective_merge_mode) + if not selected_merge_function: + raise ValueError(f"Unsupported merge mode: {effective_merge_mode}") + merge_sd(sd_path, full_sd_from_pipe, selected_merge_function) + + logger.info("SD successfully extracted from SD_DATA and Saved.") + + +def download_sds_with_version(env, base_sd_path, sd_version, effective_merge_mode: MergeType): + logger.info(f"sd_version: {sd_version}") + if not sd_version: + logger.error("SD_SOURCE_TYPE is set to 'artifact', but SD_VERSION was not given in pipeline variables") + exit(1) + sd_version = sd_version.replace("\\n", "\n") + sd_entries = [line.strip() for line in sd_version.strip().splitlines() if line.strip()] + if not sd_entries: + logger.error("No valid SD versions found in SD_VERSION") + exit(1) + + app_def_getter_plugins = PluginEngine(plugins_dir='/module/scripts/handle_sd_plugins/app_def_getter') + sd_data_list = [] + for entry in sd_entries: # appvers + if ":" not in entry: + logger.error(f"Invalid SD_VERSION format: '{entry}'. Expected 'name:version'") + exit(1) + + source_name, version = entry.split(":", 1) + logger.info(f"Starting download of SD: {source_name}-{version}") + + sd_data = download_sd_by_appver(source_name, version, app_def_getter_plugins) + + sd_data_list.append(sd_data) + + sd_data_json = json.dumps(sd_data_list) + extract_sds_from_json(env, base_sd_path, sd_data_json, effective_merge_mode) + + +def download_sd_by_appver(app_name: str, version: str, plugins: PluginEngine) -> dict[str, object]: + if 'SNAPSHOT' in version: + raise ValueError("SNAPSHOT is not supported version of Solution Descriptor artifacts") + # TODO: check if job would fail without plugins + app_def = get_appdef_for_app(f"{app_name}:{version}", app_name, plugins) + + artifact_info = asyncio.run(artifact.check_artifact_async(app_def, artifact.FileExtension.JSON, version)) + if not artifact_info: + raise ValueError( + f'Solution descriptor content was not received for {app_name}:{version}') + sd_url, _ = artifact_info + return artifact.download_json_content(sd_url) + + +def get_appdef_for_app(appver: str, app_name: str, plugins: PluginEngine) -> artifact_models.Application: + results = plugins.run(appver=appver) + for result in results: + if result is not None: + return result + app_def_path = identify_yaml_extension(f"{APP_DEFS_PATH}/{app_name}") + app_dict = helper.openYaml(app_def_path) + reg_def_path = identify_yaml_extension(f"{REG_DEFS_PATH}/{app_dict['registryName']}") + app_dict['registry'] = artifact_models.Registry.model_validate(helper.openYaml(reg_def_path)) + app_def = artifact_models.Application.model_validate(app_dict) + return app_def + + +if __name__ == "__main__": + prepare_vars_and_run_sd_handling() diff --git a/scripts/build_env/tests/app_reg_defs/test_appregdefs.py b/scripts/build_env/tests/app_reg_defs/test_appregdefs.py index bd98f04e4..2d77b3cd9 100644 --- a/scripts/build_env/tests/app_reg_defs/test_appregdefs.py +++ b/scripts/build_env/tests/app_reg_defs/test_appregdefs.py @@ -4,7 +4,9 @@ from pathlib import Path import pytest import yaml + from render_config_env import EnvGenerator +from envgenehelper.test_helpers import TestHelpers class TestAppRegDefRendering: @@ -62,47 +64,8 @@ def _verify_rendered_files(self, test_number: str, render_dir: Path): expected_appdefs = test_case_dir / "expected" / "appdefs" expected_regdefs = test_case_dir / "expected" / "regdefs" - if expected_appdefs.exists(): - for expected_file in expected_appdefs.glob("*.y*ml"): - base_name = expected_file.stem - rendered_file = None - for ext in ['.yml', '.yaml']: - candidate = render_dir / "AppDefs" / f"{base_name}{ext}" - if candidate.exists(): - rendered_file = candidate - break - - assert rendered_file and rendered_file.exists(), \ - f"AppDef file {expected_file.name} should be rendered (checked {base_name}.yml and {base_name}.yaml)" - - with open(expected_file, encoding="utf-8") as f: - expected_content = yaml.safe_load(f) - with open(rendered_file, encoding="utf-8") as f: - rendered_content = yaml.safe_load(f) - - assert rendered_content == expected_content, \ - f"AppDef {expected_file.name} content mismatch.\nExpected: {expected_content}\nGot: {rendered_content}" - - if expected_regdefs.exists(): - for expected_file in expected_regdefs.glob("*.y*ml"): - base_name = expected_file.stem - rendered_file = None - for ext in ['.yml', '.yaml']: - candidate = render_dir / "RegDefs" / f"{base_name}{ext}" - if candidate.exists(): - rendered_file = candidate - break - - assert rendered_file and rendered_file.exists(), \ - f"RegDef file {expected_file.name} should be rendered (checked {base_name}.yml and {base_name}.yaml)" - - with open(expected_file) as f: - expected_content = yaml.safe_load(f) - with open(rendered_file) as f: - rendered_content = yaml.safe_load(f) - - assert rendered_content == expected_content, \ - f"RegDef {expected_file.name} content mismatch.\nExpected: {expected_content}\nGot: {rendered_content}" + TestHelpers.assert_dirs_content(expected_appdefs, render_dir / "AppDefs") + TestHelpers.assert_dirs_content(expected_regdefs, render_dir / "RegDefs") POSITIVE_CASES = [ "TC-001-001", diff --git a/scripts/build_env/tests/sd/test_handle_sd_artifact.py b/scripts/build_env/tests/sd/test_process_sd_artifact.py similarity index 95% rename from scripts/build_env/tests/sd/test_handle_sd_artifact.py rename to scripts/build_env/tests/sd/test_process_sd_artifact.py index 089402fd5..071308c21 100644 --- a/scripts/build_env/tests/sd/test_handle_sd_artifact.py +++ b/scripts/build_env/tests/sd/test_process_sd_artifact.py @@ -10,7 +10,7 @@ os.environ['CLUSTER_NAME'] = "temporary" os.environ['CI_PROJECT_DIR'] = "temporary" -from handle_sd import handle_sd +from process_sd import handle_sd from envgenehelper import * from envgenehelper.env_helper import Environment @@ -38,7 +38,7 @@ @pytest.mark.parametrize("test_case_name", TEST_CASES_POSITIVE) -@patch("handle_sd.download_sd_by_appver") +@patch("process_sd.download_sd_by_appver") def test_sd_positive(mock_download_sd, test_case_name): env = Environment(str(Path(OUTPUT_DIR, test_case_name)), "cluster-01", "env-01") do_prerequisites(SD, TEST_SD_DIR, OUTPUT_DIR, test_case_name, env, test_suits_map) @@ -60,7 +60,7 @@ def test_sd_positive(mock_download_sd, test_case_name): @pytest.mark.parametrize("test_case_name,expected_exception", [(k, v) for k, v in TEST_CASES_NEGATIVE.items()]) -@patch("handle_sd.download_sd_by_appver") +@patch("process_sd.download_sd_by_appver") def test_sd_negative(mock_download_sd, test_case_name, expected_exception): env = Environment(str(Path(OUTPUT_DIR, test_case_name)), "cluster-01", "env-01") do_prerequisites(SD, TEST_SD_DIR, OUTPUT_DIR, test_case_name, env, test_suits_map) diff --git a/scripts/build_env/tests/sd/test_handle_sd_local.py b/scripts/build_env/tests/sd/test_process_sd_local.py similarity index 98% rename from scripts/build_env/tests/sd/test_handle_sd_local.py rename to scripts/build_env/tests/sd/test_process_sd_local.py index de9edadae..d6a4a8082 100644 --- a/scripts/build_env/tests/sd/test_handle_sd_local.py +++ b/scripts/build_env/tests/sd/test_process_sd_local.py @@ -9,7 +9,7 @@ os.environ['CLUSTER_NAME'] = "temporary" os.environ['CI_PROJECT_DIR'] = "temporary" -from handle_sd import handle_sd +from process_sd import handle_sd from envgenehelper import * from envgenehelper.env_helper import Environment diff --git a/scripts/utils/pipeline_parameters.py b/scripts/utils/pipeline_parameters.py index e943febf5..f0c43e2cb 100644 --- a/scripts/utils/pipeline_parameters.py +++ b/scripts/utils/pipeline_parameters.py @@ -30,9 +30,11 @@ def get_pipeline_parameters() -> dict: 'RUNNER_SCRIPT_TIMEOUT' : getenv("RUNNER_SCRIPT_TIMEOUT") or "10m", 'DEPLOYMENT_SESSION_ID': getenv("DEPLOYMENT_SESSION_ID", ""), 'ENVGENE_LOG_LEVEL': getenv("ENVGENE_LOG_LEVEL"), - "BG_STATE": getenv("BG_STATE", None), - "BG_MANAGE": getenv("BG_MANAGE", None) == "true", - "ENV_INVENTORY_CONTENT": getenv("ENV_INVENTORY_CONTENT") + "BG_STATE": getenv("BG_STATE"), + "BG_MANAGE": getenv("BG_MANAGE") == "true", + "APP_DEFS_PATH": getenv("APP_DEFS_PATH"), + "REG_DEFS_PATH": getenv("REG_DEFS_PATH"), + "ENV_INVENTORY_CONTENT": getenv("ENV_INVENTORY_CONTENT"), } class PipelineParametersHandler: diff --git a/test_data/test_app_reg_defs/TC-001-001/expected/appdefs/application-1.yaml b/test_data/test_app_reg_defs/TC-001-001/expected/appdefs/application-1.yml similarity index 100% rename from test_data/test_app_reg_defs/TC-001-001/expected/appdefs/application-1.yaml rename to test_data/test_app_reg_defs/TC-001-001/expected/appdefs/application-1.yml diff --git a/test_data/test_app_reg_defs/TC-001-002/expected/regdefs/registry-1.yaml b/test_data/test_app_reg_defs/TC-001-002/expected/regdefs/registry-1.yml similarity index 100% rename from test_data/test_app_reg_defs/TC-001-002/expected/regdefs/registry-1.yaml rename to test_data/test_app_reg_defs/TC-001-002/expected/regdefs/registry-1.yml diff --git a/test_data/test_app_reg_defs/TC-001-003/expected/appdefs/application-1.yaml b/test_data/test_app_reg_defs/TC-001-003/expected/appdefs/application-1.yml similarity index 100% rename from test_data/test_app_reg_defs/TC-001-003/expected/appdefs/application-1.yaml rename to test_data/test_app_reg_defs/TC-001-003/expected/appdefs/application-1.yml diff --git a/test_data/test_app_reg_defs/TC-001-004/expected/regdefs/registry-1.yaml b/test_data/test_app_reg_defs/TC-001-004/expected/regdefs/registry-1.yml similarity index 95% rename from test_data/test_app_reg_defs/TC-001-004/expected/regdefs/registry-1.yaml rename to test_data/test_app_reg_defs/TC-001-004/expected/regdefs/registry-1.yml index 4f4b1d908..d75d7b480 100644 --- a/test_data/test_app_reg_defs/TC-001-004/expected/regdefs/registry-1.yaml +++ b/test_data/test_app_reg_defs/TC-001-004/expected/regdefs/registry-1.yml @@ -16,4 +16,4 @@ dockerConfig: snapshotRepoName: "docker-snapshot" stagingRepoName: "docker-staging" releaseRepoName: "docker-release" - groupName: "docker-group" \ No newline at end of file + groupName: "docker-group" diff --git a/test_data/test_app_reg_defs/TC-001-005/expected/appdefs/application-1.yaml b/test_data/test_app_reg_defs/TC-001-005/expected/appdefs/application-1.yml similarity index 100% rename from test_data/test_app_reg_defs/TC-001-005/expected/appdefs/application-1.yaml rename to test_data/test_app_reg_defs/TC-001-005/expected/appdefs/application-1.yml diff --git a/test_data/test_app_reg_defs/TC-001-006/expected/appdefs/application-1.yaml b/test_data/test_app_reg_defs/TC-001-006/expected/appdefs/application-1.yml similarity index 100% rename from test_data/test_app_reg_defs/TC-001-006/expected/appdefs/application-1.yaml rename to test_data/test_app_reg_defs/TC-001-006/expected/appdefs/application-1.yml diff --git a/test_data/test_app_reg_defs/TC-001-008/expected/appdefs/application-1.yaml b/test_data/test_app_reg_defs/TC-001-008/expected/appdefs/application-1.yml similarity index 100% rename from test_data/test_app_reg_defs/TC-001-008/expected/appdefs/application-1.yaml rename to test_data/test_app_reg_defs/TC-001-008/expected/appdefs/application-1.yml diff --git a/test_data/test_app_reg_defs/TC-001-008/expected/appdefs/application-2.yml.j2 b/test_data/test_app_reg_defs/TC-001-008/expected/appdefs/application-2.yml similarity index 100% rename from test_data/test_app_reg_defs/TC-001-008/expected/appdefs/application-2.yml.j2 rename to test_data/test_app_reg_defs/TC-001-008/expected/appdefs/application-2.yml diff --git a/test_data/test_app_reg_defs/TC-001-008/expected/appdefs/application-3.yml b/test_data/test_app_reg_defs/TC-001-008/expected/appdefs/application-3.yml index 53606066f..5aa758575 100644 --- a/test_data/test_app_reg_defs/TC-001-008/expected/appdefs/application-3.yml +++ b/test_data/test_app_reg_defs/TC-001-008/expected/appdefs/application-3.yml @@ -4,4 +4,4 @@ artifactId: "application-3" groupId: "org.qubership" supportParallelDeploy: true deployParameters: {} -technicalConfigurationParameters: {} \ No newline at end of file +technicalConfigurationParameters: {} From 05f11fe41b6abd13fe0e902c0b19aab8666c747c Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Wed, 4 Feb 2026 15:45:24 +0000 Subject: [PATCH 005/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 61069d6ce..a4ddb2165 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.21.3" - DOCKER_IMAGE_TAG_ENVGENE: "1.21.3" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.21.3" + DOCKER_IMAGE_TAG_PIPEGENE: "1.22.0" + DOCKER_IMAGE_TAG_ENVGENE: "1.22.0" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.22.0" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 6ea5346fe..ba04cdc11 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.21.3 +version: 1.22.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 766ad25b2..059947cb7 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.21.3 +version: 1.22.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index debea1fc7..dfb675b8e 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.21.3", + "envgene_version": "1.22.0", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 725bafd01383e2bfe763ceb60606baa0000ebd15 Mon Sep 17 00:00:00 2001 From: popoveugene <42543333+popoveugene@users.noreply.github.com> Date: Wed, 4 Feb 2026 21:02:03 +0300 Subject: [PATCH 006/161] docs: add metadata to env_definition schema (#994) --- schemas/env-definition.schema.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/schemas/env-definition.schema.json b/schemas/env-definition.schema.json index 7febd9a7f..7e4fbf01f 100644 --- a/schemas/env-definition.schema.json +++ b/schemas/env-definition.schema.json @@ -5,6 +5,12 @@ "description": "Configuration for the environment, Environment Inventory", "additionalProperties": true, "properties": { + "metadata": { + "type": "object", + "title": "Metadata", + "description": "Optional metadata map for the environment definition. Structure is not specified. Used only by Colly.", + "additionalProperties": true + }, "inventory": { "type": "object", "title": "Environment definition for Inventory", From 6f951956ed6dcb43bc27064ee7f45293ee5a2148 Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Thu, 5 Feb 2026 11:35:59 +0300 Subject: [PATCH 007/161] feat: Added new variable to EnvGene Instance pipeline - ENV_INVENTORY_CONTENT (#996) --- .../.github/workflows/Envgene.yml | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index a4ddb2165..9a9e44d83 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -81,13 +81,13 @@ jobs: outputs: env_matrix: ${{ steps.matrix-generator.outputs.env_matrix }} BG_MANAGE: ${{ env.BG_MANAGE }} - ENV_TEMPLATE_TEST: ${{ env.ENV_TEMPLATE_TEST }} - ENV_SPECIFIC_PARAMS: ${{ env.ENV_SPECIFIC_PARAMS }} - ENV_TEMPLATE_NAME: ${{ env.ENV_TEMPLATE_NAME }} + ENV_INVENTORY_CONTENT: ${{ env.ENV_INVENTORY_CONTENT }} CRED_ROTATION_PAYLOAD: ${{ env.CRED_ROTATION_PAYLOAD }} ENV_BUILDER: ${{ env.ENV_BUILDER }} GENERATE_EFFECTIVE_SET: ${{ env.GENERATE_EFFECTIVE_SET }} ENV_INVENTORY_INIT: ${{ env.ENV_INVENTORY_INIT }} + ENV_SPECIFIC_PARAMS: ${{ env.ENV_SPECIFIC_PARAMS }} + ENV_TEMPLATE_NAME: ${{ env.ENV_TEMPLATE_NAME }} steps: - name: Repository Checkout uses: actions/checkout@v4.1.0 @@ -213,7 +213,7 @@ jobs: ### ENV_INVENTORY_GENERATION ### - name: ENV_INVENTORY_GENERATION - if: needs.process_environment_variables.outputs.ENV_TEMPLATE_TEST == 'false' && (needs.process_environment_variables.outputs.ENV_SPECIFIC_PARAMS != '' || needs.process_environment_variables.outputs.ENV_TEMPLATE_NAME != '') + if: (needs.process_environment_variables.outputs.ENV_INVENTORY_CONTENT != '') || (needs.process_environment_variables.outputs.ENV_SPECIFIC_PARAMS != '' || needs.process_environment_variables.outputs.ENV_TEMPLATE_NAME != '') run: | docker run --rm -v ${{ github.workspace }}:${{ env.CI_PROJECT_DIR }} -w ${{ env.CI_PROJECT_DIR }} --env-file .env.container --user root -e HOME=/root \ ${{ env.DOCKER_IMAGE_NAME_ENVGENE }}:${{ env.DOCKER_IMAGE_TAG_ENVGENE }} \ @@ -229,7 +229,7 @@ jobs: - name: ENV_INVENTORY_GENERATION - Upload ENV_INVENTORY_GENERATION Package uses: actions/upload-artifact@v4 - if: needs.process_environment_variables.outputs.ENV_TEMPLATE_TEST == 'false' && (needs.process_environment_variables.outputs.ENV_SPECIFIC_PARAMS != '' || needs.process_environment_variables.outputs.ENV_TEMPLATE_NAME != '') + if: (needs.process_environment_variables.outputs.ENV_INVENTORY_CONTENT != '') || (needs.process_environment_variables.outputs.ENV_SPECIFIC_PARAMS != '' || needs.process_environment_variables.outputs.ENV_TEMPLATE_NAME != '') with: name: env_inventory_generation_${{ env.PACKAGE_NAME }} path: environments/${{ matrix.environment }} @@ -303,11 +303,6 @@ jobs: python /module/scripts/utils/log_pipe_params.py /module/scripts/handle_certs.sh cd /build_env; python3 /build_env/scripts/build_env/main.py - - if [ \"\$ENV_TEMPLATE_TEST\" == \"true\" ]; then - env_name=\$(cat set_variable.txt) - sed -i \"s|\\\\\\\"envgeneNullValue\\\\\\\"|\\\\\\\"test_value\\\\\\\"|g\" \"\${CI_PROJECT_DIR}/environments/\$env_name/Credentials/credentials.yml\" - fi " - name: ENV_BUILD - Upload Build Env Package From ab3786b734eb22cf2bc2c4d5e730429379d4cf46 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Thu, 5 Feb 2026 08:38:44 +0000 Subject: [PATCH 008/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 9a9e44d83..df9d14435 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.22.0" - DOCKER_IMAGE_TAG_ENVGENE: "1.22.0" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.22.0" + DOCKER_IMAGE_TAG_PIPEGENE: "1.22.1" + DOCKER_IMAGE_TAG_ENVGENE: "1.22.1" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.22.1" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index ba04cdc11..576d25701 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.22.0 +version: 1.22.1 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 059947cb7..fa0fd3b5c 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.22.0 +version: 1.22.1 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index dfb675b8e..57ae203ac 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.22.0", + "envgene_version": "1.22.1", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 13cc89774ec4d3c03b2cb658339fbde3694533df Mon Sep 17 00:00:00 2001 From: Dias <120464230+dysmon@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:19:24 +0500 Subject: [PATCH 009/161] fix: delete extra file (#997) --- scripts/build_env/handle_sd.py | 324 -------------------------------- scripts/build_env/process_sd.py | 7 - 2 files changed, 331 deletions(-) delete mode 100644 scripts/build_env/handle_sd.py diff --git a/scripts/build_env/handle_sd.py b/scripts/build_env/handle_sd.py deleted file mode 100644 index 8abcd73f6..000000000 --- a/scripts/build_env/handle_sd.py +++ /dev/null @@ -1,324 +0,0 @@ -import asyncio -import json -import os -from enum import Enum -from os import path, getenv -from pathlib import Path - -import envgenehelper as helper -import yaml -from artifact_searcher import artifact -from artifact_searcher.utils import models as artifact_models -from envgenehelper.business_helper import getenv_and_log, getenv_with_error -from envgenehelper.env_helper import Environment -from envgenehelper.file_helper import identify_yaml_extension -from envgenehelper.logger import logger -from envgenehelper.plugin_engine import PluginEngine -from envgenehelper.sd_merge_helper import basic_merge_multiple - - -class MergeType(Enum): - EXTENDED = "extended-merge" - REPLACE = "replace" - BASIC = "basic-merge" - BASIC_EXCLUSION = "basic-exclusion-merge" - - @classmethod - def from_value(cls, value: str): - if not isinstance(value, str): - raise ValueError(f"SD_REPO_MERGE_MODE value: '{value}' cannot be non-string") - value_lower = value.strip().lower() - for member in cls: - if member.value == value_lower: - return member - valid_values = [member.value for member in cls] - raise ValueError( - f"Invalid SD_REPO_MERGE_MODE: '{value}'. Valid values are: {valid_values}" - ) - - -MERGE_METHODS = { - MergeType.BASIC: helper.basic_merge, - MergeType.BASIC_EXCLUSION: helper.basic_exclusion_merge, - MergeType.EXTENDED: helper.extended_merge -} - -ENVIRONMENT_NAME = getenv_with_error('ENVIRONMENT_NAME') -CLUSTER_NAME = getenv_with_error('CLUSTER_NAME') -WORK_DIR = getenv_with_error('CI_PROJECT_DIR') -BASE_ENV_PATH = f"{WORK_DIR}/environments/{CLUSTER_NAME}/{ENVIRONMENT_NAME}" -APP_DEFS_PATH = f"{BASE_ENV_PATH}/AppDefs" -REG_DEFS_PATH = f"{BASE_ENV_PATH}/RegDefs" - - -def handle_deploy_postfix_namespace_transformation(sd_data: dict, namespace_dict: dict) -> dict: - """ - Transforms the SD data before writing: - - If userData.useDeployPostfixAsNamespace == True: - - Replace deployPostfix with corresponding folder name from namespace_dict if exists. - - If userData contains ONLY field useDeployPostfixAsNamespace, remove userData. - - If other keys exist, remove only useDeployPostfixAsNamespace. - """ - logger.info( - f"[Pre handle_deploy_postfix_namespace_transformation] Original SD data: {json.dumps(sd_data, indent=2)}") - user_data = sd_data.get("userData", {}) - - if isinstance(user_data, dict) and user_data.get("useDeployPostfixAsNamespace") is True: - for app in sd_data.get("applications", []): - if "deployPostfix" in app and isinstance(app["deployPostfix"], str): - current_postfix = app["deployPostfix"] - replacement = namespace_dict.get(current_postfix) - if replacement: - logger.info(f"Replacing deployPostfix '{current_postfix}' with '{replacement}'") - app["deployPostfix"] = replacement - else: - logger.error(f"No replacement found for deployPostfix '{current_postfix}', cannot continue.") - exit(1) - # Remove entire userData if it has only one key - if len(user_data) == 1: - sd_data.pop("userData", None) - else: - user_data.pop("useDeployPostfixAsNamespace", None) - sd_data["userData"] = user_data # Reassign to make sure it's updated - - return sd_data - - -def prepare_vars_and_run_sd_handling(): - base_dir = getenv_and_log('CI_PROJECT_DIR') - env_name = getenv_and_log('ENV_NAME') - cluster = getenv_and_log('CLUSTER_NAME') - - env = Environment(base_dir, cluster, env_name) - - sd_source_type = getenv('SD_SOURCE_TYPE') - sd_version = getenv('SD_VERSION') - sd_data = getenv('SD_DATA') - sd_delta = getenv('SD_DELTA') - sd_merge_mode = getenv("SD_REPO_MERGE_MODE") - handle_sd(env, sd_source_type, sd_version, sd_data, sd_delta, sd_merge_mode) - - -def build_namespace_dict(env) -> dict: - namespaces_dir = f'{env.env_path}/Namespaces/' - result = {} - - if not os.path.exists(namespaces_dir): - logger.warning(f"Namespaces directory does not exist: {namespaces_dir}") - return result # Return empty dict instead of throwing an error - - # Iterate over all items in Namespaces directory - for folder_name in os.listdir(namespaces_dir): - folder_path = os.path.join(namespaces_dir, folder_name) - if os.path.isdir(folder_path): - namespace_file = os.path.join(folder_path, "namespace.yml") - if os.path.isfile(namespace_file): - with open(namespace_file, 'r') as f: - data = yaml.safe_load(f) - logger.info(f"Parsed content of {namespace_file}: {data}") - # Extract 'name' property - ns_name = data.get("name") - logger.info(f"ns_name = {ns_name}") - if ns_name and isinstance(ns_name, str): - result[ns_name] = folder_name - else: - logger.warning(f"Warning: 'name' property missing or invalid in {namespace_file}") - else: - continue - logger.info(f"Namespace dict built: {result}") - return result - - -def merge_sd(sd_path: Path, sd_data, merge_func): - logger.info(f"Final destination! - {sd_path}") - full_sd_yaml = helper.openYaml(sd_path) - logger.info(f"full_sd.yaml before merge: {full_sd_yaml}") - helper.check_dir_exist_and_create(sd_path.parent) - result = merge_func(full_sd_yaml, sd_data) - helper.writeYamlToFile(sd_path, result) - logger.info(f"Merged data into Target Path! - {result}") - - -def calculate_merge_mode(sd_merge_mode, sd_delta) -> MergeType: - if sd_merge_mode is not None: - effective_merge_mode = MergeType.from_value(sd_merge_mode) - elif sd_delta == "true": - effective_merge_mode = MergeType.EXTENDED - logger.info( - f"SD_REPO_MERGE_MODE not passed. Calculated based on SD_DELTA={sd_delta}: {effective_merge_mode.value}") - elif sd_delta == "false": - effective_merge_mode = MergeType.REPLACE - logger.info( - f"SD_REPO_MERGE_MODE not passed. Calculated based on SD_DELTA={sd_delta}: {effective_merge_mode.value}") - else: - effective_merge_mode = MergeType.BASIC - logger.info(f"SD_REPO_MERGE_MODE not passed. Default value: {effective_merge_mode.value}") - return effective_merge_mode - - -def calculate_sd_delta(sd_delta): - logger.info(f"printing sd_delta before {sd_delta}") - if sd_delta is not None and str(sd_delta).strip() != "": - sd_delta = str(sd_delta).strip().lower() - else: - sd_delta = None - logger.info(f"printing sd_delta after {sd_delta}") - return sd_delta - - -def multiply_sds_to_single(sds_data, effective_merge_mode): - if effective_merge_mode == MergeType.EXTENDED: - if isinstance(sds_data, list): - if len(sds_data) > 1: - raise ValueError("Multiple SDs not supported in extended merge mode") - full_sd_from_pipe = sds_data[0] - elif isinstance(sds_data, dict): - full_sd_from_pipe = sds_data - else: - sds_data = sds_data if isinstance(sds_data, list) else [sds_data] - cropped_sds = [] - for sd in sds_data: - cropped_sds.append({"applications": sd["applications"]}) - - full_sd_from_pipe = basic_merge_multiple(cropped_sds) - - logger.info(f"Merged data after performing basic-merge for multiple SDs: {full_sd_from_pipe}") - return full_sd_from_pipe - - -def handle_sd(env, sd_source_type, sd_version, sd_data, sd_delta, sd_merge_mode): - base_sd_path = Path(f'{env.env_path}/Inventory/solution-descriptor/') - - sd_delta = calculate_sd_delta(sd_delta) - effective_merge_mode = calculate_merge_mode(sd_merge_mode, sd_delta) - - helper.check_dir_exist_and_create(base_sd_path) - if sd_source_type == "artifact": - download_sds_with_version(env, base_sd_path, sd_version, effective_merge_mode) - elif sd_source_type == "json": - extract_sds_from_json(env, base_sd_path, sd_data, effective_merge_mode) - else: - logger.error('SD_SOURCE_TYPE must be set either to "artifact" or "json"') - exit(1) - - -def validate_applications(sd, effective_merge_mode: MergeType): - applications = sd.get("applications") - for app in applications: - if effective_merge_mode != MergeType.EXTENDED and (not isinstance(app, dict) or not app.get("deployPostfix")): - raise ValueError( - f"Application {app} doesn't have deployPostfix. : notation is supported only for " - f"extended merge. Current merge mode: {effective_merge_mode.value}") - - -def extract_sds_from_json(env, base_sd_path: Path, sd_data, effective_merge_mode: MergeType): - if not sd_data: - logger.error("SD_SOURCE_TYPE is set to 'json', but SD_DATA was not given in pipeline variables") - exit(1) - sds_from_pipe = json.loads(sd_data) - - logger.info(f"printing data inside extract_sd_from_json {sds_from_pipe}") - if not isinstance(sds_from_pipe, (list, dict)) or not sds_from_pipe: - logger.error("SD_DATA must be a non-empty list of SD dictionaries or a single SD.") - exit(1) - - # Build namespace mapping and transform each SD before any operations - namespace_dict = build_namespace_dict(env) - - # Transform each SD item before processing - if isinstance(sds_from_pipe, list): - transformed_data = [] - for item in sds_from_pipe: - transformed_item = handle_deploy_postfix_namespace_transformation(item, namespace_dict) - transformed_data.append(transformed_item) - else: - transformed_data = handle_deploy_postfix_namespace_transformation(sds_from_pipe, namespace_dict) - full_sd_from_pipe = multiply_sds_to_single(transformed_data, effective_merge_mode) - validate_applications(full_sd_from_pipe, effective_merge_mode) - - sd_path = base_sd_path.joinpath("sd.yaml") - sd_delta_path = base_sd_path.joinpath("delta_sd.yaml") - if effective_merge_mode == MergeType.REPLACE: - logger.info("Inside replace") - if helper.check_file_exists(sd_path): - full_sd_yaml = helper.openYaml(sd_path) - logger.info(f"full_sd.yaml before replacement: {json.dumps(full_sd_yaml, indent=2)}") - else: - logger.info("No existing SD found at destination. Proceeding to write new SD.") - helper.check_dir_exist_and_create(path.dirname(sd_path)) - helper.writeYamlToFile(sd_path, full_sd_from_pipe) - if helper.check_file_exists(sd_delta_path): - helper.deleteFile(sd_delta_path) - logger.info(f"Replaced existing SD with new data at: {sd_path}") - else: - if not helper.check_file_exists(sd_path): - helper.writeYamlToFile(sd_path, full_sd_from_pipe) - else: - helper.writeYamlToFile(sd_delta_path, full_sd_from_pipe) - # Call merge_sd with correct merge function - selected_merge_function = MERGE_METHODS.get(effective_merge_mode) - if not selected_merge_function: - raise ValueError(f"Unsupported merge mode: {effective_merge_mode}") - merge_sd(sd_path, full_sd_from_pipe, selected_merge_function) - - logger.info("SD successfully extracted from SD_DATA and Saved.") - - -def download_sds_with_version(env, base_sd_path, sd_version, effective_merge_mode: MergeType): - logger.info(f"sd_version: {sd_version}") - if not sd_version: - logger.error("SD_SOURCE_TYPE is set to 'artifact', but SD_VERSION was not given in pipeline variables") - exit(1) - sd_version = sd_version.replace("\\n", "\n") - sd_entries = [line.strip() for line in sd_version.strip().splitlines() if line.strip()] - if not sd_entries: - logger.error("No valid SD versions found in SD_VERSION") - exit(1) - - app_def_getter_plugins = PluginEngine(plugins_dir='/module/scripts/handle_sd_plugins/app_def_getter') - sd_data_list = [] - for entry in sd_entries: # appvers - if ":" not in entry: - logger.error(f"Invalid SD_VERSION format: '{entry}'. Expected 'name:version'") - exit(1) - - source_name, version = entry.split(":", 1) - logger.info(f"Starting download of SD: {source_name}-{version}") - - sd_data = download_sd_by_appver(source_name, version, app_def_getter_plugins) - - sd_data_list.append(sd_data) - - sd_data_json = json.dumps(sd_data_list) - extract_sds_from_json(env, base_sd_path, sd_data_json, effective_merge_mode) - - -def download_sd_by_appver(app_name: str, version: str, plugins: PluginEngine) -> dict[str, object]: - if 'SNAPSHOT' in version: - raise ValueError("SNAPSHOT is not supported version of Solution Descriptor artifacts") - # TODO: check if job would fail without plugins - app_def = get_appdef_for_app(f"{app_name}:{version}", app_name, plugins) - - artifact_info = asyncio.run(artifact.check_artifact_async(app_def, artifact.FileExtension.JSON, version)) - if not artifact_info: - raise ValueError( - f'Solution descriptor content was not received for {app_name}:{version}') - sd_url, _ = artifact_info - return artifact.download_json_content(sd_url) - - -def get_appdef_for_app(appver: str, app_name: str, plugins: PluginEngine) -> artifact_models.Application: - results = plugins.run(appver=appver) - for result in results: - if result is not None: - return result - app_def_path = identify_yaml_extension(f"{APP_DEFS_PATH}/{app_name}") - app_dict = helper.openYaml(app_def_path) - reg_def_path = identify_yaml_extension(f"{REG_DEFS_PATH}/{app_dict['registryName']}") - app_dict['registry'] = artifact_models.Registry.model_validate(helper.openYaml(reg_def_path)) - app_def = artifact_models.Application.model_validate(app_dict) - return app_def - - -if __name__ == "__main__": - prepare_vars_and_run_sd_handling() diff --git a/scripts/build_env/process_sd.py b/scripts/build_env/process_sd.py index 6602663d6..8abcd73f6 100644 --- a/scripts/build_env/process_sd.py +++ b/scripts/build_env/process_sd.py @@ -188,13 +188,6 @@ def multiply_sds_to_single(sds_data, effective_merge_mode): def handle_sd(env, sd_source_type, sd_version, sd_data, sd_delta, sd_merge_mode): base_sd_path = Path(f'{env.env_path}/Inventory/solution-descriptor/') - handle_sd_skip_msg = "SD_SOURCE_TYPE is not specified, skipping SD file creation" - if not sd_source_type: - if not sd_version and not sd_data: - logger.info(handle_sd_skip_msg) - else: - logger.warning(handle_sd_skip_msg) - return sd_delta = calculate_sd_delta(sd_delta) effective_merge_mode = calculate_merge_mode(sd_merge_mode, sd_delta) From 2d4e104b1d2e48df8b57825aef7044770e074eff Mon Sep 17 00:00:00 2001 From: basudev91 Date: Thu, 5 Feb 2026 20:38:02 +0530 Subject: [PATCH 010/161] docs: update documents to support multi-value delimiters in pipeline parameters (#794) * docs: updated document for support multi value delimiters * docs: added usecase document for mutli value delimiter * docs: fixed diagram render issue * docs: added GITHUB instance usecases for multi values params * docs: updates --------- Co-authored-by: popoveugene --- docs/instance-pipeline-parameters.md | 48 +++- .../environment-instance-generation.md | 64 +++++ docs/use-cases/sd-processing.md | 244 +++++++++++++++++- 3 files changed, 341 insertions(+), 15 deletions(-) diff --git a/docs/instance-pipeline-parameters.md b/docs/instance-pipeline-parameters.md index 308f57f41..bc513b422 100644 --- a/docs/instance-pipeline-parameters.md +++ b/docs/instance-pipeline-parameters.md @@ -32,6 +32,7 @@ - [Deprecated Parameters](#deprecated-parameters) - [`SD_DELTA`](#sd_delta) - [Archived Parameters](#archived-parameters) + - [Multiple Values Support](#multiple-values-support) The following are the launch parameters for the instance repository pipeline. These parameters influence, the execution of specific jobs within the pipeline. @@ -41,7 +42,10 @@ All parameters are of the string data type ### `ENV_NAMES` -**Description**: Specifies the environment(s) for which processing will be triggered. Uses the `/` notation. If multiple environments are provided, they must be separated by a `\n` (newline) delimiter. In multi-environment case, each environment will trigger its own independent pipeline flow. All environments will use the same set of pipeline parameters (as documented in this spec) +**Description**: Specifies the environment(s) for which processing will be triggered. Uses the `/` notation. + +If specifying more than one environment, separate them as described in [Multiple Values Support](#multiple-values-support). +For multiple environments, each environment will initiate its own independent pipeline flow, using the same set of pipeline parameters for all. **Default Value**: None @@ -50,7 +54,11 @@ All parameters are of the string data type **Example**: - Single environment: `ocp-01/platform` -- Multiple environments (separated by \n) `k8s-01/env-1\nk8s-01/env2` +- Multiple environments: + - `k8s-01/env-1\nk8s-01/env2` + - `k8s-01/env-1;k8s-01/env2` + - `k8s-01/env-1,k8s-01/env2` + - `k8s-01/env-1 k8s-01/env2` ### `ENV_BUILDER` @@ -341,7 +349,9 @@ See details in [SD processing](/docs/features/sd-processing.md) ### `SD_VERSION` -**Description**: Specifies one or more SD artifacts in `application:version` notation passed via a `\n` separator. +**Description**: Specifies one or more SD artifacts in `application:version` notation. + +If specifying more than one environment, separate them as described in [Multiple Values Support](#multiple-values-support). EnvGene downloads and sequentially merges them in the `basic-merge` mode, where subsequent `application:version` takes priority over the previous one. Optionally saves the result to [Delta SD](/docs/features/sd-processing.md#delta-sd), then merges with [Full SD](/docs/features/sd-processing.md#full-sd) using `SD_REPO_MERGE_MODE` merge mode @@ -354,7 +364,11 @@ See details in [SD processing](/docs/features/sd-processing.md) **Example**: - Single SD: `MONITORING:0.64.1` -- Multiple SD (separated by \n) `solution-part-1:0.64.2\nsolution-part-2:0.44.1` +- Multiple SDs: + - `solution-part-1:0.64.2\nsolution-part-2:0.44.1` + - `solution-part-1:0.64.2;solution-part-2:0.44.1` + - `solution-part-1:0.64.2,solution-part-2:0.44.1` + - `solution-part-1:0.64.2 solution-part-2:0.44.1` ### `SD_DATA` @@ -619,3 +633,29 @@ See details in [SD processing](/docs/features/sd-processing.md) ## Archived Parameters These parameters are no longer in use and are maintained for historical reference + +## Multiple Values Support + +Some pipeline parameters support multiple values. +Values can be separated using one of the following delimiters: + +- Newline (`\n`) +- Semicolon (`;`) +- Comma (`,`) +- Space (` `) + +**Example:** + +```text +# Using newline +k8s-01/env-1\nk8s-01/env-2 + +# Using comma +k8s-01/env-1,k8s-01/env-2 + +# Using semicolon +k8s-01/env-1;k8s-01/env-2 + +# Using space +k8s-01/env-1 k8s-01/env-2 +``` diff --git a/docs/use-cases/environment-instance-generation.md b/docs/use-cases/environment-instance-generation.md index 43c91f22d..f70a039a8 100644 --- a/docs/use-cases/environment-instance-generation.md +++ b/docs/use-cases/environment-instance-generation.md @@ -15,6 +15,8 @@ - [UC-EIG-TA-1: Environment Instance Generation with `artifact` only](#uc-eig-ta-1-environment-instance-generation-with-artifact-only) - [UC-EIG-TA-2: Environment Instance Generation with `artifact` and `bgNsArtifacts` and BG Domain](#uc-eig-ta-2-environment-instance-generation-with-artifact-and-bgnsartifacts-and-bg-domain) - [UC-EIG-TA-3: Environment Instance Generation with `artifact` and `bgNsArtifacts` and without BG Domain](#uc-eig-ta-3-environment-instance-generation-with-artifact-and-bgnsartifacts-and-without-bg-domain) + - [Multiple Environments Processing](#multiple-environments-processing) + - [UC-EIG-ME-1: Parallel Environment Instance Generation for Multiple Environments](#uc-eig-me-1-parallel-environment-instance-generation-for-multiple-environments) ## Overview @@ -447,3 +449,65 @@ Instance pipeline (GitLab or GitHub) is started with parameters: 1. All Namespaces are rendered using `project-env-template:v1.2.3` 2. All other objects (Tenant, Cloud, Applications, etc.) are rendered using `project-env-template:v1.2.3` 3. `bgNsArtifacts` are ignored since BG Domain is absent + +## Multiple Environments Processing + +When multiple environments are specified in `ENV_NAMES`, the pipeline processes them in parallel. Each environment triggers an independent pipeline flow with the same set of pipeline parameters. This section describes how Environment Instance generation works for multiple environments. + +### UC-EIG-ME-1: Parallel Environment Instance Generation for Multiple Environments + +**Pre-requisites:** + +1. Multiple Environment Inventories exist +2. Each Environment Inventory contains valid Environment Template configuration +3. Template artifacts are available for all environments + +**Trigger:** + +> [!Note] +> One of the following conditions must be met. + +1. GitLab Instance pipeline is started with parameters: + 1. `ENV_NAMES: \n` + 2. `ENV_BUILDER: true` + + Or using semicolon separator: + 1. `ENV_NAMES: ;` + 2. `ENV_BUILDER: true` + + Or using comma separator: + 1. `ENV_NAMES: ,` + 2. `ENV_BUILDER: true` + + Or using space separator: + 1. `ENV_NAMES: ` + 2. `ENV_BUILDER: true` +2. GitHub Instance pipeline is started with parameters: + 1. `ENV_NAMES: \n` + 2. `ENV_BUILDER: true` + + Or using semicolon separator: + 1. `ENV_NAMES: ;` + 2. `ENV_BUILDER: true` + + Or using comma separator: + 1. `ENV_NAMES: ,` + 2. `ENV_BUILDER: true` + + Or using space separator: + 1. `ENV_NAMES: ` + 2. `ENV_BUILDER: true` + +**Steps:** + +1. For each environment from the list, parallel and independent pipeline flows are started +2. For each environment, the `env_builder` job runs in parallel: + 1. Reads Environment Inventory for the specific environment + 2. Generates Environment Instance objects according to the template configuration + +**Results:** + +1. Environment Instance for each environment from `ENV_NAMES` is generated and saved to `/environments///` +2. All Environment Instances are generated in parallel, independently of each other +3. Each Environment Instance generation uses the same pipeline parameters but processes its own Environment Inventory +4. If one environment generation fails, other environments continue processing independently diff --git a/docs/use-cases/sd-processing.md b/docs/use-cases/sd-processing.md index 34b5bbe01..832b43013 100644 --- a/docs/use-cases/sd-processing.md +++ b/docs/use-cases/sd-processing.md @@ -329,25 +329,71 @@ The SD processing logic depends on: **Trigger:** > [!Note] -> One of the following conditions must be met: +> One of the following conditions must be met. 1. GitLab Instance pipeline is started with parameters: 1. `ENV_NAMES: ` 2. `SD_VERSION: \n` - Or with explicit parameters: + Or using semicolon separator: + 1. `ENV_NAMES: ` + 2. `SD_VERSION: ;` + + Or using comma separator: + 1. `ENV_NAMES: ` + 2. `SD_VERSION: ,` + + Or using space separator: + 1. `ENV_NAMES: ` + 2. `SD_VERSION: ` + + Or with explicit parameters (using newline): 1. `ENV_NAMES: ` 2. `SD_SOURCE_TYPE: artifact` 3. `SD_VERSION: \n` 4. `SD_REPO_MERGE_MODE: basic-merge` + + Or with explicit parameters (using semicolon): + 1. `ENV_NAMES: ` + 2. `SD_SOURCE_TYPE: artifact` + 3. `SD_VERSION: ;` + 4. `SD_REPO_MERGE_MODE: basic-merge` + + Or with explicit parameters (using comma): + 1. `ENV_NAMES: ` + 2. `SD_SOURCE_TYPE: artifact` + 3. `SD_VERSION: ,` + 4. `SD_REPO_MERGE_MODE: basic-merge` + + Or with explicit parameters (using space): + 1. `ENV_NAMES: ` + 2. `SD_SOURCE_TYPE: artifact` + 3. `SD_VERSION: ` + 4. `SD_REPO_MERGE_MODE: basic-merge` 2. GitHub Instance pipeline is started with parameters: 1. `ENV_NAMES: ` 2. `GH_ADDITIONAL_PARAMS: "SD_VERSION=\\n"` - Or with explicit parameters: + Or using comma separator: + 1. `ENV_NAMES: ` + 2. `GH_ADDITIONAL_PARAMS: "SD_VERSION=,"` + + Or using space separator: + 1. `ENV_NAMES: ` + 2. `GH_ADDITIONAL_PARAMS: "SD_VERSION= "` + + Or with explicit parameters (using newline): 1. `ENV_NAMES: ` 2. `GH_ADDITIONAL_PARAMS: "SD_SOURCE_TYPE=artifact,SD_VERSION=\\n,SD_REPO_MERGE_MODE=basic-merge"` + Or with explicit parameters (using comma): + 1. `ENV_NAMES: ` + 2. `GH_ADDITIONAL_PARAMS: "SD_SOURCE_TYPE=artifact,SD_VERSION=,,SD_REPO_MERGE_MODE=basic-merge"` + + Or with explicit parameters (using space): + 1. `ENV_NAMES: ` + 2. `GH_ADDITIONAL_PARAMS: "SD_SOURCE_TYPE=artifact,SD_VERSION= ,SD_REPO_MERGE_MODE=basic-merge"` + **Steps:** 1. The `process_sd` job runs in the pipeline: @@ -375,25 +421,71 @@ The SD processing logic depends on: **Trigger:** > [!Note] -> One of the following conditions must be met: +> One of the following conditions must be met. 1. GitLab Instance pipeline is started with parameters: 1. `ENV_NAMES: ` 2. `SD_VERSION: \n` - Or with explicit parameters: + Or using semicolon separator: + 1. `ENV_NAMES: ` + 2. `SD_VERSION: ;` + + Or using comma separator: + 1. `ENV_NAMES: ` + 2. `SD_VERSION: ,` + + Or using space separator: + 1. `ENV_NAMES: ` + 2. `SD_VERSION: ` + + Or with explicit parameters (using newline): 1. `ENV_NAMES: ` 2. `SD_SOURCE_TYPE: artifact` 3. `SD_VERSION: \n` 4. `SD_REPO_MERGE_MODE: basic-merge` + + Or with explicit parameters (using semicolon): + 1. `ENV_NAMES: ` + 2. `SD_SOURCE_TYPE: artifact` + 3. `SD_VERSION: ;` + 4. `SD_REPO_MERGE_MODE: basic-merge` + + Or with explicit parameters (using comma): + 1. `ENV_NAMES: ` + 2. `SD_SOURCE_TYPE: artifact` + 3. `SD_VERSION: ,` + 4. `SD_REPO_MERGE_MODE: basic-merge` + + Or with explicit parameters (using space): + 1. `ENV_NAMES: ` + 2. `SD_SOURCE_TYPE: artifact` + 3. `SD_VERSION: ` + 4. `SD_REPO_MERGE_MODE: basic-merge` 2. GitHub Instance pipeline is started with parameters: 1. `ENV_NAMES: ` 2. `GH_ADDITIONAL_PARAMS: "SD_VERSION=\\n"` - Or with explicit parameters: + Or using comma separator: + 1. `ENV_NAMES: ` + 2. `GH_ADDITIONAL_PARAMS: "SD_VERSION=,"` + + Or using space separator: + 1. `ENV_NAMES: ` + 2. `GH_ADDITIONAL_PARAMS: "SD_VERSION= "` + + Or with explicit parameters (using newline): 1. `ENV_NAMES: ` 2. `GH_ADDITIONAL_PARAMS: "SD_SOURCE_TYPE=artifact,SD_VERSION=\\n,SD_REPO_MERGE_MODE=basic-merge"` + Or with explicit parameters (using comma): + 1. `ENV_NAMES: ` + 2. `GH_ADDITIONAL_PARAMS: "SD_SOURCE_TYPE=artifact,SD_VERSION=,,SD_REPO_MERGE_MODE=basic-merge"` + + Or with explicit parameters (using space): + 1. `ENV_NAMES: ` + 2. `GH_ADDITIONAL_PARAMS: "SD_SOURCE_TYPE=artifact,SD_VERSION= ,SD_REPO_MERGE_MODE=basic-merge"` + **Steps:** 1. The `process_sd` job runs in the pipeline: @@ -420,17 +512,43 @@ The SD processing logic depends on: **Trigger:** > [!Note] -> One of the following conditions must be met: +> One of the following conditions must be met. 1. GitLab Instance pipeline is started with parameters: 1. `ENV_NAMES: ` 2. `SD_SOURCE_TYPE: artifact` 3. `SD_VERSION: \n` 4. `SD_REPO_MERGE_MODE: basic-exclusion-merge` + + Or using semicolon separator: + 1. `ENV_NAMES: ` + 2. `SD_SOURCE_TYPE: artifact` + 3. `SD_VERSION: ;` + 4. `SD_REPO_MERGE_MODE: basic-exclusion-merge` + + Or using comma separator: + 1. `ENV_NAMES: ` + 2. `SD_SOURCE_TYPE: artifact` + 3. `SD_VERSION: ,` + 4. `SD_REPO_MERGE_MODE: basic-exclusion-merge` + + Or using space separator: + 1. `ENV_NAMES: ` + 2. `SD_SOURCE_TYPE: artifact` + 3. `SD_VERSION: ` + 4. `SD_REPO_MERGE_MODE: basic-exclusion-merge` 2. GitHub Instance pipeline is started with parameters: 1. `ENV_NAMES: ` 2. `GH_ADDITIONAL_PARAMS: "SD_SOURCE_TYPE=artifact,SD_VERSION=\\n,SD_REPO_MERGE_MODE=basic-exclusion-merge"` + Or using comma separator: + 1. `ENV_NAMES: ` + 2. `GH_ADDITIONAL_PARAMS: "SD_SOURCE_TYPE=artifact,SD_VERSION=,,SD_REPO_MERGE_MODE=basic-exclusion-merge"` + + Or using space separator: + 1. `ENV_NAMES: ` + 2. `GH_ADDITIONAL_PARAMS: "SD_SOURCE_TYPE=artifact,SD_VERSION= ,SD_REPO_MERGE_MODE=basic-exclusion-merge"` + **Steps:** 1. The `process_sd` job runs in the pipeline: @@ -458,17 +576,43 @@ The SD processing logic depends on: **Trigger:** > [!Note] -> One of the following conditions must be met: +> One of the following conditions must be met. 1. GitLab Instance pipeline is started with parameters: 1. `ENV_NAMES: ` 2. `SD_SOURCE_TYPE: artifact` 3. `SD_VERSION: \n` 4. `SD_REPO_MERGE_MODE: basic-exclusion-merge` + + Or using semicolon separator: + 1. `ENV_NAMES: ` + 2. `SD_SOURCE_TYPE: artifact` + 3. `SD_VERSION: ;` + 4. `SD_REPO_MERGE_MODE: basic-exclusion-merge` + + Or using comma separator: + 1. `ENV_NAMES: ` + 2. `SD_SOURCE_TYPE: artifact` + 3. `SD_VERSION: ,` + 4. `SD_REPO_MERGE_MODE: basic-exclusion-merge` + + Or using space separator: + 1. `ENV_NAMES: ` + 2. `SD_SOURCE_TYPE: artifact` + 3. `SD_VERSION: ` + 4. `SD_REPO_MERGE_MODE: basic-exclusion-merge` 2. GitHub Instance pipeline is started with parameters: 1. `ENV_NAMES: ` 2. `GH_ADDITIONAL_PARAMS: "SD_SOURCE_TYPE=artifact,SD_VERSION=\\n,SD_REPO_MERGE_MODE=basic-exclusion-merge"` + Or using comma separator: + 1. `ENV_NAMES: ` + 2. `GH_ADDITIONAL_PARAMS: "SD_SOURCE_TYPE=artifact,SD_VERSION=,,SD_REPO_MERGE_MODE=basic-exclusion-merge"` + + Or using space separator: + 1. `ENV_NAMES: ` + 2. `GH_ADDITIONAL_PARAMS: "SD_SOURCE_TYPE=artifact,SD_VERSION= ,SD_REPO_MERGE_MODE=basic-exclusion-merge"` + **Steps:** 1. The `process_sd` job runs in the pipeline: @@ -495,17 +639,43 @@ The SD processing logic depends on: **Trigger:** > [!Note] -> One of the following conditions must be met: +> One of the following conditions must be met. Multiple SDs with `extended-merge` mode are not supported and will result in an error. 1. GitLab Instance pipeline is started with parameters: 1. `ENV_NAMES: ` 2. `SD_SOURCE_TYPE: artifact` 3. `SD_VERSION: \n` 4. `SD_REPO_MERGE_MODE: extended-merge` + + Or using semicolon separator: + 1. `ENV_NAMES: ` + 2. `SD_SOURCE_TYPE: artifact` + 3. `SD_VERSION: ;` + 4. `SD_REPO_MERGE_MODE: extended-merge` + + Or using comma separator: + 1. `ENV_NAMES: ` + 2. `SD_SOURCE_TYPE: artifact` + 3. `SD_VERSION: ,` + 4. `SD_REPO_MERGE_MODE: extended-merge` + + Or using space separator: + 1. `ENV_NAMES: ` + 2. `SD_SOURCE_TYPE: artifact` + 3. `SD_VERSION: ` + 4. `SD_REPO_MERGE_MODE: extended-merge` 2. GitHub Instance pipeline is started with parameters: 1. `ENV_NAMES: ` 2. `GH_ADDITIONAL_PARAMS: "SD_SOURCE_TYPE=artifact,SD_VERSION=\\n,SD_REPO_MERGE_MODE=extended-merge"` + Or using comma separator: + 1. `ENV_NAMES: ` + 2. `GH_ADDITIONAL_PARAMS: "SD_SOURCE_TYPE=artifact,SD_VERSION=,,SD_REPO_MERGE_MODE=extended-merge"` + + Or using space separator: + 1. `ENV_NAMES: ` + 2. `GH_ADDITIONAL_PARAMS: "SD_SOURCE_TYPE=artifact,SD_VERSION= ,SD_REPO_MERGE_MODE=extended-merge"` + **Steps:** 1. The `process_sd` job runs in the pipeline: @@ -534,17 +704,43 @@ The SD processing logic depends on: **Trigger:** > [!Note] -> One of the following conditions must be met: +> One of the following conditions must be met. Multiple SDs with `extended-merge` mode are not supported and will result in an error. 1. GitLab Instance pipeline is started with parameters: 1. `ENV_NAMES: ` 2. `SD_SOURCE_TYPE: artifact` 3. `SD_VERSION: \n` 4. `SD_REPO_MERGE_MODE: extended-merge` + + Or using semicolon separator: + 1. `ENV_NAMES: ` + 2. `SD_SOURCE_TYPE: artifact` + 3. `SD_VERSION: ;` + 4. `SD_REPO_MERGE_MODE: extended-merge` + + Or using comma separator: + 1. `ENV_NAMES: ` + 2. `SD_SOURCE_TYPE: artifact` + 3. `SD_VERSION: ,` + 4. `SD_REPO_MERGE_MODE: extended-merge` + + Or using space separator: + 1. `ENV_NAMES: ` + 2. `SD_SOURCE_TYPE: artifact` + 3. `SD_VERSION: ` + 4. `SD_REPO_MERGE_MODE: extended-merge` 2. GitHub Instance pipeline is started with parameters: 1. `ENV_NAMES: ` 2. `GH_ADDITIONAL_PARAMS: "SD_SOURCE_TYPE=artifact,SD_VERSION=\\n,SD_REPO_MERGE_MODE=extended-merge"` + Or using comma separator: + 1. `ENV_NAMES: ` + 2. `GH_ADDITIONAL_PARAMS: "SD_SOURCE_TYPE=artifact,SD_VERSION=,,SD_REPO_MERGE_MODE=extended-merge"` + + Or using space separator: + 1. `ENV_NAMES: ` + 2. `GH_ADDITIONAL_PARAMS: "SD_SOURCE_TYPE=artifact,SD_VERSION= ,SD_REPO_MERGE_MODE=extended-merge"` + **Steps:** 1. The `process_sd` job runs in the pipeline: @@ -570,17 +766,43 @@ The SD processing logic depends on: **Trigger:** > [!Note] -> One of the following conditions must be met: +> One of the following conditions must be met. 1. GitLab Instance pipeline is started with parameters: 1. `ENV_NAMES: ` 2. `SD_SOURCE_TYPE: artifact` 3. `SD_VERSION: \n` 4. `SD_REPO_MERGE_MODE: replace` + + Or using semicolon separator: + 1. `ENV_NAMES: ` + 2. `SD_SOURCE_TYPE: artifact` + 3. `SD_VERSION: ;` + 4. `SD_REPO_MERGE_MODE: replace` + + Or using comma separator: + 1. `ENV_NAMES: ` + 2. `SD_SOURCE_TYPE: artifact` + 3. `SD_VERSION: ,` + 4. `SD_REPO_MERGE_MODE: replace` + + Or using space separator: + 1. `ENV_NAMES: ` + 2. `SD_SOURCE_TYPE: artifact` + 3. `SD_VERSION: ` + 4. `SD_REPO_MERGE_MODE: replace` 2. GitHub Instance pipeline is started with parameters: 1. `ENV_NAMES: ` 2. `GH_ADDITIONAL_PARAMS: "SD_SOURCE_TYPE=artifact,SD_VERSION=\\n,SD_REPO_MERGE_MODE=replace"` + Or using comma separator: + 1. `ENV_NAMES: ` + 2. `GH_ADDITIONAL_PARAMS: "SD_SOURCE_TYPE=artifact,SD_VERSION=,,SD_REPO_MERGE_MODE=replace"` + + Or using space separator: + 1. `ENV_NAMES: ` + 2. `GH_ADDITIONAL_PARAMS: "SD_SOURCE_TYPE=artifact,SD_VERSION= ,SD_REPO_MERGE_MODE=replace"` + **Steps:** 1. The `process_sd` job runs in the pipeline: From 14a1477dd3e6df81a47016ca626df95fb97f2167 Mon Sep 17 00:00:00 2001 From: Tesma Jose <113982972+tesmarishy@users.noreply.github.com> Date: Thu, 5 Feb 2026 21:40:43 +0530 Subject: [PATCH 011/161] fix: extend delimiters for multi value params (#983) --- build_pipegene/scripts/gitlab_ci.py | 6 ++- build_pipegene/scripts/validations.py | 4 +- .../envgene/envgenehelper/business_helper.py | 4 -- .../envgenehelper/collections_helper.py | 39 +++++++++++++++++++ python/envgene/envgenehelper/crypt.py | 3 +- .../envgene/envgenehelper/test_collections.py | 36 +++++++++++++++++ scripts/build_env/process_sd.py | 4 +- 7 files changed, 87 insertions(+), 9 deletions(-) diff --git a/build_pipegene/scripts/gitlab_ci.py b/build_pipegene/scripts/gitlab_ci.py index df060fe64..140aac2aa 100644 --- a/build_pipegene/scripts/gitlab_ci.py +++ b/build_pipegene/scripts/gitlab_ci.py @@ -1,7 +1,7 @@ import os from os import listdir -from envgenehelper import logger, get_cluster_name_from_full_name, get_environment_name_from_full_name, parse_env_names +from envgenehelper import logger, get_cluster_name_from_full_name, get_environment_name_from_full_name from envgenehelper.plugin_engine import PluginEngine from gcip import JobFilter, Pipeline @@ -14,6 +14,8 @@ from passport_jobs import prepare_trigger_passport_job, prepare_passport_job from process_sd_job import prepare_process_sd from pipeline_helper import get_gav_coordinates_from_build, find_predecessor_job +from envgenehelper.collections_helper import split_multi_value_param + PROJECT_DIR = os.getenv('CI_PROJECT_DIR') or os.getenv('GITHUB_WORKSPACE') IS_GITLAB = bool(os.getenv('CI_PROJECT_DIR')) and not bool(os.getenv('GITHUB_ACTIONS')) @@ -55,7 +57,7 @@ def build_pipeline(params: dict) -> None: per_env_plugin_engine = PluginEngine(plugins_dir='/module/scripts/pipegene_plugins/per_env') - env_names = parse_env_names(params['ENV_NAMES']) + env_names = split_multi_value_param(params['ENV_NAMES']) if len(env_names) > 1 and is_inventory_generation_needed(params['IS_TEMPLATE_TEST'], params): raise ValueError( f"Generating Inventories for multiple Environments in single pipeline is not supported. " diff --git a/build_pipegene/scripts/validations.py b/build_pipegene/scripts/validations.py index 582616bb5..bcbe85386 100644 --- a/build_pipegene/scripts/validations.py +++ b/build_pipegene/scripts/validations.py @@ -2,6 +2,7 @@ from os import getenv from envgenehelper import check_for_cyrillic, logger, findAllYamlsInDir, openYaml, check_dir_exists, get_cluster_name_from_full_name, get_environment_name_from_full_name, check_environment_is_valid_or_fail, check_file_exists, validate_yaml_by_scheme_or_fail +from envgenehelper.collections_helper import split_multi_value_param project_dir = os.getenv('CI_PROJECT_DIR') or os.getenv('GITHUB_WORKSPACE') logger.info(f"Info about project_dir: {project_dir}") @@ -42,7 +43,8 @@ def template_test_checks(): raise ReferenceError("Execution is aborted as validation is not successful. See logs above.") def real_execution_checks(env_names, get_passport, env_build, env_inventory_init, env_inventory_content): - for env in env_names.split("\n"): + environment_names = split_multi_value_param(env_names) + for env in environment_names: # now we are using only complex environment names that contain both cluster_name and environment_name if env.count('/') != 1: logger.fatal(f"Wrong env_name given: {env}. Env_name should contain both cloud name and environment name by pattern '/'") diff --git a/python/envgene/envgenehelper/business_helper.py b/python/envgene/envgenehelper/business_helper.py index b8bb0aa5b..7ac7733f0 100644 --- a/python/envgene/envgenehelper/business_helper.py +++ b/python/envgene/envgenehelper/business_helper.py @@ -408,7 +408,3 @@ def get_bgd_object(env_dir: Path | None = None) -> CommentedMap: bgd_object = openYaml(bgd_path, allow_default=True) logger.debug(bgd_object) return bgd_object - - -def parse_env_names(full_env_names: str): - return full_env_names.split("\n") diff --git a/python/envgene/envgenehelper/collections_helper.py b/python/envgene/envgenehelper/collections_helper.py index ef301fb8a..6676da92f 100644 --- a/python/envgene/envgenehelper/collections_helper.py +++ b/python/envgene/envgenehelper/collections_helper.py @@ -2,6 +2,7 @@ from pprint import pformat from .yaml_helper import yaml import copy +from .logger import logger def merge_lists(list1, list2) : if len(list2) > 0 : @@ -95,3 +96,41 @@ def _compare_dicts_recurse(source: object, target: object, path: DictPath, diff_ elif source != target: diff_paths.append(path.copy()) +def split_multi_value_param(param: str)-> list[str]: + + if not param: + return [] + + param = param.strip() + if not param: + return [] + + has_comma = ',' in param + has_semicolon = ';' in param + has_space = ' ' in param + has_newline = '\n' in param + + delimiter_count = sum([has_comma, has_semicolon, has_space, has_newline]) + + if delimiter_count > 1: + raise ValueError( + "Invalid input: use only ONE delimiter type (comma, semicolon, space, or newline)" + ) + + if has_comma: + logger.info(f"env names {param} has comma as delimiter. splitting it") + parts = param.split(',') + elif has_semicolon: + logger.info(f"env names {param} has semicolon as delimiter. splitting it") + parts = param.split(';') + elif has_space: + logger.info(f"env names {param} has space as delimiter. splitting it") + parts = param.split() + elif has_newline: + logger.info(f"env names {param} has newline as delimiter. splitting it") + parts = param.splitlines() + else: + return [param] + + return [p.strip() for p in parts if p.strip()] + diff --git a/python/envgene/envgenehelper/crypt.py b/python/envgene/envgenehelper/crypt.py index ef9bb6d4f..83b72f199 100644 --- a/python/envgene/envgenehelper/crypt.py +++ b/python/envgene/envgenehelper/crypt.py @@ -7,6 +7,7 @@ from .yaml_helper import openYaml, get_empty_yaml from .file_helper import check_file_exists, get_files_with_filter from .logger import logger +from .collections_helper import split_multi_value_param from .crypt_backends.fernet_handler import crypt_Fernet, extract_value_Fernet, is_encrypted_Fernet from .crypt_backends.sops_handler import crypt_SOPS, extract_value_SOPS, is_encrypted_SOPS @@ -117,7 +118,7 @@ def get_all_necessary_cred_files() -> set[str]: if env_names == "env_template_test": logger.info("Running in env_template_test mode") return get_files_with_filter(BASE_DIR, is_cred_file) - env_names_list = env_names.split("\n") + env_names_list = split_multi_value_param(env_names) sources = set() sources.add("configuration") diff --git a/python/envgene/envgenehelper/test_collections.py b/python/envgene/envgenehelper/test_collections.py index 550cc28ae..77e7ecc6f 100644 --- a/python/envgene/envgenehelper/test_collections.py +++ b/python/envgene/envgenehelper/test_collections.py @@ -1,4 +1,5 @@ from .collections_helper import * +import pytest def convert_list_elements_to_strings_in_place(lst): for i in range(len(lst)): @@ -59,3 +60,38 @@ def test_compare_dicts(): assert len(s_diff) == len(diff) # no duplicates assert s_diff == set(expected_arr) and removed == [], f"Failed Test 8: {diff}, {removed}" + +@pytest.mark.parametrize( + "value, expected", + [ + ("env01", ["env01"]), + ("env01,env02", ["env01", "env02"]), + ("env01;env02", ["env01", "env02"]), + ("env01 env02", ["env01", "env02"]), + ("env01\nenv02", ["env01", "env02"]), + (" env01 env02 ", ["env01", "env02"]), + ("app1:1.0", ["app1:1.0"]), + ("app1:1.0;app2:2.0", ["app1:1.0", "app2:2.0"]), + ("app1:1.0;app2:2.0", ["app1:1.0", "app2:2.0"]), + ("app1:1.0;app2:2.0", ["app1:1.0", "app2:2.0"]), + ("app1:1.0;app2:2.0", ["app1:1.0", "app2:2.0"]), + ("", []), + (" ", []), + ], +) +def test_split_multi_value_param_valid(value, expected): + assert split_multi_value_param(value) == expected + + +@pytest.mark.parametrize( + "value", + [ + "env01, env02", + "env01; env02", + "env01\nenv02 env03", + "env01,env02;env03", + ], +) +def test_split_multi_value_param_invalid(value): + with pytest.raises(ValueError, match=r"use only ONE delimiter type"): + split_multi_value_param(value) \ No newline at end of file diff --git a/scripts/build_env/process_sd.py b/scripts/build_env/process_sd.py index 8abcd73f6..9aaba559b 100644 --- a/scripts/build_env/process_sd.py +++ b/scripts/build_env/process_sd.py @@ -5,6 +5,7 @@ from os import path, getenv from pathlib import Path + import envgenehelper as helper import yaml from artifact_searcher import artifact @@ -15,6 +16,7 @@ from envgenehelper.logger import logger from envgenehelper.plugin_engine import PluginEngine from envgenehelper.sd_merge_helper import basic_merge_multiple +from envgenehelper.collections_helper import split_multi_value_param class MergeType(Enum): @@ -270,7 +272,7 @@ def download_sds_with_version(env, base_sd_path, sd_version, effective_merge_mod logger.error("SD_SOURCE_TYPE is set to 'artifact', but SD_VERSION was not given in pipeline variables") exit(1) sd_version = sd_version.replace("\\n", "\n") - sd_entries = [line.strip() for line in sd_version.strip().splitlines() if line.strip()] + sd_entries = split_multi_value_param(sd_version) if not sd_entries: logger.error("No valid SD versions found in SD_VERSION") exit(1) From b2340f7ba73d1681dc0e54f87ec072b0bece64f3 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Thu, 5 Feb 2026 16:17:49 +0000 Subject: [PATCH 012/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index df9d14435..6ede13c35 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.22.1" - DOCKER_IMAGE_TAG_ENVGENE: "1.22.1" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.22.1" + DOCKER_IMAGE_TAG_PIPEGENE: "1.22.2" + DOCKER_IMAGE_TAG_ENVGENE: "1.22.2" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.22.2" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 576d25701..0bc359ad5 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.22.1 +version: 1.22.2 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index fa0fd3b5c..8ac215df2 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.22.1 +version: 1.22.2 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 57ae203ac..3e3da8563 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.22.1", + "envgene_version": "1.22.2", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 7f39fa2081b1e064d7e8ad068e30e91d97bd3e70 Mon Sep 17 00:00:00 2001 From: popoveugene <42543333+popoveugene@users.noreply.github.com> Date: Fri, 6 Feb 2026 00:42:28 +0300 Subject: [PATCH 013/161] docs: Custom Params (#999) --- docs/README.md | 1 + docs/features/calculator-cli.md | 99 +++++++++++++++++----------- docs/glossary.md | 5 ++ docs/instance-pipeline-parameters.md | 32 ++++++++- schemas/custom-params.schema.json | 18 +++++ 5 files changed, 117 insertions(+), 38 deletions(-) create mode 100644 schemas/custom-params.schema.json diff --git a/docs/README.md b/docs/README.md index 56a7494bd..9a7f5c67d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -46,6 +46,7 @@ - [**Solution Descriptor Processing**](/docs/features/sd-processing.md) - Manage [Solution Descriptor](/docs/envgene-objects.md#solution-descriptor) for your Environments - [**Effective Set Calculation**](/docs/features/calculator-cli.md) - Calculate the [Effective Set](/docs/features/calculator-cli.md#effective-set-v20) +- [**Custom Params**](/docs/instance-pipeline-parameters.md#custom_params) for session-scoped overrides - [**Application and Registry Definition**](/docs/features/app-reg-defs.md) - Describe how applications and registries are defined and referenced - [**Environment Inventory Generation**](/docs/features/env-inventory-generation.md) - Auto-generate [Environment Inventory](/docs/envgene-configs.md#env_definitionyml) - [**Environment Instance Generation**](/docs/features/environment-instance-generation.md) - Generate Environment Instances from templates and inventories (including BG support) diff --git a/docs/features/calculator-cli.md b/docs/features/calculator-cli.md index 81a72ee29..cf4a7121b 100644 --- a/docs/features/calculator-cli.md +++ b/docs/features/calculator-cli.md @@ -35,6 +35,7 @@ - [\[Version 2.0\]\[Deployment Parameter Context\] `credentials.yaml`](#version-20deployment-parameter-context-credentialsyaml) - [\[Version 2.0\] Predefined `credentials.yaml` parameters](#version-20-predefined-credentialsyaml-parameters) - [\[Version 2.0\]\[Deployment Parameter Context\] Collision Parameters](#version-20deployment-parameter-context-collision-parameters) + - [\[Version 2.0\]\[Deployment Parameter Context\] `custom-params.yaml`](#version-20deployment-parameter-context-custom-paramsyaml) - [\[Version 2.0\]\[Deployment Parameter Context\] `deploy-descriptor.yaml`](#version-20deployment-parameter-context-deploy-descriptoryaml) - [\[Version 2.0\] Predefined `deploy-descriptor.yaml` parameters](#version-20-predefined-deploy-descriptoryaml-parameters) - [\[Version 2.0\] Service Artifacts](#version-20-service-artifacts) @@ -98,19 +99,20 @@ Below is a **complete** list of attributes -| Attribute | Type | Mandatory | Description | Default | Example | -|-----------------------------------------------------|---------|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|--------------------------------------------------------------------------------------| -| `--env-id`/`-e` | string | yes | Environment ID in `/` notation | N/A | `cluster/platform-00` | -| `--envs-path`/`-ep` | string | yes | Path to `/environments` folder | N/A | `/environments` | -| `--sboms-path`/`-sp` | string | no | Path to the folder with Application SBOMs. If the attribute is not provided, generation occurs in [No SBOMs Mode](#version-20-no-sboms-mode) | N/A | `/sboms` | -| `--sd-path`/`-sdp` | string | yes | Path to the Solution Descriptor | N/A | `/environments/cluster/platform-00/Inventory/solution-descriptor/sd.yaml` | -| `--registries`/`-r` | string | no | Required when `--sd-path` and `--sboms-path` are provided. Optional for [No SBOMs Mode](#version-20-no-sboms-mode) | N/A | `/configuration/registry.yml` | -| `--output`/`-o` | string | yes | Folder where the result will be put by Calculator command-line tool | N/A | `/environments/cluster/platform-00/effective-set` | -| `--effective-set-version`/`-esv` | string | no | The version of the effective set to be generated. Available options are `v1.0` and `v2.0` | `v2.0` | `v1.0` | -| `--pipeline-consumer-specific-schema-path`/`-pcssp` | string | no | Path to a JSON schema defining a consumer-specific pipeline context component. Multiple attributes of this type can be provided | N/A | | -| `--extra_params`/`-ex` | string | no | Additional parameters used by the Calculator for effective set generation. Multiple instances of this attribute can be provided | N/A | `DEPLOYMENT_SESSION_ID=550e8400-e29b-41d4-a716-446655440000` | -| `--app_chart_validation`/`-acv` | boolean | no | Determines whether [app chart validation](#version-20-app-chart-validation) should be performed. If `true` validation is enabled (checks for `application/vnd.qubership.app.chart` in SBOM). If `false` validation is skipped | `true` | `false` | -| `--enable-traceability`/`-etr` | boolean | no | Determines whether [traceability](#version-20-traceability-comments) will be enabled. If `true`, traceability comments will be added. If `false`, they will be omitted. | `false` | `true` | +| Attribute | Type | Mandatory | Description | Default | Example | +|-----------------------------------------------------|---------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|---------------------------------------------------------------------------| +| `--env-id`/`-e` | string | yes | Environment ID in `/` notation | N/A | `cluster/platform-00` | +| `--envs-path`/`-ep` | string | yes | Path to `/environments` folder | N/A | `/environments` | +| `--sboms-path`/`-sp` | string | no | Path to the folder with Application SBOMs. If the attribute is not provided, generation occurs in [No SBOMs Mode](#version-20-no-sboms-mode) | N/A | `/sboms` | +| `--sd-path`/`-sdp` | string | yes | Path to the Solution Descriptor | N/A | `/environments/cluster/platform-00/Inventory/solution-descriptor/sd.yaml` | +| `--registries`/`-r` | string | no | Required when `--sd-path` and `--sboms-path` are provided. Optional for [No SBOMs Mode](#version-20-no-sboms-mode) | N/A | `/configuration/registry.yml` | +| `--output`/`-o` | string | yes | Folder where the result will be put by Calculator command-line tool | N/A | `/environments/cluster/platform-00/effective-set` | +| `--effective-set-version`/`-esv` | string | no | The version of the effective set to be generated. Available options are `v1.0` and `v2.0` | `v2.0` | `v1.0` | +| `--pipeline-consumer-specific-schema-path`/`-pcssp` | string | no | Path to a JSON schema defining a consumer-specific pipeline context component. Multiple attributes of this type can be provided | N/A | | +| `--extra_params`/`-ex` | string | no | Additional parameters used by the Calculator for effective set generation. Multiple instances of this attribute can be provided | N/A | `DEPLOYMENT_SESSION_ID=550e8400-e29b-41d4-a716-446655440000` | +| `--app_chart_validation`/`-acv` | boolean | no | Determines whether [app chart validation](#version-20-app-chart-validation) should be performed. If `true` validation is enabled (checks for `application/vnd.qubership.app.chart` in SBOM). If `false` validation is skipped | `true` | `false` | +| `--enable-traceability`/`-etr` | boolean | no | Determines whether [traceability](#version-20-traceability-comments) will be enabled. If `true`, traceability comments will be added. If `false`, they will be omitted. | `false` | `true` | +| `--custom-params`/`-cp` | string | no | [Custom Params](/docs/glossary.md#custom-params) to inject into the Effective Set with highest priority. Applied to deployment, runtime, and cleanup contexts. Treated as sensitive. JSON-in-string format; value structure described in [CUSTOM_PARAMS](/docs/instance-pipeline-parameters.md#custom_params) | N/A | `"{\"deployment\":{\"KEY\":\"val\"}}"` | ### Registry Configuration @@ -278,7 +280,8 @@ Effective Set generation in Version 1.0 does not support [No SBOMs Mode](#versio | | | ├── collision-deployment-parameters.yaml | | | ├── credentials.yaml | | | ├── collision-credentials.yaml - | | | └── deploy-descriptor.yaml + | | | ├── deploy-descriptor.yaml + | | | └── custom-params.yaml | | └── | | └── values | | ├── per-service-parameters @@ -292,7 +295,8 @@ Effective Set generation in Version 1.0 does not support [No SBOMs Mode](#versio | | ├── collision-deployment-parameters.yaml | | ├── credentials.yaml | | ├── collision-credentials.yaml - | | └── deploy-descriptor.yaml + | | ├── deploy-descriptor.yaml + | | └── custom-params.yaml | └── | ├── | | └── values @@ -307,7 +311,8 @@ Effective Set generation in Version 1.0 does not support [No SBOMs Mode](#versio | | ├── collision-deployment-parameters.yaml | | ├── credentials.yaml | | ├── collision-credentials.yaml - | | └── deploy-descriptor.yaml + | | ├── deploy-descriptor.yaml + | | └── custom-params.yaml | └── | └── values | ├── per-service-parameters @@ -321,7 +326,8 @@ Effective Set generation in Version 1.0 does not support [No SBOMs Mode](#versio | ├── collision-deployment-parameters.yaml | ├── credentials.yaml | ├── collision-credentials.yaml - | └── deploy-descriptor.yaml + | ├── deploy-descriptor.yaml + | └── custom-params.yaml ├── runtime | ├── mapping.yml | ├── @@ -428,6 +434,7 @@ Sensitive parameters in the Effective Set are grouped into dedicated credentials 4. `effective-set/deployment///credentials.yaml` 5. `effective-set/deployment///collision-credentials.yaml` 6. `effective-set/runtime///credentials.yaml` +7. `effective-set/deployment///values/custom-params.yaml` **Splitting principle:** @@ -503,22 +510,23 @@ Parameters can be set at different levels: If the same parameter key is set in multiple sources or levels, the Calculator uses the following priority (from highest to lowest): -1. User-defined in [Resource Profile Override](/docs/envgene-objects.md#resource-profile-override-in-instance) of the Environment Instance -2. Service level defined in Resource Profile Baseline in Application SBOM -3. Service level defined in other Application SBOM attributes -4. Calculator-generated at the Service level -5. User-defined at the [Application](/docs/envgene-objects.md#application) level in Environment Instance -6. Application level defined in Application SBOM -7. Calculator-generated at the Application level -8. User-defined at the [Namespace](/docs/envgene-objects.md#namespace) level in Environment Instance -9. Namespace level defined in Application SBOM -10. Calculator-generated at the Namespace level -11. User-defined at the [Cloud](/docs/envgene-objects.md#cloud) level in Environment Instance -12. Cloud level defined in Application SBOM -13. Calculator-generated at the Cloud level -14. User-defined at the [Tenant](/docs/envgene-objects.md#tenant) level in Environment Instance -15. Calculator extra parameters (`--extra_params`) -16. Default values by calculator +1. [Custom Params](/docs/glossary.md#custom-params) (`--custom-params`) +2. User-defined in [Resource Profile Override](/docs/envgene-objects.md#resource-profile-override-in-instance) of the Environment Instance +3. Service level defined in Resource Profile Baseline in Application SBOM +4. Service level defined in other Application SBOM attributes +5. Calculator-generated at the Service level +6. User-defined at the [Application](/docs/envgene-objects.md#application) level in Environment Instance +7. Application level defined in Application SBOM +8. Calculator-generated at the Application level +9. User-defined at the [Namespace](/docs/envgene-objects.md#namespace) level in Environment Instance +10. Namespace level defined in Application SBOM +11. Calculator-generated at the Namespace level +12. User-defined at the [Cloud](/docs/envgene-objects.md#cloud) level in Environment Instance +13. Cloud level defined in Application SBOM +14. Calculator-generated at the Cloud level +15. User-defined at the [Tenant](/docs/envgene-objects.md#tenant) level in Environment Instance +16. Calculator extra parameters (`--extra_params`) +17. Default values by calculator For example, if a user sets `BASELINE_PROJ` at the Namespace level, this value will override the value calculated by the calculator. If `BASELINE_PROJ` is set at the cloud level, it will be overridden by calculator-generated values at Namespace or Application levels. @@ -537,6 +545,7 @@ The CLI flag [`--enable-traceability`](#calculator-command-line-tool-execution-a | Parameter Source | Comment | Example | |----------------------------------------------------|--------------------------------------------|----------------------------------------------------------------------------------------------| +| Custom Params (`--custom-params`) | `# custom params` | `OVERRIDE_KEY: "value" # custom params` | | Environment Instance, Tenant | `# tenant` | `GITLAB_URL: "https://git.qibership.org" # tenant` | | Environment Instance, Cloud | `# cloud` | `CLOUD_API_HOST: "https://api.example.com" # cloud` | | Environment Instance, Namespace | `# namespace: ` | `NAMESPACE_NAME: "env-1-core" # namespace: env-1-core` | @@ -773,6 +782,14 @@ The structure of both files is following: These files must only contain keys that match the name of a [services](#version-20-service-inclusion-criteria-and-naming-convention) +##### \[Version 2.0][Deployment Parameter Context] `custom-params.yaml` + +This file is based on the parameter passed to the Calculator via `--custom-params`. + +It contains parameters from the `deployment` section of the object passed to `--custom-params`. + +If `--custom-params` is not passed, the file is generated empty. + ##### \[Version 2.0][Deployment Parameter Context] `deploy-descriptor.yaml` This file describes the parameters of the application artifacts generated during the build process. These parameters are extracted from the Application's SBOM. The file contains a **predefined** set of parameters, and users cannot modify it. @@ -1335,9 +1352,13 @@ The `` can be complex, such as a map or a list, whose elements can also b ##### \[Version 2.0][Runtime Parameter Context] `credentials.yaml` -This file contains sensitive parameters defined in the `technicalConfigurationParameters` section. +This file contains: -For more information, refer to [Sensitive parameters processing](#version-20-sensitive-parameters-processing). +1. Sensitive parameters defined in the `technicalConfigurationParameters` section. For more information, refer to [Sensitive parameters processing](#version-20-sensitive-parameters-processing) + +2. Parameters from the `runtime` section of the object passed to `--custom-params` + +Parameters from `--custom-params` have higher priority. The structure of this file is as follows: @@ -1371,9 +1392,13 @@ The structure of this file is as follows: ##### \[Version 2.0][Cleanup Context] `credentials.yaml` -This file contains sensitive parameters defined in the `deployParameters` sections of the `Tenant`, `Cloud`, and `Namespace` Environment Instance objects. +This file contains -For more information, refer to [Sensitive parameters processing](#version-20-sensitive-parameters-processing). +1. Sensitive parameters defined in the `deployParameters` sections of the `Tenant`, `Cloud`, and `Namespace` Environment Instance objects. For more information, refer to [Sensitive parameters processing](#version-20-sensitive-parameters-processing) + +2. Parameters from the `runtime` section of the object passed to `--custom-params` + +Parameters from `--custom-params` have higher priority. The structure of this file is as follows: diff --git a/docs/glossary.md b/docs/glossary.md index f6ec07568..4ab78b3b7 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -1,6 +1,7 @@ # Glossary - [Glossary](#glossary) + - [Custom Params](#custom-params) - [Deploy Postfix](#deploy-postfix) - [Environment](#environment) - [Environment Inventory](#environment-inventory) @@ -11,6 +12,10 @@ This glossary provides definitions of key terms used in the EnvGene documentation. +## Custom Params + +Session-scoped parameters passed via the Instance pipeline parameter [`CUSTOM_PARAMS`](/docs/instance-pipeline-parameters.md#custom_params) and applied to the [Effective Set](/docs/features/calculator-cli.md#version-20-effective-set-structure) with the highest priority. Custom Params are not persisted between parameter calculation sessions and are treated as sensitive. See [Calculator CLI](/docs/features/calculator-cli.md) for details. + ## Deploy Postfix A short identifier for a [Namespace](/docs/envgene-objects.md#namespace) role. Used in the Solution Descriptor. Typically matches the namespace folder name or template name. diff --git a/docs/instance-pipeline-parameters.md b/docs/instance-pipeline-parameters.md index bc513b422..bb4794fbd 100644 --- a/docs/instance-pipeline-parameters.md +++ b/docs/instance-pipeline-parameters.md @@ -14,6 +14,7 @@ - [`ENV_INVENTORY_CONTENT`](#env_inventory_content) - [`GENERATE_EFFECTIVE_SET`](#generate_effective_set) - [`EFFECTIVE_SET_CONFIG`](#effective_set_config) + - [`CUSTOM_PARAMS`](#custom_params) - [`APP_REG_DEFS_JOB`](#app_reg_defs_job) - [`APP_DEFS_PATH`](#app_defs_path) - [`REG_DEFS_PATH`](#reg_defs_path) @@ -289,6 +290,33 @@ Consumer-specific pipeline context components registered in EnvGene: "{\"version\": \"v2.0\", \"app_chart_validation\": \"false\"}" ``` +### `CUSTOM_PARAMS` + +**Description**: Session-scoped parameters injected into the Effective Set during parameter calculation. Custom Params are not persisted across parameter calculation sessions, have the highest priority in the parameter resolution hierarchy, and are treated as sensitive. + +EnvGene passes the value unchanged to the Calculator CLI via `--custom-params`. See [Calculator CLI](/docs/features/calculator-cli.md) for how Custom Params are applied to the Effective Set. + +**Format**: A string containing a JSON object (JSON-in-string). The JSON object must conform to the [schema](/schemas/custom-params.schema.json). + +```json +{ + "deployment": { + "": "", + "...": "..." + }, + "runtime": { + "": "", + "...": "..." + } +} +``` + +**Default Value**: None + +**Mandatory**: No + +**Example**: `"{\"deployment\":{\"MY_OVERRIDE\":\"value\"}}"` + ### `APP_REG_DEFS_JOB` **Description**: Specifies the name of the job that is the source of [Application Definition](/docs/envgene-objects.md#application-definition) and [Registry Definitions](/docs/envgene-objects.md#registry-definition). @@ -372,12 +400,14 @@ See details in [SD processing](/docs/features/sd-processing.md) ### `SD_DATA` -**Description**: Specifies the contents of one or more SD in JSON-in-string format. Can be either a single SD object or a list of SD objects. +**Description**: Specifies the contents of one or more SD. Can be either a single SD object or a list of SD objects. If a single SD object is provided, it is processed directly. If a list is provided, EnvGene sequentially merges them in the `basic-merge` mode, where subsequent element takes priority over the previous one. Optionally saves the result to [Delta SD](/docs/features/sd-processing.md#delta-sd), then merges with [Full SD](/docs/features/sd-processing.md#full-sd) using `SD_REPO_MERGE_MODE` merge mode See details in [SD processing](/docs/features/sd-processing.md) +**Format**: A string containing a JSON object (JSON-in-string) + **Default Value**: None **Mandatory**: No diff --git a/schemas/custom-params.schema.json b/schemas/custom-params.schema.json new file mode 100644 index 000000000..a62e943cb --- /dev/null +++ b/schemas/custom-params.schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://qubership.org/schemas/custom-params.schema.json", + "title": "CUSTOM_PARAMS", + "description": "Schema for the CUSTOM_PARAMS instance pipeline parameter.", + "type": "object", + "additionalProperties": false, + "properties": { + "deployment": { + "type": "object", + "description": "Overrides applied to the deployment and cleanup context of Effective Set" + }, + "runtime": { + "type": "object", + "description": "Overrides applied to the runtime context of Effective Set" + } + } +} From 68c83c608f4116b43166f84ed72028ca6fb26555 Mon Sep 17 00:00:00 2001 From: basudev91 Date: Fri, 6 Feb 2026 16:58:16 +0530 Subject: [PATCH 014/161] docs: Fixed link checker issue (844) (#998) * docs: update MD links * chore: Added new options to link checker * chore: Updated the docs * chore: Updated the docs * chore: Updated the docs * chore: Updated the docs * docs: update MD links * docs: update docs link * docs: added sd processing testcase file * docs: change to LF format * docs: fix conflict * docs: remove env-specific-schema yml file and link --------- Co-authored-by: Andrei Rudchenko --- .github/workflows/link-checker.yaml | 5 +++- docs/dev/unify-logging.md | 4 ++-- docs/envgene-pipelines.md | 4 ++-- docs/features/blue-green-deployment.md | 14 +++++------ docs/features/calculator-cli.md | 10 ++++---- docs/features/env-specific-schema.md | 2 +- .../environment-instance-generation.md | 2 +- docs/features/resource-profile.md | 24 +++++++++---------- docs/how-to/create-cluster.md | 6 ++--- docs/instance-pipeline-parameters.md | 2 +- docs/template-macros.md | 4 ++-- docs/test-cases/sd-processing.md | 1 + docs/use-cases/calculator-cli.md | 2 +- .../.github/docs/README.md | 24 +++++++++---------- 14 files changed, 54 insertions(+), 50 deletions(-) create mode 100644 docs/test-cases/sd-processing.md diff --git a/.github/workflows/link-checker.yaml b/.github/workflows/link-checker.yaml index b11e70963..2b2e8a60b 100644 --- a/.github/workflows/link-checker.yaml +++ b/.github/workflows/link-checker.yaml @@ -41,10 +41,13 @@ --max-retries 8 --accept 100..=103,200..=299,429 --cookie-jar cookies.json - --exclude-all-private + --include 'https?://.*' --max-concurrency 4 --cache --cache-exclude-status '429, 500..502' --max-cache-age 1d + --include file + --include-fragments + --root-dir $GITHUB_WORKSPACE format: markdown fail: true diff --git a/docs/dev/unify-logging.md b/docs/dev/unify-logging.md index a450df88f..f4c49dcac 100644 --- a/docs/dev/unify-logging.md +++ b/docs/dev/unify-logging.md @@ -43,7 +43,7 @@ All other modules import and use it. A new parameter was added to control logging behavior. -[Link to documentation](https://github.com/Netcracker/qubership-envgene/blob/a823f450a671d058813991b218b9afde59f6db41/docs/envgene-repository-variables.md#envgene_log_level) +[Link to documentation](/docs/envgene-repository-variables.md#envgene_log_level) --- @@ -67,4 +67,4 @@ A script was added that: - Runs at the start of every generated job - Logs input parameters -[How it was implemented](https://github.com/Netcracker/qubership-envgene/blob/main/build_pipegene/scripts/pipeline_helper.py#L47-L50) +[How it was implemented](/build_pipegene/scripts/pipeline_helper.py#L47-L50) diff --git a/docs/envgene-pipelines.md b/docs/envgene-pipelines.md index 35971e4d4..854338bff 100644 --- a/docs/envgene-pipelines.md +++ b/docs/envgene-pipelines.md @@ -40,7 +40,7 @@ flowchart LR - **Docker image**: [`qubership-envgene`](https://github.com/Netcracker/qubership-envgene/pkgs/container/qubership-envgene) 4. **env_inventory_generation**: - - **Condition**: Runs if [`ENV_TEMPLATE_TEST: false`](/docs/instance-pipeline-parameters.md#env_template_test) AND ([`ENV_SPECIFIC_PARAMS`](/docs/instance-pipeline-parameters.md#env_specific_params) OR [`ENV_TEMPLATE_NAME`](/docs/instance-pipeline-parameters.md#env_template_name)) + - **Condition**: Runs if [`ENV_TEMPLATE_TEST: false`](/docs/envgene-repository-variables.md#env_template_test) AND ([`ENV_SPECIFIC_PARAMS`](/docs/instance-pipeline-parameters.md#env_specific_params) OR [`ENV_TEMPLATE_NAME`](/docs/instance-pipeline-parameters.md#env_template_name)) - **Docker image**: [`qubership-envgene`](https://github.com/Netcracker/qubership-envgene/pkgs/container/qubership-envgene) 5. **credential_rotation**: @@ -80,5 +80,5 @@ flowchart LR - **Docker image**: [`qubership-effective-set-generator`](https://github.com/Netcracker/qubership-envgene/pkgs/container/qubership-effective-set-generator) 10. **git_commit**: - - **Condition**: Runs if there are jobs requiring changes to the repository AND [`ENV_TEMPLATE_TEST: false`](/docs/instance-pipeline-parameters.md#env_template_test) + - **Condition**: Runs if there are jobs requiring changes to the repository AND [`ENV_TEMPLATE_TEST: false`](/docs/envgene-repository-variables.md#env_template_test) - **Docker image**: [`qubership-envgene`](https://github.com/Netcracker/qubership-envgene/pkgs/container/qubership-envgene) diff --git a/docs/features/blue-green-deployment.md b/docs/features/blue-green-deployment.md index c593e477a..4d3d51759 100644 --- a/docs/features/blue-green-deployment.md +++ b/docs/features/blue-green-deployment.md @@ -81,15 +81,15 @@ The following functionality is used in these scenarios: - EnvGene generates a [BG Domain](/docs/envgene-objects.md#bg-domain) from a [BG Domain Template](/docs/envgene-objects.md#bg-domain-template) as part of Environment Instance generation - EnvGene validates that namespaces referenced in the BG Domain object exist in the Environment during Environment Instance generation - EnvGene is able to generate particular [Namespaces](/docs/envgene-objects.md#namespace) only of Environment using [Namespace Render Filter](#namespace-render-filter) feature -- EnvGene provides parameters describing BG domain in [Effective Set](/docs/calculator-cli.md#version-20topology-context-bg_domain-example) +- EnvGene provides parameters describing BG domain in [Effective Set](/docs/features/calculator-cli.md#version-20topology-context-bg_domain-example) - EnvGene creates, updates and validates [BG state files](#bg-state-files) for peer and origin namespaces, based on BG Plugin call -- EnvGene supports the [warmup operation](#warmup-operation) by copying [Namespace](https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-objects.md#namespace) and [Application](https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-objects.md#application) for origin/peer +- EnvGene supports the [warmup operation](#warmup-operation) by copying [Namespace](/docs/envgene-objects.md#namespace) and [Application](/docs/envgene-objects.md#application) for origin/peer - EnvGene [imports](#cmdb-import) the BG domain object into CMDB ### BG Related EnvGene objects -- [BG Domain](https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-objects.md#bg-domain): Configuration object that defines domain structure -- [BG Domain Template](https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-objects.md#bg-domain-template): Template used to generate BG Domain object during Environment Instance generation. During Environment Instance generation, EnvGene validates that all namespaces referenced in the generated BG Domain object (origin, peer, and controller namespaces) actually exist in the Environment. If any referenced namespace is missing, the generation fails with a validation error. +- [BG Domain](/docs/envgene-objects.md#bg-domain): Configuration object that defines domain structure +- [BG Domain Template](/docs/envgene-objects.md#bg-domain-template): Template used to generate BG Domain object during Environment Instance generation. During Environment Instance generation, EnvGene validates that all namespaces referenced in the generated BG Domain object (origin, peer, and controller namespaces) actually exist in the Environment. If any referenced namespace is missing, the generation fails with a validation error. - [BG State Files](/docs/envgene-objects.md#bg-state-files): Files that track origin and peer namespace states ### Namespace Render Filter @@ -105,13 +105,13 @@ This job is part of the Instance pipeline and does the following: - Validates namespace names in `BG_STATE` against the [BG Domain](/docs/envgene-objects.md#bg-domain) object in the Environment Instance - Validates BG states received in `BG_STATE` against BG state files in the repository - Creates/updates [BG state files](/docs/envgene-objects.md#bg-state-files) -- During warmup, copies [Namespace](https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-objects.md#namespace) and [Applications](https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-objects.md#application) under it +- During warmup, copies [Namespace](/docs/envgene-objects.md#namespace) and [Applications](/docs/envgene-objects.md#application) under it The criteria for running this job and its order relative to other jobs are described in [envgene-pipelines](/docs/envgene-pipelines.md). ### BG Related Instance Pipeline Parameters -- [`ENV_NAMES`](https://github.com/Netcracker/qubership-envgene/blob/main/docs/instance-pipeline-parameters.md#env_names) +- [`ENV_NAMES`](/docs/instance-pipeline-parameters.md#env_names) - [`BG_MANAGE`](/docs/instance-pipeline-parameters.md#bg_manage) - [`BG_STATE`](/docs/instance-pipeline-parameters.md#bg_state) - [`GH_ADDITIONAL_PARAMS`](/docs/instance-pipeline-parameters.md#gh_additional_params) @@ -235,7 +235,7 @@ This ensures that the candidate namespace will use the same template artifact ve ### CMDB Import -The CMDB Import feature creates, among other entities such as Cloud or Namespace the [Blue Green Domain](https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-objects.md#bg-domain) in the CMDB. +The CMDB Import feature creates, among other entities such as Cloud or Namespace the [Blue Green Domain](/docs/envgene-objects.md#bg-domain) in the CMDB. To do this, run the instance pipeline with the `CMDB_IMPORT: true` pipeline parameter. diff --git a/docs/features/calculator-cli.md b/docs/features/calculator-cli.md index cf4a7121b..57449c67f 100644 --- a/docs/features/calculator-cli.md +++ b/docs/features/calculator-cli.md @@ -73,7 +73,7 @@ 2. Calculator command-line tool must support [Effective Set version 2.0](#effective-set-v20) generation 3. Calculator command-line tool must process [execution attributes](#calculator-command-line-tool-execution-attributes) 4. Calculator command-line tool must not encrypt or decrypt sensitive parameters (credentials.yaml) -5. Calculator command-line tool must resolve [macros](/docs/template-macros.md#calculator-cli-macros) +5. Calculator command-line tool must resolve [macros](/docs/template-macros.md#calculator-command-line-tool-macros) 6. Calculator command-line tool should not process Parameter Sets 7. Calculator command-line tool must not cast parameters type 8. Calculator command-line tool must display reason of error @@ -495,7 +495,7 @@ These contexts are used as a source exclusively from the environment instance. Parameters in the Effective Set come from different sources: 1. User-defined in the [Environment Instance](/docs/envgene-objects.md#environment-instance-objects) -2. [Application SBOM](/docs/sbom.md#application-sbom) +2. [Application SBOM](/docs/features/sbom.md#application-sbom) 3. Generated by the calculator 4. Calculator extra parameters (`--extra_params`) 5. Calculator defaults @@ -511,7 +511,7 @@ Parameters can be set at different levels: If the same parameter key is set in multiple sources or levels, the Calculator uses the following priority (from highest to lowest): 1. [Custom Params](/docs/glossary.md#custom-params) (`--custom-params`) -2. User-defined in [Resource Profile Override](/docs/envgene-objects.md#resource-profile-override-in-instance) of the Environment Instance +2. User-defined in [Resource Profile Override](/docs/envgene-objects.md#resource-profile-override) of the Environment Instance 3. Service level defined in Resource Profile Baseline in Application SBOM 4. Service level defined in other Application SBOM attributes 5. Calculator-generated at the Service level @@ -530,7 +530,7 @@ If the same parameter key is set in multiple sources or levels, the Calculator u For example, if a user sets `BASELINE_PROJ` at the Namespace level, this value will override the value calculated by the calculator. If `BASELINE_PROJ` is set at the cloud level, it will be overridden by calculator-generated values at Namespace or Application levels. -Not every parameter in the Effective Set can be overridden. Overriding service-level parameters is only possible via [resource profile override](/docs/envgene-objects.md#resource-profile-override-in-instance). +Not every parameter in the Effective Set can be overridden. Overriding service-level parameters is only possible via [resource profile override](/docs/envgene-objects.md#resource-profile-override). > [!NOTE] > If a complex parameter is set, the value from the higher-priority source **completely replaces** the lower-priority one; merging is not performed. @@ -591,7 +591,7 @@ The CLI flag [`--enable-traceability`](#calculator-command-line-tool-execution-a servers: - "server1.example.com" # cloud - "server2.example.com" # namespace: env-1 - + servers: name: "server1" # cloud host: "host1" # cloud diff --git a/docs/features/env-specific-schema.md b/docs/features/env-specific-schema.md index 64b47023c..994fb8b85 100644 --- a/docs/features/env-specific-schema.md +++ b/docs/features/env-specific-schema.md @@ -14,7 +14,7 @@ I as DevOps want to define the set of parameters that should be specified by cus ... ``` -2. I'm creating schema for the path (e.g. [env-specific-schema](/docs/samples/template-repository/templates/env_templates/composite/env-specific-schema.yml)) +2. I'm creating the environment-specific schema file at the referenced path. 3. I'm building template diff --git a/docs/features/environment-instance-generation.md b/docs/features/environment-instance-generation.md index 0ce15d4b0..cb75a373b 100644 --- a/docs/features/environment-instance-generation.md +++ b/docs/features/environment-instance-generation.md @@ -16,7 +16,7 @@ ## Description -This feature describes the process of generating an [Environment Instance](/docs/envgene-objects.md#environment-instance) from an [Environment Template](/docs/envgene-objects.md#environment-template) and [Environment Inventory](/docs/envgene-configs.md#env_definitionyml). The generation process creates the directory structure and files for the Environment Instance, including Namespaces, Applications, Resource Profiles, Credentials, and other EnvGene objects. +This feature describes the process of generating an [Environment Instance](/docs/envgene-objects.md#environment-instance-objects) from an [Environment Template](/docs/envgene-objects.md#environment-template-objects) and [Environment Inventory](/docs/envgene-configs.md#env_definitionyml). The generation process creates the directory structure and files for the Environment Instance, including Namespaces, Applications, Resource Profiles, Credentials, and other EnvGene objects. ## Namespace Folder Name Generation diff --git a/docs/features/resource-profile.md b/docs/features/resource-profile.md index bb2101c20..25b4bfc80 100644 --- a/docs/features/resource-profile.md +++ b/docs/features/resource-profile.md @@ -17,7 +17,7 @@ The Resource Profiles system has a 3-level hierarchy: 1. Resource Profile Baselines - These are sets of pre-configured performance parameters for services, intended to provide a standardized performance configuration for a service. + These are sets of pre-configured performance parameters for services, intended to provide a standardized performance configuration for a service. Service developers create these baselines and distribute them together with the application artifact. Later, they are included in the Application SBOM. Typical baseline profiles are: @@ -27,7 +27,7 @@ The Resource Profiles system has a 3-level hierarchy: You can have any number of profiles and call them whatever you want, e.g. `small`, `medium`, `large`. -2. [Template Resource Profile Override](/docs/envgene-objects.md#templates-resource-profile-override) +2. [Template Resource Profile Override](/docs/envgene-objects.md#template-resource-profile-override) These are customizations for performance parameters, over a Baseline Resource Profile. Such overrides are created by the configurator in the Template repository, to further adjust performance parameters on top of the Baseline Resource Profile Override for all environments of the same type. @@ -45,10 +45,10 @@ When calculating the [Effective Set](/docs/features/calculator-cli.md#effective- During Environment generation, as part of the [`env_build`](/docs/envgene-pipelines.md#instance-pipeline) job, two types of Resource Profile Overrides are processed and combined: -1. [Template Resource Profile Override](/docs/envgene-objects.md#templates-resource-profile-override) +1. [Template Resource Profile Override](/docs/envgene-objects.md#template-resource-profile-override) - The Template Resource Profile Override is configured individually for each Namespace or Cloud, identified by the `profile.name` property. - Each override is represented as a YAML file located at `/templates/resource_profiles` within the Environment Template repository. + The Template Resource Profile Override is configured individually for each Namespace or Cloud, identified by the `profile.name` property. + Each override is represented as a YAML file located at `/templates/resource_profiles` within the Environment Template repository. The filename (without the `.yaml` or `.yml` extension) must exactly correspond to the value set in `profile.name` for that specific Cloud or Namespace. For example, if a namespace specifies `profile.name: dev-over`, it will use `/templates/resource_profiles/dev-over.yaml` as its template resource profile override. @@ -68,9 +68,9 @@ During Environment generation, as part of the [`env_build`](/docs/envgene-pipeli When an Environment Specific Resource Profile Override is referenced, EnvGene searches for the corresponding YAML file in the Instance repository using the following location priority (from highest to lowest): - 1. `/environments///Inventory/resource_profiles` — Environment-specific, highest priority - 2. `/environments//resource_profiles` — Cluster-wide, applies to all environments in the cluster - 3. `/environments/resource_profiles` — Global, common for the entire repository + 1. `/environments///Inventory/resource_profiles` — Environment-specific, highest priority + 2. `/environments//resource_profiles` — Cluster-wide, applies to all environments in the cluster + 3. `/environments/resource_profiles` — Global, common for the entire repository The first match found is used as the environment-specific override for the given Cloud or Namespace. @@ -78,7 +78,7 @@ The final result of processing is a [Resource Profile Override](/docs/envgene-ob ### Combination Logic -Resource Profile combination happens when you define Environment-Specific Resource Profile Overrides for a particular Cloud or Namespace. +Resource Profile combination happens when you define Environment-Specific Resource Profile Overrides for a particular Cloud or Namespace. These overrides are referenced via `envTemplate.envSpecificResourceProfiles` in the [Environment Inventory](/docs/envgene-configs.md#env_definitionyml): ```yaml @@ -90,7 +90,7 @@ envTemplate: : ``` -There are two ways to combine overrides: **merge** and **replace**. +There are two ways to combine overrides: **merge** and **replace**. Which mode is used is controlled by the `inventory.config.mergeEnvSpecificResourceProfiles` setting in the [Environment Inventory](/docs/envgene-configs.md#env_definitionyml): ```yaml @@ -123,7 +123,7 @@ The Environment-Specific Resource Profile Override is merged **into** the Templa - If the parameter is missing in the target, add the entire parameter from the template. - If the parameter exists in both, update the parameter in the target by overwriting its `value` with the one from the template. -In this mode, the resulting [Resource Profile Override](/docs/envgene-objects.md#resource-profile-override) will have the same name as the [Template Resource Profile Override](/docs/envgene-objects.md#templates-resource-profile-override). +In this mode, the resulting [Resource Profile Override](/docs/envgene-objects.md#resource-profile-override) will have the same name as the [Template Resource Profile Override](/docs/envgene-objects.md#template-resource-profile-override). ### Naming Rules for Resource Profile Override @@ -140,7 +140,7 @@ inventory: ``` If you set `updateRPOverrideNameWithEnvName: true`, the system will: - + 1. Add a prefix to the name of each [Resource Profile Override](/docs/envgene-objects.md#resource-profile-override). The prefix will be constructed from the [``](/docs/template-macros.md#current_envtenant), [``](/docs/template-macros.md#current_envcloud), and [``](/docs/template-macros.md#current_envname), joined by hyphens, followed by the original Resource Profile Override name. 2. Add the same prefix to the `profile.name` attribute of the Cloud or Namespace diff --git a/docs/how-to/create-cluster.md b/docs/how-to/create-cluster.md index 50e1d9b87..8979177a4 100644 --- a/docs/how-to/create-cluster.md +++ b/docs/how-to/create-cluster.md @@ -86,8 +86,8 @@ In this approach, the [Cloud](/docs/envgene-objects.md) is generated from the [C - Collect all required parameters necessary to define the [Cloud](/docs/envgene-objects.md). - Assemble the [Cloud Passport](/docs/envgene-objects.md#cloud-passport) using the expected format. Refer to the sample: - - [cluster-01.yml](/docs/samples/instance-repository/environmentscluster-01/cloud-passport/cluster-01.yml) - - [cluster-01-creds.yml](/docs/samples/instance-repository/environmentscluster-01/cloud-passport/cluster-01-creds.yml) + - [cluster-01.yml](/docs/samples/instance-repository/environments/cluster-01/cloud-passport/cluster-01.yml) + - [cluster-01-creds.yml](/docs/samples/instance-repository/environments/cluster-01/cloud-passport/cluster-01-creds.yml) - Place it under the right location: `/environments//cloud-passport/` Example: @@ -121,7 +121,7 @@ In this approach, the [Cloud](/docs/envgene-objects.md#cloud) is generated using 1. The Instance and Discovery repositories has already been initialized and follows the required structure. 2. Integration with Discovery repository is configured in the Instance repository: - + `/configuration/integration.yml`: ```yaml diff --git a/docs/instance-pipeline-parameters.md b/docs/instance-pipeline-parameters.md index bb4794fbd..fe982fe0e 100644 --- a/docs/instance-pipeline-parameters.md +++ b/docs/instance-pipeline-parameters.md @@ -467,7 +467,7 @@ See details in [Namespace Render Filtering](/docs/features/namespace-render-filt **Description**: Operation identifier in Envgene. Must be a valid [UUID v4](https://www.rfc-editor.org/rfc/rfc4122). This parameter is used in two scenarios: -1. If this parameter is provided, the resulting pipeline commit will include a [Git trailer](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt-code--trailerlttokengtltvaluegtcode) in the format: `DEPLOYMENT_SESSION_ID: `. +1. If this parameter is provided, the resulting pipeline commit will include a [Git trailer](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---trailertokenvalue) in the format: `DEPLOYMENT_SESSION_ID: `. 2. It will also be part of the deployment context of the Effective Set. The EnvGene passes it to the Calculator command-line tool using the `--extra_params` attribute. In this case it is used together with `GENERATE_EFFECTIVE_SET`. **Default Value**: None diff --git a/docs/template-macros.md b/docs/template-macros.md index da5cf8601..3bf6382b3 100644 --- a/docs/template-macros.md +++ b/docs/template-macros.md @@ -1209,7 +1209,7 @@ ${creds.get('').username|password|secret} Where `username`, `password`, and `secret` are **credential fields** that define the type of sensitive data being referenced. -For each `` during Environment Instance generation a [Credential](/docs/envgene-objects.md#credential) object is created in the [Environment Credential File](/docs/envgene-objects.md#environment-credential-file) +For each `` during Environment Instance generation a [Credential](/docs/envgene-objects.md#credential) object is created in the [Environment Credential File](/docs/envgene-objects.md#environment-credentials-file) Type assignment: @@ -1224,7 +1224,7 @@ kafka_password: ${creds.get('kafka-cred').password} k8s_token: ${creds.get('k8s-cred').secret} ``` -**Usage in sample:** [Sample](/docs/samples/templates/parameters/migration/test-deploy-creds.yml) +**Usage in sample:** [Sample](/docs/samples/template-repository/templates/parameters/migration/test-deploy-creds.yml) ## Deprecated Macros diff --git a/docs/test-cases/sd-processing.md b/docs/test-cases/sd-processing.md new file mode 100644 index 000000000..69b437830 --- /dev/null +++ b/docs/test-cases/sd-processing.md @@ -0,0 +1 @@ +# SD Processing Test Cases diff --git a/docs/use-cases/calculator-cli.md b/docs/use-cases/calculator-cli.md index 1b71765d6..1a67e74fb 100644 --- a/docs/use-cases/calculator-cli.md +++ b/docs/use-cases/calculator-cli.md @@ -178,7 +178,7 @@ Instance pipeline (GitLab or GitHub) is started with parameters: ## Parameter Type Preservation in Macro Resolution -This section covers use cases for [Macro Parameter Resolution](/docs/features/calculator-cli.md#version-20-macros) in Effective Set v2.0. The Calculator CLI resolves parameter references while preserving the original parameter types according to [Parameter type conversion](/docs/features/calculator-cli.md#version-20-parameter-type-conversion) rules. +This section covers use cases for [Macro Parameter Resolution](/docs/template-macros.md#calculator-command-line-tool-macros) in Effective Set v2.0. The Calculator CLI resolves parameter references while preserving the original parameter types according to [Parameter type conversion](/docs/features/calculator-cli.md#version-20-parameter-type-conversion) rules. ### UC-CC-MR-1: Simple Type Resolution diff --git a/github_workflows/instance-repo-pipeline/.github/docs/README.md b/github_workflows/instance-repo-pipeline/.github/docs/README.md index 06fc64c26..44ae3616c 100644 --- a/github_workflows/instance-repo-pipeline/.github/docs/README.md +++ b/github_workflows/instance-repo-pipeline/.github/docs/README.md @@ -17,20 +17,20 @@ The EnvGene pipeline (`Envgene.yaml`) is a GitHub Actions workflow that supports ## Available Parameters -GitHub's UI limits manual inputs to 10 parameters. To handle this limitation while maintaining full functionality, we expose the most frequently used parameters directly in the UI and group the remaining parameters within the [GH_ADDITIONAL_PARAMS](https://github.com/Netcracker/qubership-envgene/blob/main/docs/instance-pipeline-parameters.md#gh_additional_params) parameter. +GitHub's UI limits manual inputs to 10 parameters. To handle this limitation while maintaining full functionality, we expose the most frequently used parameters directly in the UI and group the remaining parameters within the [GH_ADDITIONAL_PARAMS](/docs/instance-pipeline-parameters.md#gh_additional_params) parameter. Only a limited number of core parameters are available in the GitHub version of the pipeline: -- [ENV_NAMES](https://github.com/Netcracker/qubership-envgene/blob/main/docs/instance-pipeline-parameters.md#env_names) -- [DEPLOYMENT_TICKET_ID](https://github.com/Netcracker/qubership-envgene/blob/main/docs/instance-pipeline-parameters.md#deployment_ticket_id) -- [ENV_TEMPLATE_VERSION](https://github.com/Netcracker/qubership-envgene/blob/main/docs/instance-pipeline-parameters.md#env_template_version) -- [ENV_BUILDER](https://github.com/Netcracker/qubership-envgene/blob/main/docs/instance-pipeline-parameters.md#env_builder) -- [GENERATE_EFFECTIVE_SET](https://github.com/Netcracker/qubership-envgene/blob/main/docs/instance-pipeline-parameters.md#generate_effective_set) -- [GET_PASSPORT](https://github.com/Netcracker/qubership-envgene/blob/main/docs/instance-pipeline-parameters.md#get_passport) -- [CMDB_IMPORT](https://github.com/Netcracker/qubership-envgene/blob/main/docs/instance-pipeline-parameters.md#cmdb_import) -- [GH_ADDITIONAL_PARAMS](https://github.com/Netcracker/qubership-envgene/blob/main/docs/instance-pipeline-parameters.md#gh_additional_params) +- [ENV_NAMES](/docs/instance-pipeline-parameters.md#env_names) +- [DEPLOYMENT_TICKET_ID](/docs/instance-pipeline-parameters.md#deployment_ticket_id) +- [ENV_TEMPLATE_VERSION](/docs/instance-pipeline-parameters.md#env_template_version) +- [ENV_BUILDER](/docs/instance-pipeline-parameters.md#env_builder) +- [GENERATE_EFFECTIVE_SET](/docs/instance-pipeline-parameters.md#generate_effective_set) +- [GET_PASSPORT](/docs/instance-pipeline-parameters.md#get_passport) +- [CMDB_IMPORT](/docs/instance-pipeline-parameters.md#cmdb_import) +- [GH_ADDITIONAL_PARAMS](/docs/instance-pipeline-parameters.md#gh_additional_params) -The [GH_ADDITIONAL_PARAMS](https://github.com/Netcracker/qubership-envgene/blob/main/docs/instance-pipeline-parameters.md#gh_additional_params) parameter serves as a wrapper for all parameters except those listed above. This approach enables the transmission of all [Instance Pipeline parameters](https://github.com/Netcracker/qubership-envgene/blob/main/docs/instance-pipeline-parameters.md). +The [GH_ADDITIONAL_PARAMS](/docs/instance-pipeline-parameters.md#gh_additional_params) parameter serves as a wrapper for all parameters except those listed above. This approach enables the transmission of all [Instance Pipeline parameters](/docs/instance-pipeline-parameters.md). ## How to Trigger the Pipeline @@ -96,7 +96,7 @@ When parameter keys from different sources overlap, their values are replaced ac Best practices for setting variables are: - Define in CI/CD variables [EnvGene repository variables](https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-repository-variables.md) -- Pass through GitHub Actions UI or GitHub API Call [Instance Pipeline parameters](https://github.com/Netcracker/qubership-envgene/blob/main/docs/instance-pipeline-parameters.md) +- Pass through GitHub Actions UI or GitHub API Call [Instance Pipeline parameters](/docs/instance-pipeline-parameters.md) - Use `pipeline_vars.env` for debug purposes ## `pipeline_vars.env` @@ -110,7 +110,7 @@ ENV_SPECIFIC_PARAMS={"clusterParams":{"clusterEndpoint":"","clusterToken" EFFECTIVE_SET_CONFIG={\"version\": \"v2.0\", \"app_chart_validation\": \"false\"} ``` -Variables set in this file must NOT be wrapped with [GH_ADDITIONAL_PARAMS](https://github.com/Netcracker/qubership-envgene/blob/main/docs/instance-pipeline-parameters.md#gh_additional_params) +Variables set in this file must NOT be wrapped with [GH_ADDITIONAL_PARAMS](/docs/instance-pipeline-parameters.md#gh_additional_params) ## Pipeline Customization From 3f1d77e06e943d25e9e3bc3b9c6228348045b659 Mon Sep 17 00:00:00 2001 From: miyamuraga <198181742+miyamuraga@users.noreply.github.com> Date: Mon, 9 Feb 2026 13:29:34 +0500 Subject: [PATCH 015/161] feat: ansible replacement remains (#951) --- base_modules/scripts/decrypt_fernet.py | 1 - build_envgene/ansible/ansible.cfg | 21 ---------- .../ansible/playbooks/git_commit.yaml | 7 ---- .../roles/git_commit/defaults/main.yaml | 18 --------- .../git_commit/tasks/01_prepare_vars.yaml | 4 -- .../roles/git_commit/tasks/02_git_commit.yaml | 10 ----- .../ansible/roles/git_commit/tasks/main.yaml | 6 --- build_envgene/build/Dockerfile | 39 +++---------------- build_envgene/build/requirements.txt | 4 +- build_envgene/build/requirements.yml | 5 --- build_envgene/scripts/prepare.sh | 37 ------------------ build_pipegene/scripts/env_build_jobs.py | 16 ++++---- build_pipegene/scripts/gitlab_ci.py | 11 ++++-- .../scripts/inventory_generation_job.py | 3 -- build_pipegene/scripts/passport_jobs.py | 8 ++-- dependencies/tests_requirements.txt | 2 - .../.github/configuration/config.env | 3 -- .../.gitlab-ci.yml | 3 -- scripts/utils/pipeline_parameters.py | 2 +- 19 files changed, 25 insertions(+), 175 deletions(-) delete mode 100644 build_envgene/ansible/ansible.cfg delete mode 100644 build_envgene/ansible/playbooks/git_commit.yaml delete mode 100644 build_envgene/ansible/roles/git_commit/defaults/main.yaml delete mode 100644 build_envgene/ansible/roles/git_commit/tasks/01_prepare_vars.yaml delete mode 100644 build_envgene/ansible/roles/git_commit/tasks/02_git_commit.yaml delete mode 100644 build_envgene/ansible/roles/git_commit/tasks/main.yaml delete mode 100644 build_envgene/build/requirements.yml delete mode 100755 build_envgene/scripts/prepare.sh diff --git a/base_modules/scripts/decrypt_fernet.py b/base_modules/scripts/decrypt_fernet.py index 86aa61192..9d6d60f1c 100644 --- a/base_modules/scripts/decrypt_fernet.py +++ b/base_modules/scripts/decrypt_fernet.py @@ -16,7 +16,6 @@ def cmdb_prepare(): @click.option('--secret_key', '-s', 'secret_key', required=True, help="Set secret_key for encrypt cred files") def decrypt_file(secret_key, file_path): - ''' {getenv('CI_PROJECT_DIR')}/ansible/inventory/group_vars/{getenv('env_name')}/appdeployer_cmdb/Tenants/{getenv('tenant_name')}/Credentials''' logger.debug('Try to read %s file', file_path) with open(file_path, mode="r", encoding="utf-8") as sensitive: sensitive_data = safe_load(sensitive) diff --git a/build_envgene/ansible/ansible.cfg b/build_envgene/ansible/ansible.cfg deleted file mode 100644 index d0c087556..000000000 --- a/build_envgene/ansible/ansible.cfg +++ /dev/null @@ -1,21 +0,0 @@ -[defaults] -callbacks_enabled = ansible.posix.timer,ansible.posix.profile_tasks -force_color = 1 -host_key_checking = False -local_tmp = /module/ansible/tmp -retry_files_enabled = False -roles_path = /module/ansible/roles -collections_paths = /module/ansible/collections -stdout_callback = ansible.posix.debug -timeout = 300 -filter_plugins = /module/ansible/filter_plugins - - -[galaxy] -cache_dir=/module/ansible/galaxy_cache -token_path=/module/ansible/galaxy_token - -[ssh_connection] -pipelining = true -retries = 7 -ssh_args = -o ControlMaster=auto -oConnectTimeout=30 -o ControlPersist=60s -C -o PreferredAuthentications=publickey,password diff --git a/build_envgene/ansible/playbooks/git_commit.yaml b/build_envgene/ansible/playbooks/git_commit.yaml deleted file mode 100644 index 99ee59051..000000000 --- a/build_envgene/ansible/playbooks/git_commit.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -- name: Commit and push changes to git - hosts: localhost - connection: local - gather_facts: false - roles: - - role: git_commit diff --git a/build_envgene/ansible/roles/git_commit/defaults/main.yaml b/build_envgene/ansible/roles/git_commit/defaults/main.yaml deleted file mode 100644 index 6cf3014fd..000000000 --- a/build_envgene/ansible/roles/git_commit/defaults/main.yaml +++ /dev/null @@ -1,18 +0,0 @@ -### default paths -artifact_dest: /tmp/artifact.zip -build_env_path: /build_env -envgen_debug: "{{ lookup('env', 'envgen_debug') }}" - -env_name: "{{ lookup('env', 'ENV_NAME') }}" -cluster_name: "{{ lookup('env', 'CLUSTER_NAME') }}" -environment_name: "{{ lookup('env', 'ENVIRONMENT_NAME') }}" -base_dir: "{{ lookup('env', 'CI_PROJECT_DIR') }}" -env_template_vers: "{{ lookup('env', 'ENV_TEMPLATE_VERSION') }}" - -### configuration files -envs_directory_path: "{{ base_dir }}/environments" -registry_config_path: "{{ base_dir }}/configuration/registry.yml" -cred_config_path: "{{ base_dir }}/configuration/credentials/credentials.yml" - -instance_secret_key: "{{ lookup('env', 'SECRET_KEY') }}" -COMMIT_ENV: "{{ lookup('env', 'COMMIT_ENV)') }}" diff --git a/build_envgene/ansible/roles/git_commit/tasks/01_prepare_vars.yaml b/build_envgene/ansible/roles/git_commit/tasks/01_prepare_vars.yaml deleted file mode 100644 index b334cdfb8..000000000 --- a/build_envgene/ansible/roles/git_commit/tasks/01_prepare_vars.yaml +++ /dev/null @@ -1,4 +0,0 @@ ---- -- name: Set env_definition path - set_fact: - env_definition_path: "{{ envs_directory_path + '/' + cluster_name + '/' + environment_name }}" diff --git a/build_envgene/ansible/roles/git_commit/tasks/02_git_commit.yaml b/build_envgene/ansible/roles/git_commit/tasks/02_git_commit.yaml deleted file mode 100644 index 23fd34fc3..000000000 --- a/build_envgene/ansible/roles/git_commit/tasks/02_git_commit.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -### script arguments -- name: Set script arguments - set_fact: - script_args: "--env_definition_path={{ env_definition_path }} --version_to_add={{ env_template_vers }}" - -- name: 03.1 Commit and push changes to git - shell: | - cd ${CI_PROJECT_DIR} - . /module/scripts/git_commit.sh diff --git a/build_envgene/ansible/roles/git_commit/tasks/main.yaml b/build_envgene/ansible/roles/git_commit/tasks/main.yaml deleted file mode 100644 index 0e8145c34..000000000 --- a/build_envgene/ansible/roles/git_commit/tasks/main.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -- name: 01. prepare vars - include_tasks: 01_prepare_vars.yaml - -- name: 02. git commit - include_tasks: 02_git_commit.yaml diff --git a/build_envgene/build/Dockerfile b/build_envgene/build/Dockerfile index 6857fc417..46dd349e6 100644 --- a/build_envgene/build/Dockerfile +++ b/build_envgene/build/Dockerfile @@ -27,13 +27,11 @@ RUN apk add --no-cache \ # Copy configuration files COPY build_envgene/build/pip.conf /etc/pip.conf COPY build_envgene/build/requirements.txt /build/requirements.txt -COPY build_envgene/build/requirements.yml /build/requirements.yml COPY build_envgene/build/constraint.txt /build/constraint.txt COPY creds_rotation/build/requirements.txt /build/creds_rotation_requirements.txt # Copy source code COPY python /python -COPY build_envgene/ansible /module/ansible COPY build_envgene/scripts /module/scripts COPY scripts/bg_manage /scripts/bg_manage COPY creds_rotation/scripts /module/creds_rotation_scripts @@ -44,21 +42,10 @@ COPY scripts/cloud_passport/ /cloud_passport/scripts/ COPY schemas /build_env/schemas COPY scripts/utils /module/scripts/utils -ENV ANSIBLE_LIBRARY=/module/ansible/library - # Create virtual environment and install Python packages RUN python -m venv /module/venv RUN /module/venv/bin/pip install --upgrade pip setuptools wheel RUN /module/venv/bin/pip install --no-cache-dir --retries 10 --timeout 60 -r /build/requirements.txt -# Install essential Ansible collections -# Install to virtual environment site-packages for Python module access -RUN /module/venv/bin/ansible-galaxy collection install ansible.utils -p /module/venv/lib/python3.12/site-packages/ansible_collections -RUN /module/venv/bin/ansible-galaxy collection install ansible.posix -p /module/venv/lib/python3.12/site-packages/ansible_collections -RUN /module/venv/bin/ansible-galaxy collection install community.general -p /module/venv/lib/python3.12/site-packages/ansible_collections -# Also install to custom location for playbook usage -RUN /module/venv/bin/ansible-galaxy collection install ansible.utils -p /module/ansible/collections -RUN /module/venv/bin/ansible-galaxy collection install ansible.posix -p /module/ansible/collections -RUN /module/venv/bin/ansible-galaxy collection install community.general -p /module/ansible/collections RUN /module/venv/bin/pip install /python/jschon-sort RUN /module/venv/bin/pip install /python/envgene @@ -77,18 +64,13 @@ RUN apk del gcc musl-dev libffi-dev openssl-dev libxml2-dev libxslt-dev zlib-dev RUN rm -rf /var/cache/apk/* /tmp/* /var/tmp/* /root/.cache # Remove unnecessary files from Python packages RUN find /module/venv/lib/python3.12/site-packages -name '*.pyc' -delete -# Don't remove test directories as they might be needed by Ansible + RUN find /module/venv/lib/python3.12/site-packages -name '*.pyo' -delete RUN find /module/venv/lib/python3.12/site-packages -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null || true -# Remove heavy Ansible collections that are not essential (but keep ansible.posix and ansible.utils) -RUN rm -rf /module/venv/lib/python3.12/site-packages/ansible_collections/amazon /module/venv/lib/python3.12/site-packages/ansible_collections/azure /module/venv/lib/python3.12/site-packages/ansible_collections/google /module/venv/lib/python3.12/site-packages/ansible_collections/kubernetes 2>/dev/null || true -# Remove test packages that are not needed in runtime (but keep Ansible test files) -RUN rm -rf /module/venv/lib/python3.12/site-packages/pytest* /module/venv/lib/python3.12/site-packages/_pytest* 2>/dev/null || true +RUN rm -rf /module/venv/lib/python3.12/site-packages/pytest* \ + /module/venv/lib/python3.12/site-packages/_pytest* 2>/dev/null || true RUN /module/venv/bin/pip cache purge -# Verify collections are still accessible after cleanup -RUN /module/venv/bin/python -c "import ansible_collections.ansible.posix; print('ansible.posix collection still accessible after cleanup')" - # Set permissions RUN chmod 754 /module/scripts/* RUN chmod 754 /module/creds_rotation_scripts/* @@ -125,9 +107,6 @@ COPY --from=build /cloud_passport /cloud_passport COPY --from=build /python /python COPY --from=build /etc/pip.conf /etc/pip.conf -# Verify collections are accessible in runtime stage -RUN /module/venv/bin/python -c "import ansible_collections.ansible.posix; print('ansible.posix collection accessible in runtime')" - # Set permissions RUN chmod +x /usr/local/bin/sops @@ -147,21 +126,15 @@ RUN mkdir -p /__w/_temp/_runner_file_commands && \ # Final cleanup RUN rm -rf /var/cache/apk/* /tmp/* /var/tmp/* /root/.cache RUN find /module/venv/lib/python3.12/site-packages -name '*.pyc' -delete -# Don't remove test directories as they might be needed by Ansible RUN /module/venv/bin/pip cache purge # Keep pip for runtime compatibility, but remove setuptools and wheel -RUN rm -rf /module/venv/lib/python3.12/site-packages/setuptools* /module/venv/lib/python3.12/site-packages/wheel* 2>/dev/null || true +RUN rm -rf /module/venv/lib/python3.12/site-packages/setuptools* \ + /module/venv/lib/python3.12/site-packages/wheel* 2>/dev/null || true # Set environment ENV PATH=/module/venv/bin:$PATH \ PYTHONUNBUFFERED=1 \ - PYTHONDONTWRITEBYTECODE=1 \ - ANSIBLE_LIBRARY=/module/ansible/library \ - ANSIBLE_COLLECTIONS_PATH=/module/venv/lib/python3.12/site-packages/ansible_collections:/module/ansible/collections - -# Simple root-based container for CI/CD environments -# This container runs as root to avoid permission issues in CI/CD pipelines -WORKDIR /module/ansible + PYTHONDONTWRITEBYTECODE=1 # Health check HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ diff --git a/build_envgene/build/requirements.txt b/build_envgene/build/requirements.txt index daea4326f..5addb5d52 100644 --- a/build_envgene/build/requirements.txt +++ b/build_envgene/build/requirements.txt @@ -13,19 +13,17 @@ jsonschema==4.19.1 jmespath==1.0.1 semantic-version==2.10.0 termcolor==2.4.0 -ansible-core==2.17.12 cffi==1.16.0 click==8.1.3 deepmerge==2.0 GitPython==3.1.45 pydantic==2.10.6 +Jinja2==3.1.6 # Additional required packages platformdirs>=3.0.0 -ansible-runner==2.4.0 # Removed heavy packages: # - shyaml, yamale, prettytable (not essential) # - ruyaml (duplicate of ruamel.yaml) # - diagrams (heavy with typed-ast dependency) -# - ansible-base (replaced with ansible-core) diff --git a/build_envgene/build/requirements.yml b/build_envgene/build/requirements.yml deleted file mode 100644 index 41925492f..000000000 --- a/build_envgene/build/requirements.yml +++ /dev/null @@ -1,5 +0,0 @@ -collections: - - name: community.general - version: 7.0.1 - - name: ansible.posix - version: 1.5.4 diff --git a/build_envgene/scripts/prepare.sh b/build_envgene/scripts/prepare.sh deleted file mode 100755 index 7eb90cb44..000000000 --- a/build_envgene/scripts/prepare.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -set -e - -#### input variables -# envgen_debug -# envgen_args -# module_ansible_dir -# module_ansible_cfg -# module_inventory -# CI_SERVER_URL -# GITLAB_TOKEN - -playbook_name=$1 -ansible_dir=${module_ansible_dir} - -if ${envgen_debug} ; then set -o xtrace; fi - -chmod 700 "$ansible_dir" -cd "$ansible_dir" - -export ANSIBLE_CONFIG=${module_ansible_cfg} - -#### Run ansible -echo "ansible-playbook playbooks/$playbook_name -i ${module_inventory} ${envgen_args}" -if ansible-playbook "playbooks/$playbook_name" -i "${module_inventory}" ${envgen_args}; then - status=0 -else - status=$? -fi - -mkdir -p "$CI_PROJECT_DIR/build_env/tmp" -if [ -d "/build_env/tmp" ]; then - cp -r /build_env/tmp/* "$CI_PROJECT_DIR/build_env/tmp/" || true -fi - -exit $status - diff --git a/build_pipegene/scripts/env_build_jobs.py b/build_pipegene/scripts/env_build_jobs.py index 3dbe4f66e..ddae336ec 100644 --- a/build_pipegene/scripts/env_build_jobs.py +++ b/build_pipegene/scripts/env_build_jobs.py @@ -3,11 +3,12 @@ from pipeline_helper import job_instance -def prepare_env_build_job(pipeline, is_template_test, full_env, enviroment_name, cluster_name, group_id, artifact_id, tags): +def prepare_env_build_job(pipeline, is_template_test, full_env, enviroment_name, cluster_name, group_id, artifact_id, + tags): logger.info(f'prepare env_build job for {full_env}') script = [ - '/module/scripts/handle_certs.sh', + '/module/scripts/handle_certs.sh', ] script.append('cd /build_env; python3 /build_env/scripts/build_env/main.py') @@ -15,7 +16,7 @@ def prepare_env_build_job(pipeline, is_template_test, full_env, enviroment_name, script.append('env_name=$(cat "$CI_PROJECT_DIR/set_variable.txt")') script.append( 'sed -i "s|\\\"envgeneNullValue\\\"|\\\"test_value\\\"|g" "$CI_PROJECT_DIR/environments/$env_name/Credentials/credentials.yml"') - + env_build_params = { "name": f'env_builder.{full_env}', "image": '${envgen_image}', @@ -96,12 +97,12 @@ def prepare_git_commit_job(pipeline, full_env, enviroment_name, cluster_name, de "stage": 'git_commit', "script": [ '/module/scripts/handle_certs.sh', - '/module/scripts/prepare.sh "git_commit.yaml"', + '/module/scripts/git_commit.sh', "export env_name=$(echo $ENV_NAME | awk -F '/' '{print $NF}')", 'env_path=$(sudo find $CI_PROJECT_DIR/environments -type d -name "$env_name")', 'for path in $env_path; do if [ -d "$path/Credentials" ]; then sudo chmod ugo+rw $path/Credentials/*; fi; done', 'cp -rf $CI_PROJECT_DIR/environments $CI_PROJECT_DIR/git_envs', - ], + ], } git_commit_vars = { @@ -111,9 +112,6 @@ def prepare_git_commit_job(pipeline, full_env, enviroment_name, cluster_name, de "envgen_image": "$envgen_image", "envgen_args": " -vv", "envgen_debug": "true", - "module_ansible_dir": "/module/ansible", - "module_inventory": "${CI_PROJECT_DIR}/configuration/inventory.yaml", - "module_ansible_cfg": "/module/ansible/ansible.cfg", "module_config_default": "/module/templates/defaults.yaml", "GIT_STRATEGY": "none", "COMMIT_ENV": "true", @@ -128,4 +126,4 @@ def prepare_git_commit_job(pipeline, full_env, enviroment_name, cluster_name, de if (credential_rotation_job is not None): git_commit_job.add_needs(credential_rotation_job) pipeline.add_children(git_commit_job) - return git_commit_job \ No newline at end of file + return git_commit_job diff --git a/build_pipegene/scripts/gitlab_ci.py b/build_pipegene/scripts/gitlab_ci.py index 140aac2aa..0f5019dd0 100644 --- a/build_pipegene/scripts/gitlab_ci.py +++ b/build_pipegene/scripts/gitlab_ci.py @@ -121,8 +121,8 @@ def build_pipeline(params: dict) -> None: cluster_name, tags) jobs_map["credential_rotation_job"] = credential_rotation_job else: - logger.info(f'Credential rotation job for {full_env_name} is skipped because CRED_ROTATION_PAYLOAD is empty.') - + logger.info( + f'Credential rotation job for {full_env_name} is skipped because CRED_ROTATION_PAYLOAD is empty.') if params['ENV_BUILD']: jobs_map["appregdef_render_job"] = prepare_appregdef_render_job(pipeline, params['IS_TEMPLATE_TEST'], @@ -133,8 +133,11 @@ def build_pipeline(params: dict) -> None: else: logger.info(f'Preparing of appregdef_render_job {full_env_name} is skipped.') - if (params["SD_SOURCE_TYPE"].lower() == "json" and params["SD_DATA"]) or \ - (params["SD_SOURCE_TYPE"].lower() == "artifact" and params["SD_VERSION"]): + source_type = (params.get("SD_SOURCE_TYPE", "artifact")).lower() + if ( + (source_type == "json" and params.get("SD_DATA")) or + (source_type == "artifact" and params.get("SD_VERSION")) + ): jobs_map["process_sd_job"] = prepare_process_sd(pipeline, full_env_name, environment_name, cluster_name, params["APP_DEFS_PATH"], params["REG_DEFS_PATH"], tags) else: diff --git a/build_pipegene/scripts/inventory_generation_job.py b/build_pipegene/scripts/inventory_generation_job.py index 8d59703ae..312cb91f9 100644 --- a/build_pipegene/scripts/inventory_generation_job.py +++ b/build_pipegene/scripts/inventory_generation_job.py @@ -47,9 +47,6 @@ def prepare_inventory_generation_job(pipeline, full_env_name, environment_name, "envgen_image": "$envgen_image", "envgen_args": " -vv", "envgen_debug": "true", - "module_ansible_dir": "/module/ansible", - "module_inventory": "${CI_PROJECT_DIR}/configuration/inventory.yaml", - "module_ansible_cfg": "/module/ansible/ansible.cfg", "module_config_default": "/module/templates/defaults.yaml", "GITLAB_RUNNER_TAG_NAME": tags, **env_generation_params diff --git a/build_pipegene/scripts/passport_jobs.py b/build_pipegene/scripts/passport_jobs.py index 0d6499eee..e7bc24d91 100644 --- a/build_pipegene/scripts/passport_jobs.py +++ b/build_pipegene/scripts/passport_jobs.py @@ -45,7 +45,7 @@ def prepare_passport_job(pipeline, full_env, enviroment_name, cluster_name, tags 'for path in $env_path; do if [ -d "$path/Credentials" ]; then sudo chmod ugo+rw $path/Credentials/*; fi; done' ], } - get_passport_params['script'].append('/module/scripts/prepare.sh "git_commit.yaml"') + get_passport_params['script'].append('/module/scripts/git_commit.sh') get_passport_vars = { "ENV_NAME": full_env, "CLUSTER_NAME": cluster_name, @@ -53,13 +53,10 @@ def prepare_passport_job(pipeline, full_env, enviroment_name, cluster_name, tags "envgen_image": "$envgen_image", "envgen_args": " -vv", "envgen_debug": "true", - "module_inventory": "${CI_PROJECT_DIR}/configuration/inventory.yaml", "module_config_default": "/module/templates/defaults.yaml", "COMMIT_ENV": "false", "COMMIT_MESSAGE": f"[ci_skip] update cloud passport for {cluster_name}", - "GITLAB_RUNNER_TAG_NAME": tags, - "module_ansible_dir": "/module/ansible", - "module_ansible_cfg": "/module/ansible/ansible.cfg" + "GITLAB_RUNNER_TAG_NAME": tags } get_passport_job = job_instance(params=get_passport_params, vars=get_passport_vars) base = "${CI_PROJECT_DIR}/environments" @@ -68,3 +65,4 @@ def prepare_passport_job(pipeline, full_env, enviroment_name, cluster_name, tags get_passport_job.artifacts.when = WhenStatement.ALWAYS pipeline.add_children(get_passport_job) return get_passport_job + diff --git a/dependencies/tests_requirements.txt b/dependencies/tests_requirements.txt index 73b663ee1..b37d7ad4a 100644 --- a/dependencies/tests_requirements.txt +++ b/dependencies/tests_requirements.txt @@ -23,8 +23,6 @@ referencing==0.33.0 rpds-py==0.17.1 jsonschema-specifications==2023.12.1 cryptography==41.0.3 -ansible-core==2.17.12 -ansible_runner==2.3.5 pytest==7.4.3 junitparser==3.1.2 hiyapyco==0.6.0 diff --git a/github_workflows/instance-repo-pipeline/.github/configuration/config.env b/github_workflows/instance-repo-pipeline/.github/configuration/config.env index 8bc9db08b..825824b2d 100644 --- a/github_workflows/instance-repo-pipeline/.github/configuration/config.env +++ b/github_workflows/instance-repo-pipeline/.github/configuration/config.env @@ -8,7 +8,4 @@ PROJECT_DIR=/workspace SECRET_POSTFIX=custom_secret envgen_args=-vvv envgen_debug=true -module_ansible_cfg=/module/ansible/ansible.cfg -module_ansible_dir=/module/ansible module_config_default=/module/templates/defaults.yaml -module_inventory=/workspace/configuration/inventory.yaml diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/.gitlab-ci.yml b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/.gitlab-ci.yml index 2a3d0ae1d..05b5aaa00 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/.gitlab-ci.yml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/.gitlab-ci.yml @@ -32,9 +32,6 @@ default: .common_module.variables: variables: - module_ansible_dir: "/module/ansible" - module_inventory: "${CI_PROJECT_DIR}/configuration/inventory.yaml" - module_ansible_cfg: "/module/ansible/ansible.cfg" module_config_default: "/module/templates/defaults.yaml" .images.variables: diff --git a/scripts/utils/pipeline_parameters.py b/scripts/utils/pipeline_parameters.py index f0c43e2cb..6aa9d9ce0 100644 --- a/scripts/utils/pipeline_parameters.py +++ b/scripts/utils/pipeline_parameters.py @@ -15,7 +15,7 @@ def get_pipeline_parameters() -> dict: 'IS_TEMPLATE_TEST': getenv("ENV_TEMPLATE_TEST") == "true", 'CI_COMMIT_REF_NAME': getenv("CI_COMMIT_REF_NAME", ""), 'JSON_SCHEMAS_DIR': getenv("JSON_SCHEMAS_DIR", "/module/schemas"), - "SD_SOURCE_TYPE": getenv("SD_SOURCE_TYPE"), + "SD_SOURCE_TYPE": getenv("SD_SOURCE_TYPE") or "artifact", "SD_VERSION": getenv("SD_VERSION"), "SD_DATA": getenv("SD_DATA"), "SD_DELTA": getenv("SD_DELTA"), From cbbbc6de43ff327590b813592fa003d98f5bad60 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Mon, 9 Feb 2026 08:36:49 +0000 Subject: [PATCH 016/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 6ede13c35..89d10a2d7 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.22.2" - DOCKER_IMAGE_TAG_ENVGENE: "1.22.2" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.22.2" + DOCKER_IMAGE_TAG_PIPEGENE: "1.23.0" + DOCKER_IMAGE_TAG_ENVGENE: "1.23.0" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.23.0" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 0bc359ad5..76a6bb856 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.22.2 +version: 1.23.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 8ac215df2..850b694b6 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.22.2 +version: 1.23.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 3e3da8563..9dfa3154a 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.22.2", + "envgene_version": "1.23.0", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 223aeffae7c9df39fbddaa7d1f0412766ec1a6c9 Mon Sep 17 00:00:00 2001 From: popoveugene <42543333+popoveugene@users.noreply.github.com> Date: Mon, 9 Feb 2026 11:39:00 +0300 Subject: [PATCH 017/161] docs: mark external job as deprecated (#1001) * docs: update * docs: wip * docs: wip * docs: wip * docs: wip --- docs/features/app-reg-defs.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/features/app-reg-defs.md b/docs/features/app-reg-defs.md index 52f141ad4..de6f42d13 100644 --- a/docs/features/app-reg-defs.md +++ b/docs/features/app-reg-defs.md @@ -46,6 +46,9 @@ There are two sources for obtaining Application and Registry Definitions in EnvG #### External Job +> [!WARNING] +> The External Job–based mechanism is **deprecated**, is not recommended for use in new or actively maintained environments, and is planned to be removed in a future EnvGene release. Consumers should migrate to template-based Application and Registry Definitions as soon as reasonably possible. + An external job (not implemented in EnvGene itself, but serves as an extension point) that somehow creates/discovers/generates Application and Registry Definitions as YAML files and saves them in its artifact with the contract name `definitions.zip`. During the [`app_reg_def_process`](/docs/envgene-pipelines.md#instance-pipeline) job execution, EnvGene retrieves the Application and Registry Definitions from this artifact and saves them as part of the Environment instance. From 82ce9cd59f1acb2931e4c3575544ab45c5563a10 Mon Sep 17 00:00:00 2001 From: GeethaGadde99 Date: Wed, 4 Feb 2026 13:19:50 +0530 Subject: [PATCH 018/161] feat: Enhance Test data for Calculator CLI --- .../monitoring-origin/namespace.yml | 8 + .../cleanup/monitoring-origin/parameters.yaml | 28 ++++ .../effective-set/cleanup/pg/parameters.yaml | 20 +++ .../values/deployment-parameters.yaml | 146 ++++++++++++------ .../values/deployment-parameters.yaml | 50 +++++- .../environments/cluster-01/pl-01/tenant.yml | 14 +- 6 files changed, 209 insertions(+), 57 deletions(-) diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/Namespaces/monitoring-origin/namespace.yml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/Namespaces/monitoring-origin/namespace.yml index d794157dc..9bcb4ddc9 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/Namespaces/monitoring-origin/namespace.yml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/Namespaces/monitoring-origin/namespace.yml @@ -13,6 +13,14 @@ profile: name: "dev_bss_override" baseline: "dev" deployParameters: + server_port: 8080 + app_version: "3.0" + ssl_enabled: true + debug_mode_test: "true" + api_port: ${server_port} + service_version: ${app_version} + use_ssl: ${ssl_enabled} + log_level: ${debug_mode_test} ENVGENE_CONFIG_REF_NAME: "branch_name" ENVGENE_CONFIG_TAG: "No Ref tag" KMS_CERT_IN_BASE64: "${creds.get( \"kms-cert\" ).secret}" # paramset: test-deploy-creds version: 1 source: template diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/monitoring-origin/parameters.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/monitoring-origin/parameters.yaml index 466dd98f9..3b281204b 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/monitoring-origin/parameters.yaml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/monitoring-origin/parameters.yaml @@ -73,9 +73,37 @@ TRACING_HOST: tracing-agent TRACING_UI_URL: https://cluster-01.qubership.org ZOOKEEPER_ADDRESS: zookeeper.zookeeper:2181 ZOOKEEPER_URL: zookeeper.zookeeper:2181 +api_config: + connection: + host: db.example.com + port: 5432 +api_port: 8080 +app_version: '3.0' bss-app-exist: false core: apps: volumes: outputs: capacity: 20Gi +database_config: + connection: + host: db.example.com + port: 5432 +debug_mode_test: 'true' +log_level: 'true' +rendered_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 +server_port: 8080 +service_version: '3.0' +ssl_enabled: true +use_ssl: true +yaml_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/pg/parameters.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/pg/parameters.yaml index 38eb6ae59..bfdf1880c 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/pg/parameters.yaml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/pg/parameters.yaml @@ -68,3 +68,23 @@ TRACING_HOST: tracing-agent TRACING_UI_URL: https://cluster-01.qubership.org ZOOKEEPER_ADDRESS: zookeeper.zookeeper:2181 ZOOKEEPER_URL: zookeeper.zookeeper:2181 +api_config: + connection: + host: db.example.com + port: 5432 +database_config: + connection: + host: db.example.com + port: 5432 +rendered_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 +yaml_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/monitoring-origin/MONITORING/values/deployment-parameters.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/monitoring-origin/MONITORING/values/deployment-parameters.yaml index d0aaf505b..2bf96010b 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/monitoring-origin/MONITORING/values/deployment-parameters.yaml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/monitoring-origin/MONITORING/values/deployment-parameters.yaml @@ -74,13 +74,41 @@ TRACING_HOST: tracing-agent TRACING_UI_URL: https://cluster-01.qubership.org ZOOKEEPER_ADDRESS: zookeeper.zookeeper:2181 ZOOKEEPER_URL: zookeeper.zookeeper:2181 +api_config: &id001 + connection: + host: db.example.com + port: 5432 +api_port: 8080 +app_version: '3.0' bss-app-exist: false -core: &id001 +core: &id002 apps: volumes: outputs: capacity: 20Gi -global: &id002 +database_config: &id003 + connection: + host: db.example.com + port: 5432 +debug_mode_test: 'true' +log_level: 'true' +rendered_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 +server_port: 8080 +service_version: '3.0' +ssl_enabled: true +use_ssl: true +yaml_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 +global: &id004 API_DBAAS_ADDRESS: http://dbaas.dbaas:8080 APPLICATION_NAME: MONITORING ARTIFACT_DESCRIPTOR_ARTIFACT_ID: prod.platform.system.monitoring_monitoring-operator @@ -157,50 +185,72 @@ global: &id002 TRACING_UI_URL: https://cluster-01.qubership.org ZOOKEEPER_ADDRESS: zookeeper.zookeeper:2181 ZOOKEEPER_URL: zookeeper.zookeeper:2181 + api_config: *id001 + api_port: 8080 + app_version: '3.0' bss-app-exist: false - core: *id001 -alertmanager: *id002 -blackbox-exporter: *id002 -cert-exporter: *id002 -cloud-events-exporter: *id002 -cloud-events-reader: *id002 -cloudwatch-exporter: *id002 -common-dashboards: *id002 -configmap-reload: *id002 -configurations-streamer: *id002 -grafana: *id002 -grafana-image-renderer: *id002 -grafana-operator: *id002 -grafana-plugins-init: *id002 -graphite-remote-adapter: *id002 -json-exporter: *id002 -kube-rbac-proxy: *id002 -kube-state-metrics: *id002 -monitoring-operator: *id002 -network-latency-exporter: *id002 -node-exporter: *id002 -oauth2-proxy: *id002 -platform_monitoring_tests: *id002 -prometheus: *id002 -prometheus-adapter: *id002 -prometheus-adapter-converter: *id002 -prometheus-adapter-operator: *id002 -prometheus-config-reloader: *id002 -prometheus-operator: *id002 -promitor-agent-resource-discovery: *id002 -promitor-agent-scraper: *id002 -promxy: *id002 -pushgateway: *id002 -stackdriver-exporter: *id002 -version-exporter: *id002 -victoriametrics-operator: *id002 -vmagent: *id002 -vmalert: *id002 -vmauth: *id002 -vmcleanup: *id002 -vminsert: *id002 -vmoperator: *id002 -vmoperator_config_reloader: *id002 -vmselect: *id002 -vmsingle: *id002 -vmstorage: *id002 + core: *id002 + database_config: *id003 + debug_mode_test: 'true' + log_level: 'true' + rendered_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 + server_port: 8080 + service_version: '3.0' + ssl_enabled: true + use_ssl: true + yaml_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 +alertmanager: *id004 +blackbox-exporter: *id004 +cert-exporter: *id004 +cloud-events-exporter: *id004 +cloud-events-reader: *id004 +cloudwatch-exporter: *id004 +common-dashboards: *id004 +configmap-reload: *id004 +configurations-streamer: *id004 +grafana: *id004 +grafana-image-renderer: *id004 +grafana-operator: *id004 +grafana-plugins-init: *id004 +graphite-remote-adapter: *id004 +json-exporter: *id004 +kube-rbac-proxy: *id004 +kube-state-metrics: *id004 +monitoring-operator: *id004 +network-latency-exporter: *id004 +node-exporter: *id004 +oauth2-proxy: *id004 +platform_monitoring_tests: *id004 +prometheus: *id004 +prometheus-adapter: *id004 +prometheus-adapter-converter: *id004 +prometheus-adapter-operator: *id004 +prometheus-config-reloader: *id004 +prometheus-operator: *id004 +promitor-agent-resource-discovery: *id004 +promitor-agent-scraper: *id004 +promxy: *id004 +pushgateway: *id004 +stackdriver-exporter: *id004 +version-exporter: *id004 +victoriametrics-operator: *id004 +vmagent: *id004 +vmalert: *id004 +vmauth: *id004 +vmcleanup: *id004 +vminsert: *id004 +vmoperator: *id004 +vmoperator_config_reloader: *id004 +vmselect: *id004 +vmsingle: *id004 +vmstorage: *id004 diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/pg/postgres/values/deployment-parameters.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/pg/postgres/values/deployment-parameters.yaml index c11ace9fc..dcbe130aa 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/pg/postgres/values/deployment-parameters.yaml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/pg/postgres/values/deployment-parameters.yaml @@ -69,7 +69,27 @@ TRACING_HOST: tracing-agent TRACING_UI_URL: https://cluster-01.qubership.org ZOOKEEPER_ADDRESS: zookeeper.zookeeper:2181 ZOOKEEPER_URL: zookeeper.zookeeper:2181 -global: &id001 +api_config: &id001 + connection: + host: db.example.com + port: 5432 +database_config: &id002 + connection: + host: db.example.com + port: 5432 +rendered_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 +yaml_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 +global: &id003 API_DBAAS_ADDRESS: http://dbaas.dbaas:8080 APPLICATION_NAME: postgres ARTIFACT_DESCRIPTOR_ARTIFACT_ID: prod.platform.ha.postgres @@ -141,10 +161,24 @@ global: &id001 TRACING_UI_URL: https://cluster-01.qubership.org ZOOKEEPER_ADDRESS: zookeeper.zookeeper:2181 ZOOKEEPER_URL: zookeeper.zookeeper:2181 -patroni-core: *id001 -pg_patroni: *id001 -pg_upgrade: *id001 -pgbackrest_sidecar: *id001 -postgres_operator_init: *id001 -postgres_operator_tests: *id001 -vault_env: *id001 + api_config: *id001 + database_config: *id002 + rendered_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 + yaml_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 +patroni-core: *id003 +pg_patroni: *id003 +pg_upgrade: *id003 +pgbackrest_sidecar: *id003 +postgres_operator_init: *id003 +postgres_operator_tests: *id003 +vault_env: *id003 diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/tenant.yml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/tenant.yml index c526990a0..d267792c6 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/tenant.yml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/tenant.yml @@ -15,4 +15,16 @@ globalE2EParameters: mergeTenantsAndE2EParameters: false environmentParameters: {} deployParameters: - ESCAPE_SEQUENCE: "true" \ No newline at end of file + ESCAPE_SEQUENCE: "true" + api_config: ${database_config} + database_config: + connection: + host: db.example.com + port: 5432 + yaml_template: | + services: + api: + image: api:latest + ports: + - 8080:8080 + rendered_template: ${yaml_template} From 9cfb5e36ffd02f7704f113fcfb3426ba60c70d03 Mon Sep 17 00:00:00 2001 From: miyamuraga <198181742+miyamuraga@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:43:47 +0500 Subject: [PATCH 019/161] fix: disable setuptools due to breaking changes (#1002) --- .github/actions/run-tests/action.yml | 2 +- build_envgene/build/Dockerfile | 2 +- build_pipegene/build/Dockerfile | 2 +- build_pipegene/build/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index 970c63452..bd90f0003 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -30,7 +30,7 @@ runs: cp dependencies/sources.list /etc/apt/sources.list fi - pip install --upgrade pip setuptools wheel + pip install --upgrade pip "setuptools<82" wheel pip install --no-cache-dir -r dependencies/tests_requirements.txt if [ -f python/build_modules.sh ]; then diff --git a/build_envgene/build/Dockerfile b/build_envgene/build/Dockerfile index 46dd349e6..15dcbbec3 100644 --- a/build_envgene/build/Dockerfile +++ b/build_envgene/build/Dockerfile @@ -44,7 +44,7 @@ COPY scripts/utils /module/scripts/utils # Create virtual environment and install Python packages RUN python -m venv /module/venv -RUN /module/venv/bin/pip install --upgrade pip setuptools wheel +RUN /module/venv/bin/pip install --upgrade pip "setuptools<82" wheel RUN /module/venv/bin/pip install --no-cache-dir --retries 10 --timeout 60 -r /build/requirements.txt RUN /module/venv/bin/pip install /python/jschon-sort diff --git a/build_pipegene/build/Dockerfile b/build_pipegene/build/Dockerfile index cf2c2c19a..78047c63c 100644 --- a/build_pipegene/build/Dockerfile +++ b/build_pipegene/build/Dockerfile @@ -38,7 +38,7 @@ COPY build_pipegene/pipegene_plugins /module/scripts/pipegene_plugins # Create virtual environment and install Python packages RUN python -m venv /module/venv && \ - /module/venv/bin/pip install --upgrade pip setuptools wheel && \ + /module/venv/bin/pip install --upgrade pip "setuptools<82" wheel && \ /module/venv/bin/pip install --no-cache-dir --retries 10 --timeout 60 -r /build/requirements.txt && \ /module/venv/bin/pip install /python/integration /python/jschon-sort /python/envgene && \ /module/venv/bin/pip install PyYAML diff --git a/build_pipegene/build/requirements.txt b/build_pipegene/build/requirements.txt index ea17898f7..7591c8ed9 100644 --- a/build_pipegene/build/requirements.txt +++ b/build_pipegene/build/requirements.txt @@ -4,7 +4,7 @@ gcip==3.0.2 jmespath==1.0.1 packaging==23.2 pip>=23.0 -setuptools>=68.0 +setuptools>=68,<82 python-dateutil==2.8.2 PyYAML==6.0.1 s3transfer==0.7.0 From df827882cd91492a32973ad22858dd36b0eb766d Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Mon, 9 Feb 2026 12:56:31 +0000 Subject: [PATCH 020/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 89d10a2d7..3ab587933 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.23.0" - DOCKER_IMAGE_TAG_ENVGENE: "1.23.0" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.23.0" + DOCKER_IMAGE_TAG_PIPEGENE: "1.23.1" + DOCKER_IMAGE_TAG_ENVGENE: "1.23.1" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.23.1" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 76a6bb856..c39e4f1bc 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.23.0 +version: 1.23.1 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 850b694b6..173c90f46 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.23.0 +version: 1.23.1 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 9dfa3154a..52d7b864b 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.23.0", + "envgene_version": "1.23.1", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 4ef7d36f17af8db6f0e67c23fa3e2f71befff17f Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:49:12 +0300 Subject: [PATCH 021/161] feat: Updated GIT COMMIT job (#1006) --- .../.github/workflows/Envgene.yml | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 3ab587933..c81c62d0c 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -359,20 +359,11 @@ jobs: ${{ env.DOCKER_IMAGE_NAME_ENVGENE }}:${{ env.DOCKER_IMAGE_TAG_ENVGENE }} \ bash -c " source /module/venv/bin/activate - echo 'Prepare git_commit job for \${ENVIRONMENT_NAME}...' - /module/scripts/handle_certs.sh - - git config --global --add safe.directory \"\${CI_PROJECT_DIR}\" - # Execute git commit with proper error handling echo 'Executing git commit operations...' - if ! /module/scripts/prepare.sh \"git_commit.yaml\"; then - echo 'Git commit operations failed with exit code \$?' - echo 'This could be due to authentication issues or other git errors' - exit 1 - fi + /module/scripts/git_commit.sh env_path=\$(find \"\${CI_PROJECT_DIR}/environments\" -type d -name \"\$env_name\") for path in \$env_path; do @@ -389,6 +380,9 @@ jobs: uses: actions/upload-artifact@v4 with: name: git_commit_${{ env.PACKAGE_NAME }} - path: environments/${{ matrix.environment }} + path: | + environments/${{ matrix.environment }} + git_envs + sboms include-hidden-files: true ########################## From e1e56679ce3a430f7886e8599ccba60c62f05dad Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Tue, 10 Feb 2026 07:56:40 +0000 Subject: [PATCH 022/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index c81c62d0c..6d2f049bc 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.23.1" - DOCKER_IMAGE_TAG_ENVGENE: "1.23.1" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.23.1" + DOCKER_IMAGE_TAG_PIPEGENE: "1.23.2" + DOCKER_IMAGE_TAG_ENVGENE: "1.23.2" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.23.2" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index c39e4f1bc..28a8cf23e 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.23.1 +version: 1.23.2 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 173c90f46..daba89f84 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.23.1 +version: 1.23.2 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 52d7b864b..53df461d2 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.23.1", + "envgene_version": "1.23.2", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 4385a4ce082da3e107fb8f98b9d894a1bb5352ca Mon Sep 17 00:00:00 2001 From: Geetha Gadde Date: Tue, 10 Feb 2026 16:16:35 +0530 Subject: [PATCH 023/161] feat: remove the escape sequence false logic (#954) * feat: remove escape sequence false logic (#885) * feat: remove escape sequence false logic (#886) * feat: remove escape sequence false logic * feat: remove escape sequence false logic * feat: remove escape sequence false logic * feat: remove escape sequence false logic * feat: Remove "escape_sequence=false" logic from Effective set calculator --- .../processor/ParametersProcessor.java | 42 ++--- .../processor/expression/PlainLanguage.java | 168 ------------------ .../expression/binding/ApplicationMap.java | 1 - .../processor/expression/binding/Binding.java | 28 +-- .../binding/CloudApplicationMap.java | 4 - .../expression/binding/CloudMap.java | 4 - .../expression/binding/DynamicMap.java | 6 - .../binding/NamespaceApplicationMap.java | 2 - .../expression/binding/NamespaceMap.java | 4 - .../expression/binding/TenantMap.java | 4 - .../processor/parser/OldParametersParser.java | 67 ------- .../ParametersCalculationServiceV1.java | 3 +- .../ParametersCalculationServiceV2.java | 4 +- .../org/qubership/cloud/BindingBaseTest.java | 5 +- .../cloud/expression/BindingTest.java | 10 -- .../expression/ExpressionLanguageTest.java | 4 +- .../cloud/expression/LanguageTest.java | 6 - .../cloud/expression/PlainLanguageTest.java | 138 -------------- 18 files changed, 25 insertions(+), 475 deletions(-) delete mode 100644 build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/PlainLanguage.java delete mode 100644 build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/parser/OldParametersParser.java delete mode 100644 build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/expression/PlainLanguageTest.java diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/ParametersProcessor.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/ParametersProcessor.java index 1777771f2..8df70d716 100644 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/ParametersProcessor.java +++ b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/ParametersProcessor.java @@ -22,7 +22,6 @@ import org.qubership.cloud.parameters.processor.dto.Params; import org.qubership.cloud.parameters.processor.expression.ExpressionLanguage; import org.qubership.cloud.parameters.processor.expression.Language; -import org.qubership.cloud.parameters.processor.expression.PlainLanguage; import org.qubership.cloud.parameters.processor.expression.binding.Binding; import jakarta.inject.Inject; import jakarta.inject.Singleton; @@ -43,16 +42,11 @@ public ParametersProcessor(OpenTelemetryProvider openTelemetryProvider) { this.openTelemetryProvider = openTelemetryProvider; } - public Params processAllParameters(String tenant, String cloud, String namespace, String application, String defaultEscapeSequence, DeployerInputs deployerInputs, String originalNamespace) { + public Params processAllParameters(String tenant, String cloud, String namespace, String application, DeployerInputs deployerInputs, String originalNamespace) { return openTelemetryProvider.withSpan("process", () -> { - Binding binding = new Binding(defaultEscapeSequence, deployerInputs).init(tenant, cloud, namespace, application, originalNamespace); + Binding binding = new Binding(deployerInputs).init(tenant, cloud, namespace, application, originalNamespace); Language lang; - if (binding.getProcessorType().equals("true")) { - lang = new ExpressionLanguage(binding); - } else { - lang = new PlainLanguage(binding); - } - + lang = new ExpressionLanguage(binding); Map deploy = lang.processDeployment(); Map tech = lang.processConfigServerApp(); binding.additionalParameters(deploy); @@ -60,31 +54,21 @@ public Params processAllParameters(String tenant, String cloud, String namespace }); } - public Params processE2EParameters(String tenant, String cloud, String namespace, String application, String defaultEscapeSequence, DeployerInputs deployerInputs, String originalNamespace) { + public Params processE2EParameters(String tenant, String cloud, String namespace, String application, DeployerInputs deployerInputs, String originalNamespace) { return openTelemetryProvider.withSpan("process", () -> { - Binding binding = new Binding(defaultEscapeSequence, deployerInputs).init(tenant, cloud, namespace, application, originalNamespace); + Binding binding = new Binding(deployerInputs).init(tenant, cloud, namespace, application, originalNamespace); Language lang; - if (binding.getProcessorType().equals("true")) { - lang = new ExpressionLanguage(binding); - } else { - lang = new PlainLanguage(binding); - } - + lang = new ExpressionLanguage(binding); Map e2e = lang.processCloudE2E(); return Params.builder().e2eParams(e2e).build(); }); } - public Params processNamespaceParameters(String tenant, String cloud, String namespace, String defaultEscapeSequence, DeployerInputs deployerInputs, String originalNamespace) { + public Params processNamespaceParameters(String tenant, String cloud, String namespace, DeployerInputs deployerInputs, String originalNamespace) { return openTelemetryProvider.withSpan("process", () -> { - Binding binding = new Binding(defaultEscapeSequence, deployerInputs).init(tenant, cloud, namespace, null, originalNamespace); + Binding binding = new Binding(deployerInputs).init(tenant, cloud, namespace, null, originalNamespace); Language lang; - if (binding.getProcessorType().equals("true")) { - lang = new ExpressionLanguage(binding); - } else { - lang = new PlainLanguage(binding); - } - + lang = new ExpressionLanguage(binding); Map namespaceParams = lang.processNamespace(); binding.additionalParameters(namespaceParams); return Params.builder().cleanupParams(namespaceParams).build(); @@ -93,14 +77,10 @@ public Params processNamespaceParameters(String tenant, String cloud, String nam public Map processParameters(Map parameters) { return openTelemetryProvider.withSpan("process", () -> { - Binding binding = new Binding("true"); + Binding binding = new Binding(); binding.put("creds", new Parameter(new CredentialsMap(binding).init())); Language lang; - if (binding.getProcessorType().equals("true")) { - lang = new ExpressionLanguage(binding); - } else { - lang = new PlainLanguage(binding); - } + lang = new ExpressionLanguage(binding); return lang.processParameters(parameters); }); diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/PlainLanguage.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/PlainLanguage.java deleted file mode 100644 index 33067aba8..000000000 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/PlainLanguage.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2024-2025 NetCracker Technology Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.qubership.cloud.parameters.processor.expression; - -import org.qubership.cloud.parameters.processor.MergeMap; -import org.qubership.cloud.parameters.processor.expression.binding.Binding; -import org.qubership.cloud.parameters.processor.expression.binding.DynamicMap; -import org.qubership.cloud.parameters.processor.expression.binding.EscapeMap; -import org.qubership.cloud.devops.commons.utils.Parameter; - -import java.util.AbstractMap; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -public class PlainLanguage extends AbstractLanguage { - - public PlainLanguage(Binding binding) { - super(binding); - } - - @Override - protected Map createMap() { - return new HashMap<>(); - } - - private Object getValue(Object object) { - if (object instanceof Parameter) { - return ((Parameter) object).getValue(); - } else { - return object; - } - } - - private Parameter processValue(Object value) { - Object val = getValue(value); - if (val instanceof List) { - val = processList((List) val); - } else if (val instanceof Map) { - val = processMap((Map) val); - } - Parameter ret = new Parameter(value); - ret.setValue(val); - return ret; - } - - protected Map processMap(Map map) { - if (map == null) { - map = Collections.emptyMap(); - } - return map.entrySet().stream() - .filter(x -> getValue(x.getValue()) instanceof String || !(getValue(x.getValue()) instanceof DynamicMap || getValue(x.getValue()) instanceof EscapeMap)) - .collect(Collectors.toMap(Map.Entry::getKey, e -> processValue(e.getValue()))); - } - - private List processList(List list) { - return list.stream().map(this::processValue).collect(Collectors.toList()); - } - - protected Map processMap(String mapName) { - return processMap(binding.setDefault(mapName)); - } - - private Object convertParameterToObject(Object value) { - if (value instanceof Parameter) { - value = ((Parameter) value).getValue(); - } - if (value instanceof Map) { - value = convertParameterMapToObject((Map) value); - } else if (value instanceof List) { - value = convertParameterListToObject((List) value); - } - return value; - } - - private List convertParameterListToObject(List list) { - return list.stream() - .map(this::convertParameterToObject) - .collect(Collectors.toList()); - } - - private Map convertParameterMapToObject(Map map) { - return map.entrySet().stream() - .map(entry -> new AbstractMap.SimpleEntry<>(entry.getKey(), entry.getValue().getValue())) - .collect(HashMap::new, (m, v) -> m.put(v.getKey(), convertParameterToObject(v.getValue())), HashMap::putAll); - } - - public Map processDeployment() { - Map result = new MergeMap(); - - Map override = new HashMap<>(); - processNamespaceApp(override); - - //merge only overall cloud and custom params - result.putAll(override); - result.putAll(processMap("")); - - return result; - } - - - @Override - public Map processE2E() { - Map result = new HashMap<>(); - processE2E(result); - - return result; - } - - @Override - public Map processCloudE2E() { - Map result = new HashMap<>(); - processCloudE2E(result); - - return result; - } - - @Override - public Map processNamespaceApp() { - Map result = new HashMap<>(); - - processNamespaceApp(result); - return processMap(result); - } - - @Override - public Map processNamespace() { - Map result = new MergeMap(); - - processNamespace(result); - - return processMap(result); - } - - @Override - public Map processConfigServerApp() { - return processNamespaceAppConfigServer(); - } - - @Override - public Map processNamespaceAppConfigServer() { - Map result = new HashMap<>(); - - processNamespaceAppConfigServer(result); - return processMap(result); - } - @Override - public Map processParameters(Map parameters) { - return new HashMap<>(); - } - -} diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/ApplicationMap.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/ApplicationMap.java index b86048bcc..dec252e9d 100644 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/ApplicationMap.java +++ b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/ApplicationMap.java @@ -39,7 +39,6 @@ public Map getMap(String key) { Application config = Injector.getInstance().getDi().get(ApplicationService.class).getByName(key, namespace); if (config != null) { Map map = new EscapeMap(config.getParams(), binding, String.format(ParametersConstants.APP_ORIGIN, key)); - checkEscape(map); maps.put(key, map); return map; } diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/Binding.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/Binding.java index c6c3bec29..4c79d92fb 100644 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/Binding.java +++ b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/Binding.java @@ -26,7 +26,6 @@ import org.qubership.cloud.devops.commons.utils.Parameter; import org.qubership.cloud.parameters.processor.dto.DeployerInputs; import org.qubership.cloud.parameters.processor.parser.EscapeParametersParser; -import org.qubership.cloud.parameters.processor.parser.OldParametersParser; import org.qubership.cloud.parameters.processor.parser.ParametersParser; import lombok.Getter; @@ -38,7 +37,6 @@ @Slf4j public class Binding extends HashMap implements Cloneable { - String escapeSequence; @Getter private DeployerInputs deployerInputs; private Map defaultMap; @@ -46,32 +44,22 @@ public class Binding extends HashMap implements Cloneable { @Getter private String tenant; private ParametersParser escapeParser; - private ParametersParser oldParser; @Getter private Map> typeCollector = new HashMap<>(); - public Binding(String defaultEscapeSequence) { - this.escapeSequence = defaultEscapeSequence; + public Binding() { this.tenant = ""; } - public Binding(String defaultEscapeSequence, DeployerInputs deployerInputs) { - this.escapeSequence = defaultEscapeSequence; + public Binding(DeployerInputs deployerInputs) { this.tenant = ""; this.deployerInputs = deployerInputs; } - public String getProcessorType() { - return escapeSequence; - } public ParametersParser getParser() { ParametersParser parser; - if (escapeSequence.equals("true")) { - return (parser = escapeParser) == null ? (escapeParser = new EscapeParametersParser()) : parser; - } else { - return (parser = oldParser) == null ? (oldParser = new OldParametersParser()) : parser; - } + return (parser = escapeParser) == null ? (escapeParser = new EscapeParametersParser()) : parser; } private void processSet(String tenant, String setName, String application, EscapeMap parameterSet, EscapeMap applicationMap) { @@ -94,7 +82,7 @@ public Binding init(String tenant, String cloud, String namespace, String applic super.put("application", new Parameter(new ApplicationMap(application, this, namespace).init())); super.put("creds", new Parameter(new CredentialsMap(this).init())); - Map processed = calculateCredentialsAndPrepareStructuredParams(this, Boolean.parseBoolean(getProcessorType())); + Map processed = calculateCredentialsAndPrepareStructuredParams(this); this.putAll(processed); return this; @@ -105,7 +93,7 @@ public Binding additionalParameters(Map parameters) { return this; } - private Map calculateCredentialsAndPrepareParams(String keyCloudParameter, Parameter valueCloudParameter, boolean escapeSequence) { + private Map calculateCredentialsAndPrepareParams(String keyCloudParameter, Parameter valueCloudParameter) { Object value = valueCloudParameter.getValue(); if (value instanceof String) { value = ((String) value).trim(); @@ -148,17 +136,17 @@ private Map calculateCredentialsAndPrepareParams(String keyCl } } - private Map calculateCredentialsAndPrepareStructuredParams(Map params, Boolean escape) { + private Map calculateCredentialsAndPrepareStructuredParams(Map params) { Map result = params.entrySet().stream().map(entry -> { Parameter param = new Parameter(entry.getValue()); if (param.getValue() instanceof Map) { return new HashMap() { { - put(entry.getKey(), new Parameter(calculateCredentialsAndPrepareStructuredParams((Map) param.getValue(), escape))); + put(entry.getKey(), new Parameter(calculateCredentialsAndPrepareStructuredParams((Map) param.getValue()))); } }; } - return calculateCredentialsAndPrepareParams(entry.getKey(), param, escape); + return calculateCredentialsAndPrepareParams(entry.getKey(), param); }).flatMap(c -> c.entrySet().stream()) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e2)); params.clear(); diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/CloudApplicationMap.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/CloudApplicationMap.java index 1d526b3c9..c870e8925 100644 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/CloudApplicationMap.java +++ b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/CloudApplicationMap.java @@ -67,10 +67,6 @@ public Map getMap(String appName) { }); } - checkEscape(map); - checkEscape(parameterSetMap); - checkEscape(configServerMap); - checkEscape(parameterSetConfigServerMap); map.put("config-server", configServerMap); maps.put(appName, map); return map; diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/CloudMap.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/CloudMap.java index 0fb3c2485..4334a6e1c 100644 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/CloudMap.java +++ b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/CloudMap.java @@ -80,10 +80,6 @@ public Map getMap(String cloudName) { map.put("app", new Parameter(new CloudApplicationMap(config, defaultApp, binding).init())); - - checkEscape(map); - checkEscape(e2e); - checkEscape(configServer); CredentialUtils credentialUtils = Injector.getInstance().getCredentialUtils(); for (DBaaS dbaas : config.getDbaasCfg()) { if (dbaas.getApiUrl() != null) { diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/DynamicMap.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/DynamicMap.java index 82f7342e5..72785a877 100644 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/DynamicMap.java +++ b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/DynamicMap.java @@ -40,12 +40,6 @@ public abstract class DynamicMap implements Map, Serializable public abstract Map getMap(String key); - protected void checkEscape(Map map) { - Object processor = map.get("ESCAPE_SEQUENCE").getValue(); - if (processor != null) { - binding.escapeSequence = processor.toString(); - } - } public DynamicMap init() { if (defaultMap != null) { diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/NamespaceApplicationMap.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/NamespaceApplicationMap.java index 8c164cb23..1d3683fff 100644 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/NamespaceApplicationMap.java +++ b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/NamespaceApplicationMap.java @@ -65,8 +65,6 @@ public Map getMap(String appName) { map.put("APPLICATION_NAME", appName); - checkEscape(map); - checkEscape(configServerMap); map.put("config-server", configServerMap); try { if (binding.getDeployerInputs() != null && binding.getDeployerInputs().getAppVersion() != null) { diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/NamespaceMap.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/NamespaceMap.java index 9d2ce0dca..74d9f1fe6 100644 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/NamespaceMap.java +++ b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/NamespaceMap.java @@ -204,10 +204,6 @@ public Map getMap(String namespaceName) { EscapeMap e2e = new EscapeMap(config.getE2eParameters(), binding, String.format(ParametersConstants.NS_E2E_ORIGIN, tenant, this.cloud, namespaceName)); EscapeMap configServer = new EscapeMap(config.getConfigServerParameters(), binding, String.format(ParametersConstants.NS_CONFIG_SERVER_ORIGIN, tenant, this.cloud, namespaceName)); - checkEscape(map); - checkEscape(e2e); - checkEscape(configServer); - map.put(E2E, new Parameter(e2e)); map.put(CONFIG_SERVER, new Parameter(configServer)); maps.put(namespaceName, map); diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/TenantMap.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/TenantMap.java index 3ac5dcda5..acde3c677 100644 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/TenantMap.java +++ b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/TenantMap.java @@ -64,10 +64,6 @@ public Map getMap(String tenantName) { EscapeMap e2e = new EscapeMap(config.getGlobalParameters().getE2eParameters().getEnvParameters(), binding, String.format(ParametersConstants.TENANT_E2E_ORIGIN, tenantName)); EscapeMap configServer = new EscapeMap(config.getGlobalParameters().getTechnicalConfiguration(), binding, String.format(ParametersConstants.TENANT_CONFIG_SERVER_ORIGIN, tenantName)); - checkEscape(map); - checkEscape(e2e); - checkEscape(configServer); - map.put("cloud", new Parameter(new CloudMap(tenantName, defaultCloud, defaultNamespace, defaultApp, binding, originalNamespace).init())); map.put("e2e", new Parameter(e2e)); map.put("TENANTNAME", tenantName); diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/parser/OldParametersParser.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/parser/OldParametersParser.java deleted file mode 100644 index 1bbe8b1e6..000000000 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/parser/OldParametersParser.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2024-2025 NetCracker Technology Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.qubership.cloud.parameters.processor.parser; - -import org.qubership.cloud.devops.commons.utils.constant.ParametersConstants; -import org.qubership.cloud.devops.commons.utils.Parameter; - -import groovy.json.JsonSlurperClassic; - -import java.io.Serializable; -import java.util.HashMap; -import java.util.Map; - -public class OldParametersParser implements ParametersParser, Serializable { - - @Override - public Object processParam(String param) { - // auto escape symbols \ and " for CRM guys - String valueOfParam = param.replaceAll("(\"|\\\\)", "\\\\$0").trim(); - if (valueOfParam.startsWith("\'{")) { - String jsString = valueOfParam.substring(1, valueOfParam.length() - 1); - return new JsonSlurperClassic().parseText(jsString.replaceAll("\\\\", "")); - } else { - return valueOfParam; - } - } - - @Override - public Map parse(String customParams) { - Map k = new HashMap<>(); - - for (String paramLine : customParams.split(";")) { - if (!paramLine.trim().isEmpty()) { - String[] pairs = paramLine.split("=", 2); - - if (!paramLine.contains("=") || pairs[0].isEmpty()) - throw new IllegalArgumentException( - "For CUSTOM_PARAMS line " + paramLine + " can not be parsed. This field should contain only lines like PARAM1=VALUE1"); - - - String valueOfParam = pairs[1].replaceAll("(\"|\\\\)", "\\\\$0"); - - if (valueOfParam.trim().startsWith("\'") && !valueOfParam.trim().startsWith("\'{") && !valueOfParam.trim().startsWith("\'[")) - throw new IllegalArgumentException( - "Key " + pairs[0] + " has incorrect value " + valueOfParam + " ' symbol is invalid for non-structured variable format"); - - k.put(pairs[0].trim(), new Parameter(processParam(pairs[1]), ParametersConstants.CUSTOM_PARAMS_ORIGIN, true)); - } - } - return k; - } -} - diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV1.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV1.java index f8b5bae94..e7aa6e4a1 100644 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV1.java +++ b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV1.java @@ -57,7 +57,6 @@ private ParameterBundle getParameterBundle(String tenantName, String cloudName, cloudName, namespaceName, applicationName, - "false", deployerInputs, originalNamespace); @@ -69,7 +68,7 @@ private ParameterBundle getParameterBundle(String tenantName, String cloudName, } private ParameterBundle getE2EParameterBundle(String tenantName, String cloudName) { - Params parameters = parametersProcessor.processE2EParameters(tenantName, cloudName, null, null, "false", null, null); + Params parameters = parametersProcessor.processE2EParameters(tenantName, cloudName, null, null, null, null); ParameterBundle parameterBundle = ParameterBundle.builder().build(); prepareSecureInsecureParams(parameters.getE2eParams(), parameterBundle, ParameterType.E2E); return parameterBundle; diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV2.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV2.java index 8992ee62d..cc33ad696 100644 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV2.java +++ b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV2.java @@ -63,7 +63,6 @@ public ParameterBundle getCleanupParameterBundle(String tenantName, String cloud Params parameters = parametersProcessor.processNamespaceParameters(tenantName, cloudName, namespaceName, - "false", deployerInputs, originalNamespace); @@ -79,7 +78,6 @@ private ParameterBundle getParameterBundle(String tenantName, String cloudName, cloudName, namespaceName, applicationName, - "false", deployerInputs, originalNamespace); @@ -158,7 +156,7 @@ private static void processDeploymentDescriptorParams(Params parameters, Paramet } private ParameterBundle getE2EParameterBundle(String tenantName, String cloudName) { - Params parameters = parametersProcessor.processE2EParameters(tenantName, cloudName, null, null, "false", null, null); + Params parameters = parametersProcessor.processE2EParameters(tenantName, cloudName, null, null, null, null); ParameterBundle parameterBundle = ParameterBundle.builder().build(); prepareSecureInsecureParams(parameters.getE2eParams(), parameterBundle, ParameterType.E2E, null, null); return parameterBundle; diff --git a/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/BindingBaseTest.java b/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/BindingBaseTest.java index fa54812ab..422a47a3e 100644 --- a/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/BindingBaseTest.java +++ b/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/BindingBaseTest.java @@ -124,12 +124,11 @@ public T withSpan(String spanName, ThrowingSupplier constructor = Binding.class.getDeclaredConstructor(String.class); + Constructor constructor = Binding.class.getDeclaredConstructor(); constructor.setAccessible(true); - Binding binding = constructor.newInstance(params.get("defaultEscapeSequence") != null ? params.get("defaultEscapeSequence") : "false") + Binding binding = constructor.newInstance() .init("tenant", "cloud", "namespace", "application", "namespace"); return binding; - } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | SecurityException e) { throw new RuntimeException(e); diff --git a/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/expression/BindingTest.java b/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/expression/BindingTest.java index b3ae9a1d4..2cc347e94 100644 --- a/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/expression/BindingTest.java +++ b/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/expression/BindingTest.java @@ -56,19 +56,9 @@ public void init_ESCAPE_SEQUENCE_true() throws NoSuchMethodException, Instantiat }; Binding binding = setupBinding(params); - assertEquals("true", binding.getProcessorType()); assertTrue(binding.get("tenant").get("cloud").get("yaml").getValue() instanceof Map); } - @Test - public void init_ESCAPE_SEQUENCE_default() throws NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, SecurityException { - Map params = new HashMap(); - - Binding binding = setupBinding(params); - - assertEquals("false", binding.getProcessorType()); - } - @Test public void init_UsernamePassword() throws NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, SecurityException { diff --git a/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/expression/ExpressionLanguageTest.java b/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/expression/ExpressionLanguageTest.java index 37ed10c9e..8374027e1 100644 --- a/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/expression/ExpressionLanguageTest.java +++ b/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/expression/ExpressionLanguageTest.java @@ -448,7 +448,7 @@ public void processBackslashes(String string, String result) throws IllegalAcces @ParameterizedTest @MethodSource public void processValue(Map params, Map result) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { - Binding binding = new Binding("true"); + Binding binding = new Binding(); binding.putAll(params); ExpressionLanguage el = new ExpressionLanguage(binding); @@ -548,7 +548,7 @@ public void processDeployment_check_return_value_is_not_Parameter() { @Test void processedGlobalResourceProfileMustBeSuccessfullyProcessedAgain() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { - Binding binding = new Binding("true"); + Binding binding = new Binding(); binding.setDefault(""); HashMap map = new HashMap<>(){{ put("key1", "value1"); diff --git a/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/expression/LanguageTest.java b/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/expression/LanguageTest.java index bf48f9417..76c33429c 100644 --- a/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/expression/LanguageTest.java +++ b/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/expression/LanguageTest.java @@ -25,7 +25,6 @@ import org.qubership.cloud.devops.commons.pojo.parameterset.ParameterSetApplication; import org.qubership.cloud.devops.commons.utils.Parameter; import org.qubership.cloud.parameters.processor.expression.ExpressionLanguage; -import org.qubership.cloud.parameters.processor.expression.PlainLanguage; import org.qubership.cloud.parameters.processor.expression.binding.Binding; import org.junit.jupiter.api.Test; @@ -97,11 +96,6 @@ private Binding prepareBinding() { }); } - @Test - public void processDeployment_Plain_validate_order() { - Binding binding = prepareBinding(); - assertMap(new PlainLanguage(binding).processDeployment()); - } @Test public void processDeployment_Expression_validate_order() { diff --git a/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/expression/PlainLanguageTest.java b/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/expression/PlainLanguageTest.java deleted file mode 100644 index b8528fb0d..000000000 --- a/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/expression/PlainLanguageTest.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2024-2025 NetCracker Technology Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.qubership.cloud.expression; - -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.qubership.cloud.BindingBaseTest; -import org.qubership.cloud.devops.commons.utils.Parameter; -import org.qubership.cloud.parameters.processor.ParametersProcessor; -import org.qubership.cloud.parameters.processor.expression.PlainLanguage; -import org.qubership.cloud.parameters.processor.expression.binding.Binding; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.Map; -import java.util.stream.Stream; - -class PlainLanguageTest extends BindingBaseTest { - private static Stream processDeployment() { - return Stream.of( - Arguments.of(new HashMap(), - new HashMap(), - new HashMap()), - Arguments.of(new HashMap() {{ - put("key1", "value1"); - put("key2", "$key1"); - }}, - new HashMap(), - new HashMap() {{ - put("key1", "value1"); - put("key2", "$key1"); - }}), - Arguments.of(new HashMap() {{ - put("key1", "value1"); - put("key2", "${cloud.key1}"); - }}, - new HashMap() {{ - put("key1", "value2"); - }}, - new HashMap() {{ - put("key1", "value2"); - put("key2", "${cloud.key1}"); - }}), - Arguments.of(new HashMap() {{ - put("key1", "'{\"key\": \"value\"}'"); - put("key2", "'{\"key\": [\"value1\", \"value2\"]}'"); - }}, - new HashMap() {{ - put("key2", "'{\"key\": [\"value3\"]}'"); - }}, - new HashMap() {{ - put("key1", new HashMap() {{ - put("key", "value"); - }}); - put("key2", new HashMap() {{ - put("key", new LinkedList() {{ - add("value3"); - }}); - }}); - }}), - Arguments.of(new HashMap() {{ - put("key1", "\"value1\""); - }}, - new HashMap() {{ - put("key2", "\"value2\""); - }}, - new HashMap() {{ - put("key1", "\\\"value1\\\""); - put("key2", "\\\"value2\\\""); - }}) - ); - } - - - @ParameterizedTest - @MethodSource - void processDeployment(Map tenant, Map cloud, Map result) { - Binding binding = setupBinding(new HashMap() {{ - put("tenantParams", tenant); - put("cloudParams", cloud); - put("defaultEscapeSequence", "false"); - }}); - Map map = ParametersProcessor.convertParameterMapToObject(new PlainLanguage(binding).processDeployment()); - map.remove("MAAS_ENABLED"); - map.remove("VAULT_INTEGRATION"); - map.remove("NAMESPACE"); - map.remove("CLOUDNAME"); - map.remove("TENANTNAME"); - map.remove("APPLICATION_NAME"); - map.remove("PRODUCTION_MODE"); - map.remove("CLOUD_API_HOST"); - map.remove("CLOUD_PUBLIC_HOST"); - map.remove("CLOUD_PRIVATE_HOST"); - map.remove("CLOUD_PROTOCOL"); - map.remove("SERVER_HOSTNAME"); - map.remove("CUSTOM_HOST"); - map.remove("OPENSHIFT_SERVER"); - map.remove("CLOUD_API_PORT"); - assertEquals(result, map); - } - - @Test - void processE2E_check_return_value_is_not_Parameter() { - Binding binding = setupBinding(new HashMap() { - { - put("tenantParamsE2E", Collections.singletonMap("struct", "str")); - put("defaultEscapeSequence", "false"); - } - }); - Map map = new PlainLanguage(binding).processE2E(); - map.remove("MAAS_ENABLED"); - map.remove("VAULT_INTEGRATION"); - - assertThat(map.get("struct"), instanceOf(Parameter.class)); - } -} From fc863719b34027efa925858c0c2d366df0b255a3 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Tue, 10 Feb 2026 10:48:52 +0000 Subject: [PATCH 024/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 6d2f049bc..f56d5597b 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.23.2" - DOCKER_IMAGE_TAG_ENVGENE: "1.23.2" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.23.2" + DOCKER_IMAGE_TAG_PIPEGENE: "1.24.0" + DOCKER_IMAGE_TAG_ENVGENE: "1.24.0" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.24.0" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 28a8cf23e..726b48703 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.23.2 +version: 1.24.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index daba89f84..e0aea553e 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.23.2 +version: 1.24.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 53df461d2..52d4c99a5 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.23.2", + "envgene_version": "1.24.0", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From f2d618667c7cd1551ebc5fd6295ba00bf652d27f Mon Sep 17 00:00:00 2001 From: miyamuraga <198181742+miyamuraga@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:28:39 +0500 Subject: [PATCH 025/161] feat: Implement new pipeline parameter "ENV_TEMPLATE_VERSION_UPDATE_MODE" (#1004) --- .../scripts/appregdef_render_job.py | 22 ++++++++++-------- build_pipegene/scripts/gitlab_ci.py | 4 +--- docs/instance-pipeline-parameters.md | 2 +- python/envgene/envgenehelper/models.py | 15 ++++++++++++ scripts/build_env/appregdef_render.py | 23 +++++++++---------- .../env_template/set_template_version.py | 10 ++++++-- scripts/utils/pipeline_parameters.py | 8 +++++-- 7 files changed, 55 insertions(+), 29 deletions(-) create mode 100644 python/envgene/envgenehelper/models.py diff --git a/build_pipegene/scripts/appregdef_render_job.py b/build_pipegene/scripts/appregdef_render_job.py index 1c97a7068..5a3aa9a08 100644 --- a/build_pipegene/scripts/appregdef_render_job.py +++ b/build_pipegene/scripts/appregdef_render_job.py @@ -4,16 +4,19 @@ from pipeline_helper import job_instance -def prepare_appregdef_render_job(pipeline, is_template_test, env_template_version, full_env, environment_name, - cluster_name, group_id, artifact_id, artifact_url, tags): +def prepare_appregdef_render_job(pipeline, params, full_env, environment_name, cluster_name, group_id, artifact_id, + artifact_url, tags): logger.info(f'Prepare appregdef render job for {full_env}') + env_template_version = params.get('ENV_TEMPLATE_VERSION') + is_template_test = params.get('IS_TEMPLATE_TEST') + env_tmp_ver_update_mode = params.get('ENV_TEMPLATE_VERSION_UPDATE_MODE') script = [ - '/module/scripts/handle_certs.sh', + '/module/scripts/handle_certs.sh', ] - + if env_template_version and not is_template_test: script.append('python3 /build_env/scripts/build_env/env_template/set_template_version.py') - + script.append('cd /build_env; python3 /build_env/scripts/build_env/appregdef_render.py') appregdef_render_params = { @@ -35,15 +38,16 @@ def prepare_appregdef_render_job(pipeline, is_template_test, env_template_versio "ARTIFACT_ID": artifact_id, "ARTIFACT_URL": artifact_url, "GITLAB_RUNNER_TAG_NAME": tags, + "ENV_TEMPLATE_VERSION_UPDATE_MODE": env_tmp_ver_update_mode } appregdef_render_job = job_instance(params=appregdef_render_params, vars=appregdef_render_vars) - + appregdef_render_job.artifacts.add_paths("${CI_PROJECT_DIR}/environments/" + full_env) appregdef_render_job.artifacts.add_paths("${CI_PROJECT_DIR}/configuration") appregdef_render_job.artifacts.add_paths("${CI_PROJECT_DIR}/tmp") appregdef_render_job.artifacts.when = WhenStatement.ALWAYS - + pipeline.add_children(appregdef_render_job) - - return appregdef_render_job \ No newline at end of file + + return appregdef_render_job diff --git a/build_pipegene/scripts/gitlab_ci.py b/build_pipegene/scripts/gitlab_ci.py index 0f5019dd0..7ca267798 100644 --- a/build_pipegene/scripts/gitlab_ci.py +++ b/build_pipegene/scripts/gitlab_ci.py @@ -125,9 +125,7 @@ def build_pipeline(params: dict) -> None: f'Credential rotation job for {full_env_name} is skipped because CRED_ROTATION_PAYLOAD is empty.') if params['ENV_BUILD']: - jobs_map["appregdef_render_job"] = prepare_appregdef_render_job(pipeline, params['IS_TEMPLATE_TEST'], - params['ENV_TEMPLATE_VERSION'], - full_env_name, + jobs_map["appregdef_render_job"] = prepare_appregdef_render_job(pipeline, params, full_env_name, environment_name, cluster_name, group_id, artifact_id, artifact_url, tags) else: diff --git a/docs/instance-pipeline-parameters.md b/docs/instance-pipeline-parameters.md index fe982fe0e..171ea4fb7 100644 --- a/docs/instance-pipeline-parameters.md +++ b/docs/instance-pipeline-parameters.md @@ -129,7 +129,7 @@ This parameter serves as a configuration for an extension point. Integration wit **Allowed values**: - `PERSISTENT` (default) - Applies the standard behavior: the pipeline updates the template version in Environment Inventory by updating `envTemplate.artifact` (or `envTemplate.templateArtifact.artifact.version`) in `env_definition.yml`. + Applies the standard behavior: the pipeline updates the template version in Environment Inventory by modifying `envTemplate.artifact` (or `envTemplate.templateArtifact.artifact.version`) in `env_definition.yml`, and records the template artifact version actually applied during the run in `generatedVersions.generateEnvironmentLatestVersion` in the same file. - `TEMPORARY` Applies `ENV_TEMPLATE_VERSION` **only for the current pipeline execution** and **does not** update `envTemplate.artifact` (or `envTemplate.templateArtifact.artifact.version`) in `env_definition.yml`. diff --git a/python/envgene/envgenehelper/models.py b/python/envgene/envgenehelper/models.py new file mode 100644 index 000000000..49f60dc95 --- /dev/null +++ b/python/envgene/envgenehelper/models.py @@ -0,0 +1,15 @@ +from enum import Enum + + +class TemplateVersionUpdateMode(str, Enum): + PERSISTENT = "PERSISTENT" + TEMPORARY = "TEMPORARY" + + @classmethod + def _missing_(cls, value): + if isinstance(value, str): + value = value.upper() + for m in cls: + if m.value == value: + return m + return None diff --git a/scripts/build_env/appregdef_render.py b/scripts/build_env/appregdef_render.py index 9cded545f..f9690aa12 100644 --- a/scripts/build_env/appregdef_render.py +++ b/scripts/build_env/appregdef_render.py @@ -1,25 +1,24 @@ -import shutil +from envgenehelper import * +from envgenehelper.models import TemplateVersionUpdateMode from env_template.process_env_template import process_env_template -from envgenehelper import * from render_config_env import EnvGenerator - if __name__ == '__main__': template_version = process_env_template() - + cluster_name = getenv_with_error("CLUSTER_NAME") env_name = getenv_with_error("ENVIRONMENT_NAME") base_dir = getenv_with_error('CI_PROJECT_DIR') instances_dir = getenv_with_error("INSTANCES_DIR") - + output_dir = f"{base_dir}/environments" render_dir = f"/tmp/render/{env_name}" templates_dir = f"{base_dir}/tmp/templates" - + env_dir = get_env_instances_dir(env_name, cluster_name, instances_dir) cloud_passport_file_path = find_cloud_passport_definition(env_dir, instances_dir) - + render_context_vars = { "cluster_name": cluster_name, "output_dir": output_dir, @@ -28,17 +27,17 @@ "cloud_passport_file_path": cloud_passport_file_path, "env_instances_dir": env_dir } - + render_context = EnvGenerator() render_context.process_app_reg_defs(env_name, render_context_vars) - + for dir_name in ["AppDefs", "RegDefs"]: src = Path(render_dir) / dir_name dst = Path(env_dir) / dir_name - + if dst.exists(): shutil.rmtree(dst) if src.exists(): shutil.move(src, dst) - - update_generated_versions(env_dir, BUILD_ENV_TAG, template_version) \ No newline at end of file + + update_generated_versions(env_dir, BUILD_ENV_TAG, template_version) diff --git a/scripts/build_env/env_template/set_template_version.py b/scripts/build_env/env_template/set_template_version.py index f5c3f37c0..c140954a1 100644 --- a/scripts/build_env/env_template/set_template_version.py +++ b/scripts/build_env/env_template/set_template_version.py @@ -4,12 +4,17 @@ from envgenehelper import beautifyYaml, writeYamlToFile, logger, getenv_with_error, getEnvDefinitionPath from envgenehelper import getEnvDefinition +from envgenehelper.models import TemplateVersionUpdateMode -def update_version(env_definition_dir, version_to_add): + +def update_version(env_definition_dir, version_to_add, update_mode: TemplateVersionUpdateMode): env_definition_path = getEnvDefinitionPath(env_definition_dir) logger.info(f"Started version update to {version_to_add} in {env_definition_path}.") data = getEnvDefinition(env_instances_dir) + if update_mode == TemplateVersionUpdateMode.TEMPORARY: + logger.info("Template update mode: TEMPORARY, Skip updating template artifact version in env_definition.yml") + return if ":" in version_to_add: if 'envTemplate' in data: if 'templateArtifact' in data['envTemplate']: @@ -39,4 +44,5 @@ def update_version(env_definition_dir, version_to_add): environment_name = getenv_with_error("ENVIRONMENT_NAME") env_instances_dir = Path(f"{base_dir}/environments/{cluster_name}/{environment_name}") version_to_add = getenv("ENV_TEMPLATE_VERSION") - update_version(env_instances_dir, version_to_add) + env_tmp_ver_update_mode = TemplateVersionUpdateMode(getenv("ENV_TEMPLATE_VERSION_UPDATE_MODE")) + update_version(env_instances_dir, version_to_add, env_tmp_ver_update_mode) diff --git a/scripts/utils/pipeline_parameters.py b/scripts/utils/pipeline_parameters.py index 6aa9d9ce0..563b70192 100644 --- a/scripts/utils/pipeline_parameters.py +++ b/scripts/utils/pipeline_parameters.py @@ -2,6 +2,7 @@ from os import getenv from envgenehelper import logger from envgenehelper.plugin_engine import PluginEngine +from envgenehelper.models import TemplateVersionUpdateMode def get_pipeline_parameters() -> dict: @@ -26,8 +27,8 @@ def get_pipeline_parameters() -> dict: 'CRED_ROTATION_PAYLOAD': getenv("CRED_ROTATION_PAYLOAD", ""), 'CRED_ROTATION_FORCE': getenv("CRED_ROTATION_FORCE", ""), 'NS_BUILD_FILTER': getenv("NS_BUILD_FILTER", ""), - 'GITLAB_RUNNER_TAG_NAME' : getenv("GITLAB_RUNNER_TAG_NAME", ""), - 'RUNNER_SCRIPT_TIMEOUT' : getenv("RUNNER_SCRIPT_TIMEOUT") or "10m", + 'GITLAB_RUNNER_TAG_NAME': getenv("GITLAB_RUNNER_TAG_NAME", ""), + 'RUNNER_SCRIPT_TIMEOUT': getenv("RUNNER_SCRIPT_TIMEOUT") or "10m", 'DEPLOYMENT_SESSION_ID': getenv("DEPLOYMENT_SESSION_ID", ""), 'ENVGENE_LOG_LEVEL': getenv("ENVGENE_LOG_LEVEL"), "BG_STATE": getenv("BG_STATE"), @@ -35,8 +36,11 @@ def get_pipeline_parameters() -> dict: "APP_DEFS_PATH": getenv("APP_DEFS_PATH"), "REG_DEFS_PATH": getenv("REG_DEFS_PATH"), "ENV_INVENTORY_CONTENT": getenv("ENV_INVENTORY_CONTENT"), + "ENV_TEMPLATE_VERSION_UPDATE_MODE": getenv( + "ENV_TEMPLATE_VERSION_UPDATE_MODE") or TemplateVersionUpdateMode.PERSISTENT.value, } + class PipelineParametersHandler: def __init__(self, **kwargs): plugins_dir='/module/scripts/pipegene_plugins/pipe_parameters' From aa55ee729d5e670a2467ff8296bbb2bcde3adc4b Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Wed, 11 Feb 2026 06:36:40 +0000 Subject: [PATCH 026/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index f56d5597b..6490556c7 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.24.0" - DOCKER_IMAGE_TAG_ENVGENE: "1.24.0" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.24.0" + DOCKER_IMAGE_TAG_PIPEGENE: "1.25.0" + DOCKER_IMAGE_TAG_ENVGENE: "1.25.0" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.25.0" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 726b48703..e4257117a 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.24.0 +version: 1.25.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index e0aea553e..86c9a1e16 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.24.0 +version: 1.25.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 52d4c99a5..fb367c831 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.24.0", + "envgene_version": "1.25.0", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 46e5204cba68a7dea6543d827233465d3107910a Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Wed, 11 Feb 2026 09:38:02 +0300 Subject: [PATCH 027/161] feat: Add Python to Effective set image (#943) --- build_effective_set_generator/.gitignore | 3 + build_effective_set_generator/Dockerfile | 101 +++++++++++++-- .../build/configuration/constraint.txt | 1 + .../build/configuration/pip.conf | 5 + .../build/configuration/requirements.txt | 9 ++ .../build/configuration/sources.list | 2 + .../build/scripts/decrypt.sh | 34 +++++ .../build/scripts/decrypt_fernet.py | 81 ++++++++++++ .../build/scripts/get_include_list.sh | 49 +++++++ .../build/scripts/show_validate.py | 41 ++++++ .../build/scripts/update_ca_certs.sh | 50 +++++++ build_envgene/build/Dockerfile | 20 +-- pom.xml | 122 ------------------ src/assembly/assembly.xml | 18 --- 14 files changed, 374 insertions(+), 162 deletions(-) create mode 100644 build_effective_set_generator/build/configuration/constraint.txt create mode 100644 build_effective_set_generator/build/configuration/pip.conf create mode 100644 build_effective_set_generator/build/configuration/requirements.txt create mode 100644 build_effective_set_generator/build/configuration/sources.list create mode 100755 build_effective_set_generator/build/scripts/decrypt.sh create mode 100644 build_effective_set_generator/build/scripts/decrypt_fernet.py create mode 100755 build_effective_set_generator/build/scripts/get_include_list.sh create mode 100644 build_effective_set_generator/build/scripts/show_validate.py create mode 100755 build_effective_set_generator/build/scripts/update_ca_certs.sh delete mode 100644 pom.xml delete mode 100644 src/assembly/assembly.xml diff --git a/build_effective_set_generator/.gitignore b/build_effective_set_generator/.gitignore index a572cf5b7..25ff83d14 100644 --- a/build_effective_set_generator/.gitignore +++ b/build_effective_set_generator/.gitignore @@ -36,6 +36,9 @@ **/build.gradle **/settings.gradle **/build +# Exception: build directory for Docker build files (must be after **/build rule) +!build/ +!build/** # CMake cmake-build-debug/ diff --git a/build_effective_set_generator/Dockerfile b/build_effective_set_generator/Dockerfile index e9cd485cd..ca57f17b4 100644 --- a/build_effective_set_generator/Dockerfile +++ b/build_effective_set_generator/Dockerfile @@ -1,4 +1,7 @@ -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.1 AS builder +############################################# +# STAGE 1: CLI (UBI minimal runtime with Java) +############################################# +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.1 AS cli ARG JAVA_PACKAGE=java-17-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 @@ -7,28 +10,102 @@ ARG RUN_JAVA_VERSION=1.3.8 RUN microdnf install -y curl ca-certificates ${JAVA_PACKAGE} && \ microdnf clean all && \ mkdir -p /deployments && \ - curl -s https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh && \ + curl -sSf https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh \ + -o /deployments/run-java.sh && \ chmod 540 /deployments/run-java.sh && \ echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security COPY build_effective_set_generator/effective-set-generator/target/*.jar /deployments/app.jar -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.1 + + +############################################# +# STAGE 2: Build Python Environment (Alpine) +############################################# +FROM python:3.12-alpine3.19 AS build + +# Install ALL system-level *build dependencies* +# hadolint ignore=DL3018 +RUN apk add --no-cache \ + gcc \ + musl-dev \ + libffi-dev \ + openssl-dev \ + python3-dev \ + cargo \ + rust \ + build-base \ + curl \ + wget + +COPY build_effective_set_generator/build/configuration/pip.conf /etc/pip.conf +COPY build_effective_set_generator/build/configuration/requirements.txt /build/requirements.txt +COPY build_effective_set_generator/build/configuration/constraint.txt /build/constraint.txt + +# Build Python virtual environment +RUN python3 -m venv /module/venv && \ + /module/venv/bin/pip install --upgrade pip setuptools wheel && \ + /module/venv/bin/pip install --no-cache-dir --no-binary cffi --no-binary cryptography \ + -r /build/requirements.txt + +# Install SOPS +RUN wget --quiet --tries=3 \ + https://github.com/mozilla/sops/releases/download/v3.9.0/sops-v3.9.0.linux.amd64 \ + -O /usr/local/bin/sops && \ + chmod +x /usr/local/bin/sops + +# DO NOT delete build dependencies here — runtime needs them indirectly + + + + +############################################# +# STAGE 3: Final Runtime (Alpine) +############################################# +FROM python:3.12-alpine3.19 AS runtime ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' -COPY --from=builder --chown=1001:root /deployments /deployments -COPY --from=builder /etc/alternatives/jre/lib/security/java.security /etc/alternatives/jre/lib/security/java.security +COPY --from=cli --chown=1001:root /deployments /deployments +COPY --from=cli /etc/alternatives/jre/lib/security/java.security \ + /etc/alternatives/jre/lib/security/java.security + +RUN chmod g+rwX /deployments + +COPY build_effective_set_generator/build/configuration/pip.conf /etc/pip.conf +COPY build_effective_set_generator/build/configuration/constraint.txt /build/constraint.txt +COPY build_effective_set_generator/build/configuration/sources.list /etc/apk/repositories -## NOTE: This script requires Python and will fail unless Python is added in the future. +# Install ONLY runtime dependencies (NO dev packages) +# hadolint ignore=DL3018 +RUN apk add --no-cache \ + libffi \ + openssl \ + bash \ + ca-certificates \ + tar \ + curl \ + jq \ + yq \ + gettext \ + sed \ + age + +# Bring the virtual environment compiled in build stage +COPY --from=build /module/venv /module/venv +COPY --from=build /usr/local/bin/sops /usr/local/bin/sops +COPY build_effective_set_generator/build/scripts /module/scripts COPY scripts/utils /module/scripts/utils -RUN chmod g+rwX /deployments +# User creation + permissions +RUN addgroup ci && adduser -D -h /module/ -s /bin/bash -G ci ci && \ + chown ci:ci -R /module && \ + chmod +x /module/scripts/*.sh && \ + chmod 644 /module/scripts/*.py 2>/dev/null || true && \ + chmod +x /usr/local/bin/sops -# Ensure sane permissions on copied tree without findutils -RUN chmod -R u=rwX,go=rX /deployments && \ - chmod 540 /deployments/run-java.sh +ENV PATH=/module/venv/bin:$PATH -USER 1001 +USER ci:ci -ENTRYPOINT [ "/deployments/run-java.sh" ] +ENTRYPOINT [""] diff --git a/build_effective_set_generator/build/configuration/constraint.txt b/build_effective_set_generator/build/configuration/constraint.txt new file mode 100644 index 000000000..039eb0db2 --- /dev/null +++ b/build_effective_set_generator/build/configuration/constraint.txt @@ -0,0 +1 @@ +cython<3 diff --git a/build_effective_set_generator/build/configuration/pip.conf b/build_effective_set_generator/build/configuration/pip.conf new file mode 100644 index 000000000..31058a45f --- /dev/null +++ b/build_effective_set_generator/build/configuration/pip.conf @@ -0,0 +1,5 @@ +[global] +index-url=https://pypi.org/simple +extra-index-url=https://example.com/pypi/simple +trusted-host=pypi.org +# constraint=/build/constraint.txt diff --git a/build_effective_set_generator/build/configuration/requirements.txt b/build_effective_set_generator/build/configuration/requirements.txt new file mode 100644 index 000000000..3a4cb8dfa --- /dev/null +++ b/build_effective_set_generator/build/configuration/requirements.txt @@ -0,0 +1,9 @@ +# Base module requirements (essential only) +shyaml==0.6.2 +yamale==4.0.4 +prettytable==3.5.0 +cryptography==38.0.0 +pyyaml>=6.0 +PyGithub==1.55 +certifi==2022.6.15 +GitPython==3.1.45 diff --git a/build_effective_set_generator/build/configuration/sources.list b/build_effective_set_generator/build/configuration/sources.list new file mode 100644 index 000000000..91b969a08 --- /dev/null +++ b/build_effective_set_generator/build/configuration/sources.list @@ -0,0 +1,2 @@ +https://dl-cdn.alpinelinux.org/alpine/v3.20/main +https://dl-cdn.alpinelinux.org/alpine/v3.20/community diff --git a/build_effective_set_generator/build/scripts/decrypt.sh b/build_effective_set_generator/build/scripts/decrypt.sh new file mode 100755 index 000000000..715667598 --- /dev/null +++ b/build_effective_set_generator/build/scripts/decrypt.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -e + +# Ensure externally provided variables are defined to satisfy shellcheck +env_name="${env_name:-}" +encrypt_file_path="${encrypt_file_path:-}" +DECRYPT_TYPE="${DECRYPT_TYPE:-}" +module_fernet_key_name="${module_fernet_key_name:-}" +module_age_key_name="${module_age_key_name:-}" + +FERNET_KEY="CREDENTIALS_SECRET_KEY_${env_name}" +SOPS_AGE_PRIVATE_KEY="AGE_SECRET_KEY_${env_name}" + +if [ -n "${module_fernet_key_name}" ]; then + FERNET_KEY="${module_fernet_key_name}" +fi +if [ -n "${module_age_key_name}" ]; then + SOPS_AGE_PRIVATE_KEY="${module_age_key_name}" +fi + +if [ -n "${!FERNET_KEY}" ] && [ -f "${encrypt_file_path}" ] && [ "${DECRYPT_TYPE}" = 'fernet' ]; then + + echo "${encrypt_file_path} exists and key variable is defined" + python /module/scripts/decrypt_fernet.py decrypt_cred_file --file_path "${encrypt_file_path}" --secret_key "${!FERNET_KEY}" +elif [ -n "${!SOPS_AGE_PRIVATE_KEY}" ] && [ -f "${encrypt_file_path}" ] && [ "${DECRYPT_TYPE}" = 'sops' ]; then + SOPS_AGE_KEY="${!SOPS_AGE_PRIVATE_KEY}" + export SOPS_AGE_KEY + sops --decrypt -i "${encrypt_file_path}" + echo "${encrypt_file_path} was decrypted" +elif [ "${DECRYPT_TYPE}" == 'none' ]; then + echo "Skipping decryption...." +else + echo "Variable encrypt_file_path not exists or key variable is undefined. encrypt_file_path: '$encrypt_file_path'" +fi diff --git a/build_effective_set_generator/build/scripts/decrypt_fernet.py b/build_effective_set_generator/build/scripts/decrypt_fernet.py new file mode 100644 index 000000000..86aa61192 --- /dev/null +++ b/build_effective_set_generator/build/scripts/decrypt_fernet.py @@ -0,0 +1,81 @@ +from envgenehelper import logger + +from yaml import safe_load, safe_dump +import click +from cryptography.fernet import Fernet + +ENCRYPTED_CONST = 'encrypted:AES256_Fernet' + +@click.group(chain=True) +def cmdb_prepare(): + pass + + +@cmdb_prepare.command("decrypt_cred_file") +@click.option('--file_path', '-f', 'file_path', required=True, help="Path to creds file") +@click.option('--secret_key', '-s', 'secret_key', required=True, + help="Set secret_key for encrypt cred files") +def decrypt_file(secret_key, file_path): + ''' {getenv('CI_PROJECT_DIR')}/ansible/inventory/group_vars/{getenv('env_name')}/appdeployer_cmdb/Tenants/{getenv('tenant_name')}/Credentials''' + logger.debug('Try to read %s file', file_path) + with open(file_path, mode="r", encoding="utf-8") as sensitive: + sensitive_data = safe_load(sensitive) + + is_encrypted = check_if_file_is_encrypted(sensitive_data) + if is_encrypted: + if not secret_key: + logger.error(f'Variable "{secret_key}" is not specified') + exit(1) + cipher = Fernet(secret_key) + logger.debug('Try to decrypt data from %s file', file_path) + if isinstance(sensitive_data, dict): + decrypted_data = decode_sensitive(cipher, sensitive_data) + logger.debug('Try to write data to %s file', file_path) + with open(file_path, mode="w") as sensitive: + safe_dump(decrypted_data, sensitive, default_flow_style=False) + logger.info('The %s file has been decrypted', file_path) + else: + logger.info('The %s is empty or has no dict struct or not encrypted', file_path) + else: + logger.info('File is not encrypted') + +def check_if_file_is_encrypted(sensitive_data) -> bool: + for key, data in sensitive_data.items(): + if key != "type" and key != "credentialsId" and data: + if isinstance(data, dict): + if check_if_file_is_encrypted(data): + return True + elif isinstance(data, str): + if ENCRYPTED_CONST in data: + return True + elif isinstance(data, list): + for item in data: + if ENCRYPTED_CONST in item: + return True + + return False + + + +def decode_sensitive(cipher:Fernet, sensitive_data) -> str: + for key, data in sensitive_data.items(): + if key != "type" and key != "credentialsId" and data: + if isinstance(data, dict): + decode_sensitive(cipher, data) + elif isinstance(data, list): + _list = [] + for item in data: + if ENCRYPTED_CONST in item: + _list.append( + cipher.decrypt(item.replace( + f'[{ENCRYPTED_CONST}]','').encode('utf-8')).decode('utf-8')) + sensitive_data[key] = _list + elif ENCRYPTED_CONST in data: + sensitive_data[key] = cipher.decrypt( + data.replace(f'[{ENCRYPTED_CONST}]','').encode('utf-8')).decode('utf-8') + return sensitive_data + + +if __name__ == "__main__": + # yaml = create_yaml_processor() + cmdb_prepare() diff --git a/build_effective_set_generator/build/scripts/get_include_list.sh b/build_effective_set_generator/build/scripts/get_include_list.sh new file mode 100755 index 000000000..68d6b07da --- /dev/null +++ b/build_effective_set_generator/build/scripts/get_include_list.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +CI_FILE="$1" + +include_list=$(shyaml get-value include <"${CI_FILE}" 2>/dev/null || true) +if [[ "$include_list" != "" ]]; then + include_list_length=$(($(shyaml get-value include <"${CI_FILE}" | shyaml get-length) - 1)) + for i in $(seq 0 $include_list_length); do + include_project=$(shyaml get-value include."$i" <"${CI_FILE}" 2>/dev/null | shyaml get-value project 2>/dev/null || true) + if [[ "$include_project" != "" ]]; then + include_project_full_path=$(shyaml get-value include."$i" <"${CI_FILE}" | shyaml get-value project) + include_project_branch=$(shyaml get-value include."$i" <"${CI_FILE}" | shyaml get-value ref) + include_project_file=$(shyaml get-value include."$i" <"${CI_FILE}" | shyaml get-value file) + + include_project_group=${include_project_full_path%/*} + include_project_repo=${include_project_full_path##*/} + + if [[ "$include_project_file" == *"api.yaml"* || "$include_project_file" == *"pipeline.yaml"* ]]; then + module_project_path_result=$(env | grep "${include_project_full_path}") || true + IFS='=' read -r -a module_project_path_array <<<"$module_project_path_result" + module_project_path=${module_project_path_array[0]} + + module_project=${include_project_repo} + module_group=${include_project_group} + module_full_path=${include_project_full_path} + module_version=${include_project_branch} + module_name=${module_project_path//_project_path/} + + # if _project_path not specifed + : "${module_name:=$module_project}" + + cat </dev/stderr + fi + else + unsupported_type=$(shyaml get-value include."$i" <"${CI_FILE}" 2>/dev/null || true) + echo "Included file of unsupported type (${unsupported_type}). Only inlude:file is supported now" >/dev/stderr + fi + done +else + echo "Nothing to include. Check that your ci file contains include section" >/dev/stderr +fi diff --git a/build_effective_set_generator/build/scripts/show_validate.py b/build_effective_set_generator/build/scripts/show_validate.py new file mode 100644 index 000000000..fb90f32bb --- /dev/null +++ b/build_effective_set_generator/build/scripts/show_validate.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +import yaml +import glob +import argparse +from prettytable import PrettyTable, ALL + +parser = argparse.ArgumentParser(description="Get variables from repository") +parser.add_argument("-p", "--path", dest="dest_dir", type=str, help='Path to folder validation') +parser.add_argument("-n", "--name", dest="place_validation", type=str, help='Module name') +args = parser.parse_args() + + +header = ['Module name', 'Place of validation', 'Validation status'] +table = PrettyTable(header, align='l') +table._max_width = {'Module name' : 20, 'Place of validation' : 20, 'Validation status' : 90} +table.hrules = ALL +yaml_file = [] +if args.place_validation: + with open(f"{args.dest_dir}/{args.place_validation}_validation.yaml", "r") as report: + #with open(f"dvm_validation.yaml", "r") as file: + yaml_file = yaml.safe_load(report) + for module in yaml_file: + table.add_row([module['module_name'], module['place_of_validation'], module['validation_status']]) +else: + file_list = glob.glob(f"{args.dest_dir}/*_validation.yaml") + for file in file_list: + with open(file, "r") as report: + yaml_content = yaml.safe_load(report) + for module in yaml_content: + yaml_file.append(module) + for module in yaml_file: + if len(module['validation_status'].split("\n"))>1: + messages = '' + for error in module['validation_status'].split("\n")[1:]: + if error: + messages = messages + "\n\033[33m"+error.split(':')[0]+":\033[39m"+":".join(error.split(':')[1:]) + module['validation_status'] = module['validation_status'].split("\n")[0] + messages + table.add_row([module['module_name'], module['place_of_validation'], module['validation_status']]) +print(table) + diff --git a/build_effective_set_generator/build/scripts/update_ca_certs.sh b/build_effective_set_generator/build/scripts/update_ca_certs.sh new file mode 100755 index 000000000..e6e42d240 --- /dev/null +++ b/build_effective_set_generator/build/scripts/update_ca_certs.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +CA_FILE="$1" + +function getLinuxDisto { + if [[ -f /etc/os-release ]]; then + # freedesktop.org and systemd + # shellcheck disable=SC1091 + . /etc/os-release + DIST=$NAME + elif type lsb_release >/dev/null 2>&1; then + # linuxbase.org + DIST=$(lsb_release -si) + elif [[ -f /etc/lsb-release ]]; then + # For some versions of Debian/Ubuntu without lsb_release command + # shellcheck disable=SC1091 + . /etc/lsb-release + DIST=$DISTRIB_ID + elif [[ -f /etc/debian_version ]]; then + # Older Debian/Ubuntu/etc. + DIST=Debian + else + # Fall back to uname, e.g. "Linux ", also works for BSD, etc. + DIST=$(uname -s) + fi + # convert to lowercase + DIST="$(tr '[:upper:]' '[:lower:]' <<<"$DIST")" +} + +function updateCertificates { + if [[ -e "${CA_FILE}" && -n "${CA_FILE}" ]]; then + getLinuxDisto + echo "Linux Distribution identified as: $DIST" + if [[ "${DIST}" == *"debian"* || "${DIST}" == *"ubuntu"* ]]; then + cp "${CA_FILE}" /usr/local/share/ca-certificates/ca.crt + update-ca-certificates --fresh >/dev/null + elif [[ "${DIST}" == *"centos"* ]]; then + cp "${CA_FILE}" /etc/pki/ca-trust/source/anchors/ca.crt + update-ca-trust + elif [[ "${DIST}" == *"alpine"* ]]; then + cat "${CA_FILE}" >>/etc/ssl/certs/ca-certificates.crt + echo "certs from $CA_FILE added to trusted root" + fi + else + echo "CA file ${CA_FILE} not found or empty" + exit 1 + fi +} + +updateCertificates diff --git a/build_envgene/build/Dockerfile b/build_envgene/build/Dockerfile index 15dcbbec3..3e6390759 100644 --- a/build_envgene/build/Dockerfile +++ b/build_envgene/build/Dockerfile @@ -47,21 +47,21 @@ RUN python -m venv /module/venv RUN /module/venv/bin/pip install --upgrade pip "setuptools<82" wheel RUN /module/venv/bin/pip install --no-cache-dir --retries 10 --timeout 60 -r /build/requirements.txt -RUN /module/venv/bin/pip install /python/jschon-sort -RUN /module/venv/bin/pip install /python/envgene -RUN /module/venv/bin/pip install /python/integration -RUN /module/venv/bin/pip install /python/artifact-searcher -RUN /module/venv/bin/pip install --no-cache-dir --no-deps -r /build/creds_rotation_requirements.txt +RUN /module/venv/bin/pip install /python/jschon-sort && \ + /module/venv/bin/pip install /python/envgene && \ + /module/venv/bin/pip install /python/integration && \ + /module/venv/bin/pip install /python/artifact-searcher && \ + /module/venv/bin/pip install --no-cache-dir --no-deps -r /build/creds_rotation_requirements.txt # Download and install SOPS for secrets management -RUN wget --tries=3 \ +RUN wget --quiet --tries=3 \ https://github.com/mozilla/sops/releases/download/v3.9.0/sops-v3.9.0.linux.amd64 \ -O /usr/local/bin/sops && \ chmod +x /usr/local/bin/sops # Aggressive cleanup to reduce image size -RUN apk del gcc musl-dev libffi-dev openssl-dev libxml2-dev libxslt-dev zlib-dev -RUN rm -rf /var/cache/apk/* /tmp/* /var/tmp/* /root/.cache +RUN apk del gcc musl-dev libffi-dev openssl-dev libxml2-dev libxslt-dev zlib-dev && \ + rm -rf /var/cache/apk/* /tmp/* /var/tmp/* /root/.cache # Remove unnecessary files from Python packages RUN find /module/venv/lib/python3.12/site-packages -name '*.pyc' -delete @@ -72,8 +72,8 @@ RUN rm -rf /module/venv/lib/python3.12/site-packages/pytest* \ RUN /module/venv/bin/pip cache purge # Set permissions -RUN chmod 754 /module/scripts/* -RUN chmod 754 /module/creds_rotation_scripts/* +RUN chmod 754 /module/scripts/* && \ + chmod 754 /module/creds_rotation_scripts/* ######################################### # Stage 2: Runtime diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 194abd016..000000000 --- a/pom.xml +++ /dev/null @@ -1,122 +0,0 @@ - - - 4.0.0 - - org.qubership - qubership_envgene_templates - ${revision} - - ${project.groupId}:${project.artifactId} - >A Maven artifact that contains templates for Qubership Envgene Instance - https://github.com/Netcracker/qubership-envgene - - - Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0 - - - - - Netcracker - opensourcegroup@netcracker.com - Netcracker Technology - https://www.netcracker.com - - - - scm:git:git://github.com/Netcracker/qubership-envgene.git - scm:git:ssh://github.com:Netcracker/qubership-envgene.git - https://github.com/Netcracker/qubership-envgene/tree/main - - - - - central - Central Maven Repository - - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 3.2.7 - - - sign-artifacts - verify - - sign - - - - - - - org.apache.maven.plugins - maven-source-plugin - 3.3.0 - - - attach-sources - - jar-no-fork - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.6.3 - - - attach-javadocs - - jar - - - - - - - org.sonatype.central - central-publishing-maven-plugin - 0.6.0 - true - - central - true - published - - - - - org.apache.maven.plugins - maven-assembly-plugin - 3.6.0 - - - make-assembly - package - - single - - - - - - src/assembly/assembly.xml - - false - - - - - - - 0.0.0.1 - - diff --git a/src/assembly/assembly.xml b/src/assembly/assembly.xml deleted file mode 100644 index 6ab61e251..000000000 --- a/src/assembly/assembly.xml +++ /dev/null @@ -1,18 +0,0 @@ - - templates - - zip - - false - - - build_envgene_templates - / - - **/* - - - - From 42b7ccfdacbf1c31fd81e49ecf8cf0e1ac7aae22 Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:50:45 +0300 Subject: [PATCH 028/161] feat: Added Process SD job to Github Evngene Instance Pipe (#1010) --- .../.github/workflows/Envgene.yml | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 6490556c7..b56b1416a 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -88,6 +88,9 @@ jobs: ENV_INVENTORY_INIT: ${{ env.ENV_INVENTORY_INIT }} ENV_SPECIFIC_PARAMS: ${{ env.ENV_SPECIFIC_PARAMS }} ENV_TEMPLATE_NAME: ${{ env.ENV_TEMPLATE_NAME }} + SD_SOURCE_TYPE: ${{ env.SD_SOURCE_TYPE }} + SD_DATA: ${{ env.SD_DATA }} + SD_VERSION: ${{ env.SD_VERSION }} steps: - name: Repository Checkout uses: actions/checkout@v4.1.0 @@ -291,6 +294,38 @@ jobs: ########################## + ### PROCESS_SD ### + - name: PROCESS_SD + if: (needs.process_environment_variables.outputs.SD_SOURCE_TYPE == 'json' && needs.process_environment_variables.outputs.SD_DATA != '') || (needs.process_environment_variables.outputs.SD_SOURCE_TYPE == 'artifact' && needs.process_environment_variables.outputs.SD_VERSION != '') + run: | + docker run --rm -v ${{ github.workspace }}:${{ env.CI_PROJECT_DIR }} -w ${{ env.CI_PROJECT_DIR }} --env-file .env.container --user root -e HOME=/root \ + ${{ env.DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR }}:${{ env.DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR }} \ + bash -c " + source /module/venv/bin/activate + + echo 'Executing PROCESS_SD...' + /module/scripts/handle_certs.sh + base_env_path=\"\${CI_PROJECT_DIR}/environments/${{ matrix.environment }}\" + app_defs_path=\"\${base_env_path}/AppDefs\" + reg_defs_path=\"\${base_env_path}/RegDefs\" + if [ -n \"\$APP_REG_DEFS_JOB\" ] && [ -n \"\$APP_DEFS_PATH\" ]; then mkdir -p \"\$app_defs_path\" && cp -rf \"\$APP_DEFS_PATH\"/* \"\$app_defs_path\"; fi + if [ -n \"\$APP_REG_DEFS_JOB\" ] && [ -n \"\$REG_DEFS_PATH\" ]; then mkdir -p \"\$reg_defs_path\" && cp -fr \"\$REG_DEFS_PATH\"/* \"\$reg_defs_path\"; fi + if ! python3 /build_env/scripts/build_env/process_sd.py; then + echo 'PROCESS_SD failed with exit code \$?' + exit 1 + fi + " + + - name: PROCESS_SD - Upload PROCESS_SD Package + uses: actions/upload-artifact@v4 + if: (needs.process_environment_variables.outputs.SD_SOURCE_TYPE == 'json' && needs.process_environment_variables.outputs.SD_DATA != '') || (needs.process_environment_variables.outputs.SD_SOURCE_TYPE == 'artifact' && needs.process_environment_variables.outputs.SD_VERSION != '') + with: + name: process_sd_${{ env.PACKAGE_NAME }} + path: environments/${{ matrix.environment }} + include-hidden-files: true + ################## + + ### ENV_BUILD ### - name: ENV_BUILD if: needs.process_environment_variables.outputs.ENV_BUILDER == 'true' From ea2e1e24350745ce321f333fe22439d91d6953f5 Mon Sep 17 00:00:00 2001 From: Dias <120464230+dysmon@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:12:33 +0500 Subject: [PATCH 029/161] feat: open source publication of generate_effective_set (#1003) --- .../actions/build-effective-set/action.yml | 2 +- build_effective_set_generator/Dockerfile | 111 --------------- .../build/Dockerfile | 103 ++++++++++++++ .../build/configuration/constraint.txt | 1 - .../build/configuration/requirements.txt | 9 -- .../build/{configuration => }/pip.conf | 1 - .../build/requirements.txt | 32 +++++ .../build/scripts/decrypt.sh | 34 ----- .../build/scripts/decrypt_fernet.py | 81 ----------- .../build/scripts/get_include_list.sh | 49 ------- .../build/scripts/show_validate.py | 41 ------ .../build/scripts/update_ca_certs.sh | 50 ------- .../build/{configuration => }/sources.list | 0 .../scripts/handle_effective_set_config.py | 109 +++++++++++++++ build_effective_set_generator/scripts/main.py | 26 ++++ build_pipegene/scripts/effective_set_job.py | 126 ++++++++++++++++++ build_pipegene/scripts/env_build_jobs.py | 34 ----- build_pipegene/scripts/gitlab_ci.py | 8 +- python/envgene/envgenehelper/creds_helper.py | 56 +++++++- scripts/utils/pipeline_parameters.py | 6 +- 20 files changed, 462 insertions(+), 417 deletions(-) delete mode 100644 build_effective_set_generator/Dockerfile create mode 100644 build_effective_set_generator/build/Dockerfile delete mode 100644 build_effective_set_generator/build/configuration/constraint.txt delete mode 100644 build_effective_set_generator/build/configuration/requirements.txt rename build_effective_set_generator/build/{configuration => }/pip.conf (76%) create mode 100644 build_effective_set_generator/build/requirements.txt delete mode 100755 build_effective_set_generator/build/scripts/decrypt.sh delete mode 100644 build_effective_set_generator/build/scripts/decrypt_fernet.py delete mode 100755 build_effective_set_generator/build/scripts/get_include_list.sh delete mode 100644 build_effective_set_generator/build/scripts/show_validate.py delete mode 100755 build_effective_set_generator/build/scripts/update_ca_certs.sh rename build_effective_set_generator/build/{configuration => }/sources.list (100%) create mode 100644 build_effective_set_generator/scripts/handle_effective_set_config.py create mode 100644 build_effective_set_generator/scripts/main.py create mode 100644 build_pipegene/scripts/effective_set_job.py diff --git a/.github/actions/build-effective-set/action.yml b/.github/actions/build-effective-set/action.yml index a7b65eb65..73e7aa3c3 100644 --- a/.github/actions/build-effective-set/action.yml +++ b/.github/actions/build-effective-set/action.yml @@ -18,7 +18,7 @@ inputs: dockerfile-path: description: 'Path to Dockerfile' required: false - default: './build_effective_set_generator/Dockerfile' + default: './build_effective_set_generator/build/Dockerfile' git-user: description: 'Git username for build args' required: false diff --git a/build_effective_set_generator/Dockerfile b/build_effective_set_generator/Dockerfile deleted file mode 100644 index ca57f17b4..000000000 --- a/build_effective_set_generator/Dockerfile +++ /dev/null @@ -1,111 +0,0 @@ -############################################# -# STAGE 1: CLI (UBI minimal runtime with Java) -############################################# -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.1 AS cli - -ARG JAVA_PACKAGE=java-17-openjdk-headless -ARG RUN_JAVA_VERSION=1.3.8 - -# hadolint ignore=DL3041 -RUN microdnf install -y curl ca-certificates ${JAVA_PACKAGE} && \ - microdnf clean all && \ - mkdir -p /deployments && \ - curl -sSf https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh \ - -o /deployments/run-java.sh && \ - chmod 540 /deployments/run-java.sh && \ - echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security - -COPY build_effective_set_generator/effective-set-generator/target/*.jar /deployments/app.jar - - - -############################################# -# STAGE 2: Build Python Environment (Alpine) -############################################# -FROM python:3.12-alpine3.19 AS build - -# Install ALL system-level *build dependencies* -# hadolint ignore=DL3018 -RUN apk add --no-cache \ - gcc \ - musl-dev \ - libffi-dev \ - openssl-dev \ - python3-dev \ - cargo \ - rust \ - build-base \ - curl \ - wget - -COPY build_effective_set_generator/build/configuration/pip.conf /etc/pip.conf -COPY build_effective_set_generator/build/configuration/requirements.txt /build/requirements.txt -COPY build_effective_set_generator/build/configuration/constraint.txt /build/constraint.txt - -# Build Python virtual environment -RUN python3 -m venv /module/venv && \ - /module/venv/bin/pip install --upgrade pip setuptools wheel && \ - /module/venv/bin/pip install --no-cache-dir --no-binary cffi --no-binary cryptography \ - -r /build/requirements.txt - -# Install SOPS -RUN wget --quiet --tries=3 \ - https://github.com/mozilla/sops/releases/download/v3.9.0/sops-v3.9.0.linux.amd64 \ - -O /usr/local/bin/sops && \ - chmod +x /usr/local/bin/sops - -# DO NOT delete build dependencies here — runtime needs them indirectly - - - - -############################################# -# STAGE 3: Final Runtime (Alpine) -############################################# -FROM python:3.12-alpine3.19 AS runtime - -ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' - -COPY --from=cli --chown=1001:root /deployments /deployments -COPY --from=cli /etc/alternatives/jre/lib/security/java.security \ - /etc/alternatives/jre/lib/security/java.security - -RUN chmod g+rwX /deployments - -COPY build_effective_set_generator/build/configuration/pip.conf /etc/pip.conf -COPY build_effective_set_generator/build/configuration/constraint.txt /build/constraint.txt -COPY build_effective_set_generator/build/configuration/sources.list /etc/apk/repositories - -# Install ONLY runtime dependencies (NO dev packages) -# hadolint ignore=DL3018 -RUN apk add --no-cache \ - libffi \ - openssl \ - bash \ - ca-certificates \ - tar \ - curl \ - jq \ - yq \ - gettext \ - sed \ - age - -# Bring the virtual environment compiled in build stage -COPY --from=build /module/venv /module/venv -COPY --from=build /usr/local/bin/sops /usr/local/bin/sops -COPY build_effective_set_generator/build/scripts /module/scripts -COPY scripts/utils /module/scripts/utils - -# User creation + permissions -RUN addgroup ci && adduser -D -h /module/ -s /bin/bash -G ci ci && \ - chown ci:ci -R /module && \ - chmod +x /module/scripts/*.sh && \ - chmod 644 /module/scripts/*.py 2>/dev/null || true && \ - chmod +x /usr/local/bin/sops - -ENV PATH=/module/venv/bin:$PATH - -USER ci:ci - -ENTRYPOINT [""] diff --git a/build_effective_set_generator/build/Dockerfile b/build_effective_set_generator/build/Dockerfile new file mode 100644 index 000000000..93117d9f9 --- /dev/null +++ b/build_effective_set_generator/build/Dockerfile @@ -0,0 +1,103 @@ +FROM python:3.12-alpine3.19 AS build + +ARG RUN_JAVA_VERSION=1.3.8 +ARG SOPS_VERSION=3.9.0 + +# hadolint ignore=DL3018 +RUN apk add --no-cache \ + gcc \ + musl-dev \ + libffi-dev \ + openssl-dev \ + python3-dev \ + cargo \ + rust \ + build-base \ + curl \ + wget + +COPY build_effective_set_generator/build/sources.list /etc/apk/repositories +COPY build_effective_set_generator/build/pip.conf /etc/pip.conf +COPY build_effective_set_generator/build/requirements.txt /build/requirements.txt +COPY python /python + +RUN python3 -m venv /module/venv && \ + /module/venv/bin/pip install --upgrade pip "setuptools<82" wheel + +RUN /module/venv/bin/pip install --no-cache-dir \ + --no-binary cffi \ + --no-binary cryptography \ + -r /build/requirements.txt + +RUN /module/venv/bin/pip install --no-cache-dir \ + /python/integration \ + /python/jschon-sort \ + /python/envgene \ + /python/artifact-searcher + +RUN curl -fsSL \ + https://github.com/mozilla/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.linux.amd64 \ + -o /usr/local/bin/sops && \ + chmod +x /usr/local/bin/sops + +RUN mkdir -p /deployments && \ + curl -sSL https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh \ + -o /deployments/run-java.sh + +COPY build_effective_set_generator/effective-set-generator/target/*.jar /deployments/app.jar + + +FROM python:3.12-alpine3.19 AS runtime + +ARG JAVA_PACKAGE=openjdk17-jre-headless + +ENV LANG='en_US.UTF-8' \ + LANGUAGE='en_US:en' \ + PATH=/module/venv/bin:$PATH + +COPY build_effective_set_generator/build/pip.conf /etc/pip.conf +COPY build_effective_set_generator/build/sources.list /etc/apk/repositories + +# Install ONLY runtime dependencies (NO dev packages) +# hadolint ignore=DL3018 +RUN apk add --no-cache \ + libffi \ + libgcc \ + openssl \ + bash \ + ca-certificates \ + tar \ + curl \ + jq \ + yq \ + gettext \ + sed \ + age \ + ${JAVA_PACKAGE} + +COPY --from=build /python /python +COPY --from=build /module/venv /module/venv +COPY --from=build /usr/local/bin/sops /usr/local/bin/sops +COPY --from=build /deployments /deployments + +COPY scripts/build_env/ /build_env/scripts/build_env/ +COPY scripts/build_env/process_sd.py /module/scripts/ +COPY build_effective_set_generator/scripts/ /module/scripts/ +COPY scripts/utils/ /module/scripts/utils/ + +# cert script should be moved to scripts/utils/ +COPY build_envgene/scripts/handle_certs.sh /module/scripts/ + +# User creation + permissions +RUN addgroup ci && \ + adduser -D -h /module/ -s /bin/bash -G ci ci && \ + chown ci:ci -R /module /deployments && \ + chmod +x /module/scripts/*.sh && \ + chmod 644 /module/scripts/*.py 2>/dev/null || true && \ + chmod +x /usr/local/bin/sops && \ + chmod 540 /deployments/run-java.sh && \ + chmod g+rwX /deployments + +USER ci:ci + +WORKDIR /module diff --git a/build_effective_set_generator/build/configuration/constraint.txt b/build_effective_set_generator/build/configuration/constraint.txt deleted file mode 100644 index 039eb0db2..000000000 --- a/build_effective_set_generator/build/configuration/constraint.txt +++ /dev/null @@ -1 +0,0 @@ -cython<3 diff --git a/build_effective_set_generator/build/configuration/requirements.txt b/build_effective_set_generator/build/configuration/requirements.txt deleted file mode 100644 index 3a4cb8dfa..000000000 --- a/build_effective_set_generator/build/configuration/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Base module requirements (essential only) -shyaml==0.6.2 -yamale==4.0.4 -prettytable==3.5.0 -cryptography==38.0.0 -pyyaml>=6.0 -PyGithub==1.55 -certifi==2022.6.15 -GitPython==3.1.45 diff --git a/build_effective_set_generator/build/configuration/pip.conf b/build_effective_set_generator/build/pip.conf similarity index 76% rename from build_effective_set_generator/build/configuration/pip.conf rename to build_effective_set_generator/build/pip.conf index 31058a45f..8cfdd31ec 100644 --- a/build_effective_set_generator/build/configuration/pip.conf +++ b/build_effective_set_generator/build/pip.conf @@ -2,4 +2,3 @@ index-url=https://pypi.org/simple extra-index-url=https://example.com/pypi/simple trusted-host=pypi.org -# constraint=/build/constraint.txt diff --git a/build_effective_set_generator/build/requirements.txt b/build_effective_set_generator/build/requirements.txt new file mode 100644 index 000000000..a4c3ffb31 --- /dev/null +++ b/build_effective_set_generator/build/requirements.txt @@ -0,0 +1,32 @@ +# Base module requirements (essential only) +PyGithub==1.55 +certifi==2022.6.15 + +boto3==1.29.3 +botocore==1.32.3 +gcip==3.0.2 +jmespath==1.0.1 +packaging==23.2 +python-dateutil==2.8.2 +PyYAML==6.0.1 +s3transfer==0.7.0 +setuptools-git-versioning==1.13.5 +six==1.16.0 +toml==0.10.2 +urllib3==2.0.7 +lxml==4.9.3 +ruamel.yaml==0.18.5 +ruamel.yaml.clib==0.2.8 +ruyaml==0.91.0 +jschon==0.11.0 +jsonschema==4.19.1 +diagrams==0.24.1 +graphviz==0.20.3 +attrs==23.2.0 +referencing==0.33.0 +rpds-py==0.17.1 +jsonschema-specifications==2023.12.1 +cryptography==41.0.3 +click==8.1.7 +deepmerge==2.0 +flatten_dict==0.4.2 \ No newline at end of file diff --git a/build_effective_set_generator/build/scripts/decrypt.sh b/build_effective_set_generator/build/scripts/decrypt.sh deleted file mode 100755 index 715667598..000000000 --- a/build_effective_set_generator/build/scripts/decrypt.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -set -e - -# Ensure externally provided variables are defined to satisfy shellcheck -env_name="${env_name:-}" -encrypt_file_path="${encrypt_file_path:-}" -DECRYPT_TYPE="${DECRYPT_TYPE:-}" -module_fernet_key_name="${module_fernet_key_name:-}" -module_age_key_name="${module_age_key_name:-}" - -FERNET_KEY="CREDENTIALS_SECRET_KEY_${env_name}" -SOPS_AGE_PRIVATE_KEY="AGE_SECRET_KEY_${env_name}" - -if [ -n "${module_fernet_key_name}" ]; then - FERNET_KEY="${module_fernet_key_name}" -fi -if [ -n "${module_age_key_name}" ]; then - SOPS_AGE_PRIVATE_KEY="${module_age_key_name}" -fi - -if [ -n "${!FERNET_KEY}" ] && [ -f "${encrypt_file_path}" ] && [ "${DECRYPT_TYPE}" = 'fernet' ]; then - - echo "${encrypt_file_path} exists and key variable is defined" - python /module/scripts/decrypt_fernet.py decrypt_cred_file --file_path "${encrypt_file_path}" --secret_key "${!FERNET_KEY}" -elif [ -n "${!SOPS_AGE_PRIVATE_KEY}" ] && [ -f "${encrypt_file_path}" ] && [ "${DECRYPT_TYPE}" = 'sops' ]; then - SOPS_AGE_KEY="${!SOPS_AGE_PRIVATE_KEY}" - export SOPS_AGE_KEY - sops --decrypt -i "${encrypt_file_path}" - echo "${encrypt_file_path} was decrypted" -elif [ "${DECRYPT_TYPE}" == 'none' ]; then - echo "Skipping decryption...." -else - echo "Variable encrypt_file_path not exists or key variable is undefined. encrypt_file_path: '$encrypt_file_path'" -fi diff --git a/build_effective_set_generator/build/scripts/decrypt_fernet.py b/build_effective_set_generator/build/scripts/decrypt_fernet.py deleted file mode 100644 index 86aa61192..000000000 --- a/build_effective_set_generator/build/scripts/decrypt_fernet.py +++ /dev/null @@ -1,81 +0,0 @@ -from envgenehelper import logger - -from yaml import safe_load, safe_dump -import click -from cryptography.fernet import Fernet - -ENCRYPTED_CONST = 'encrypted:AES256_Fernet' - -@click.group(chain=True) -def cmdb_prepare(): - pass - - -@cmdb_prepare.command("decrypt_cred_file") -@click.option('--file_path', '-f', 'file_path', required=True, help="Path to creds file") -@click.option('--secret_key', '-s', 'secret_key', required=True, - help="Set secret_key for encrypt cred files") -def decrypt_file(secret_key, file_path): - ''' {getenv('CI_PROJECT_DIR')}/ansible/inventory/group_vars/{getenv('env_name')}/appdeployer_cmdb/Tenants/{getenv('tenant_name')}/Credentials''' - logger.debug('Try to read %s file', file_path) - with open(file_path, mode="r", encoding="utf-8") as sensitive: - sensitive_data = safe_load(sensitive) - - is_encrypted = check_if_file_is_encrypted(sensitive_data) - if is_encrypted: - if not secret_key: - logger.error(f'Variable "{secret_key}" is not specified') - exit(1) - cipher = Fernet(secret_key) - logger.debug('Try to decrypt data from %s file', file_path) - if isinstance(sensitive_data, dict): - decrypted_data = decode_sensitive(cipher, sensitive_data) - logger.debug('Try to write data to %s file', file_path) - with open(file_path, mode="w") as sensitive: - safe_dump(decrypted_data, sensitive, default_flow_style=False) - logger.info('The %s file has been decrypted', file_path) - else: - logger.info('The %s is empty or has no dict struct or not encrypted', file_path) - else: - logger.info('File is not encrypted') - -def check_if_file_is_encrypted(sensitive_data) -> bool: - for key, data in sensitive_data.items(): - if key != "type" and key != "credentialsId" and data: - if isinstance(data, dict): - if check_if_file_is_encrypted(data): - return True - elif isinstance(data, str): - if ENCRYPTED_CONST in data: - return True - elif isinstance(data, list): - for item in data: - if ENCRYPTED_CONST in item: - return True - - return False - - - -def decode_sensitive(cipher:Fernet, sensitive_data) -> str: - for key, data in sensitive_data.items(): - if key != "type" and key != "credentialsId" and data: - if isinstance(data, dict): - decode_sensitive(cipher, data) - elif isinstance(data, list): - _list = [] - for item in data: - if ENCRYPTED_CONST in item: - _list.append( - cipher.decrypt(item.replace( - f'[{ENCRYPTED_CONST}]','').encode('utf-8')).decode('utf-8')) - sensitive_data[key] = _list - elif ENCRYPTED_CONST in data: - sensitive_data[key] = cipher.decrypt( - data.replace(f'[{ENCRYPTED_CONST}]','').encode('utf-8')).decode('utf-8') - return sensitive_data - - -if __name__ == "__main__": - # yaml = create_yaml_processor() - cmdb_prepare() diff --git a/build_effective_set_generator/build/scripts/get_include_list.sh b/build_effective_set_generator/build/scripts/get_include_list.sh deleted file mode 100755 index 68d6b07da..000000000 --- a/build_effective_set_generator/build/scripts/get_include_list.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash - -CI_FILE="$1" - -include_list=$(shyaml get-value include <"${CI_FILE}" 2>/dev/null || true) -if [[ "$include_list" != "" ]]; then - include_list_length=$(($(shyaml get-value include <"${CI_FILE}" | shyaml get-length) - 1)) - for i in $(seq 0 $include_list_length); do - include_project=$(shyaml get-value include."$i" <"${CI_FILE}" 2>/dev/null | shyaml get-value project 2>/dev/null || true) - if [[ "$include_project" != "" ]]; then - include_project_full_path=$(shyaml get-value include."$i" <"${CI_FILE}" | shyaml get-value project) - include_project_branch=$(shyaml get-value include."$i" <"${CI_FILE}" | shyaml get-value ref) - include_project_file=$(shyaml get-value include."$i" <"${CI_FILE}" | shyaml get-value file) - - include_project_group=${include_project_full_path%/*} - include_project_repo=${include_project_full_path##*/} - - if [[ "$include_project_file" == *"api.yaml"* || "$include_project_file" == *"pipeline.yaml"* ]]; then - module_project_path_result=$(env | grep "${include_project_full_path}") || true - IFS='=' read -r -a module_project_path_array <<<"$module_project_path_result" - module_project_path=${module_project_path_array[0]} - - module_project=${include_project_repo} - module_group=${include_project_group} - module_full_path=${include_project_full_path} - module_version=${include_project_branch} - module_name=${module_project_path//_project_path/} - - # if _project_path not specifed - : "${module_name:=$module_project}" - - cat </dev/stderr - fi - else - unsupported_type=$(shyaml get-value include."$i" <"${CI_FILE}" 2>/dev/null || true) - echo "Included file of unsupported type (${unsupported_type}). Only inlude:file is supported now" >/dev/stderr - fi - done -else - echo "Nothing to include. Check that your ci file contains include section" >/dev/stderr -fi diff --git a/build_effective_set_generator/build/scripts/show_validate.py b/build_effective_set_generator/build/scripts/show_validate.py deleted file mode 100644 index fb90f32bb..000000000 --- a/build_effective_set_generator/build/scripts/show_validate.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python3 - -import yaml -import glob -import argparse -from prettytable import PrettyTable, ALL - -parser = argparse.ArgumentParser(description="Get variables from repository") -parser.add_argument("-p", "--path", dest="dest_dir", type=str, help='Path to folder validation') -parser.add_argument("-n", "--name", dest="place_validation", type=str, help='Module name') -args = parser.parse_args() - - -header = ['Module name', 'Place of validation', 'Validation status'] -table = PrettyTable(header, align='l') -table._max_width = {'Module name' : 20, 'Place of validation' : 20, 'Validation status' : 90} -table.hrules = ALL -yaml_file = [] -if args.place_validation: - with open(f"{args.dest_dir}/{args.place_validation}_validation.yaml", "r") as report: - #with open(f"dvm_validation.yaml", "r") as file: - yaml_file = yaml.safe_load(report) - for module in yaml_file: - table.add_row([module['module_name'], module['place_of_validation'], module['validation_status']]) -else: - file_list = glob.glob(f"{args.dest_dir}/*_validation.yaml") - for file in file_list: - with open(file, "r") as report: - yaml_content = yaml.safe_load(report) - for module in yaml_content: - yaml_file.append(module) - for module in yaml_file: - if len(module['validation_status'].split("\n"))>1: - messages = '' - for error in module['validation_status'].split("\n")[1:]: - if error: - messages = messages + "\n\033[33m"+error.split(':')[0]+":\033[39m"+":".join(error.split(':')[1:]) - module['validation_status'] = module['validation_status'].split("\n")[0] + messages - table.add_row([module['module_name'], module['place_of_validation'], module['validation_status']]) -print(table) - diff --git a/build_effective_set_generator/build/scripts/update_ca_certs.sh b/build_effective_set_generator/build/scripts/update_ca_certs.sh deleted file mode 100755 index e6e42d240..000000000 --- a/build_effective_set_generator/build/scripts/update_ca_certs.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -CA_FILE="$1" - -function getLinuxDisto { - if [[ -f /etc/os-release ]]; then - # freedesktop.org and systemd - # shellcheck disable=SC1091 - . /etc/os-release - DIST=$NAME - elif type lsb_release >/dev/null 2>&1; then - # linuxbase.org - DIST=$(lsb_release -si) - elif [[ -f /etc/lsb-release ]]; then - # For some versions of Debian/Ubuntu without lsb_release command - # shellcheck disable=SC1091 - . /etc/lsb-release - DIST=$DISTRIB_ID - elif [[ -f /etc/debian_version ]]; then - # Older Debian/Ubuntu/etc. - DIST=Debian - else - # Fall back to uname, e.g. "Linux ", also works for BSD, etc. - DIST=$(uname -s) - fi - # convert to lowercase - DIST="$(tr '[:upper:]' '[:lower:]' <<<"$DIST")" -} - -function updateCertificates { - if [[ -e "${CA_FILE}" && -n "${CA_FILE}" ]]; then - getLinuxDisto - echo "Linux Distribution identified as: $DIST" - if [[ "${DIST}" == *"debian"* || "${DIST}" == *"ubuntu"* ]]; then - cp "${CA_FILE}" /usr/local/share/ca-certificates/ca.crt - update-ca-certificates --fresh >/dev/null - elif [[ "${DIST}" == *"centos"* ]]; then - cp "${CA_FILE}" /etc/pki/ca-trust/source/anchors/ca.crt - update-ca-trust - elif [[ "${DIST}" == *"alpine"* ]]; then - cat "${CA_FILE}" >>/etc/ssl/certs/ca-certificates.crt - echo "certs from $CA_FILE added to trusted root" - fi - else - echo "CA file ${CA_FILE} not found or empty" - exit 1 - fi -} - -updateCertificates diff --git a/build_effective_set_generator/build/configuration/sources.list b/build_effective_set_generator/build/sources.list similarity index 100% rename from build_effective_set_generator/build/configuration/sources.list rename to build_effective_set_generator/build/sources.list diff --git a/build_effective_set_generator/scripts/handle_effective_set_config.py b/build_effective_set_generator/scripts/handle_effective_set_config.py new file mode 100644 index 000000000..0c6536c37 --- /dev/null +++ b/build_effective_set_generator/scripts/handle_effective_set_config.py @@ -0,0 +1,109 @@ +import json +import os +import tempfile +import shutil +import argparse +from envgenehelper import logger + +def handle_effective_set_config(config_str): + + if isinstance(config_str, str): + try: + config = json.loads(config_str) + except json.JSONDecodeError as e: + logger.error(f"Failed to parse JSON: {e}") + raise + + version = config.get("version") or "v2.0" + extra_args = [f"--effective-set-version={version}"] + + app_chart_value = config.get("app_chart_validation", True) + extra_args.append(f"--app_chart_validation={str(app_chart_value).lower()}") + + consumers = ( + config.get("contexts", {}) + .get("pipeline", {}) + .get("consumers", []) + ) + + if not isinstance(consumers, list) or not consumers: + logger.info("No consumers provided; skipping schema generation.") + result_args = { + "extra_args": extra_args, # Only --effective-set-version + } + logger.info(json.dumps(result_args)) + return result_args + + temp_root = tempfile.gettempdir() + schema_output_dir = os.path.join(temp_root, "schemas", "registered_consumer_specific") + os.makedirs(schema_output_dir, exist_ok=True) + logger.info(f"Ensured directory exists: {schema_output_dir}") + + image_schema_dir = "/module/schemas/registered_consumer_specific" + + for consumer in consumers: + schema_json = consumer.get("schema") + name = consumer.get("name") + consumer_version = consumer.get("version") + + if not name or not consumer_version: + logger.error(f"Consumer entry missing required 'name' or 'version'") + continue + + filename = f"{name}-{consumer_version}.schema.json" + schema_file_path = os.path.join(schema_output_dir, filename) + + if schema_json: + try: + logger.info(f"Processing schema file: {filename}") + with open(schema_file_path, 'w') as schema_file: + json.dump(schema_json, schema_file, indent=2) + if os.path.isfile(schema_file_path): + logger.info(f"Schema file written successfully: {schema_file_path}, size={os.path.getsize(schema_file_path)}") + else: + logger.error(f"Schema file NOT found after writing: {schema_file_path}") + logger.info(f"Wrote schema for consumer '{name}' to {schema_file_path}") + except Exception as e: + logger.error(f"Failed to write schema for consumer '{name}': {e}") + continue + + else: + fallback_path = os.path.join(image_schema_dir, filename) + if os.path.isfile(fallback_path): + try: + shutil.copy(fallback_path, schema_file_path) + logger.info(f"Copied schema for {name} to {schema_file_path}") + except Exception as e: + logger.error(f"Failed to copy schema: {e}") + continue + else: + logger.error(f"Schema not found: {fallback_path}") + continue + + extra_args.append(f"--pipeline-consumer-specific-schema-path={schema_file_path}") + + result_args = { + "extra_args": extra_args, + } + return result_args + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--effective-set-config", + required=True, + help="JSON string or path to JSON file" + ) + args = parser.parse_args() + config_str = args.effective_set_config + + logger.info(f"config_str inside: {config_str}") + + try: + result_args = handle_effective_set_config(config_str) + logger.info(f"Resolved Extra args: {result_args}") + with open("/tmp/effective_set_output.json", "w") as f: + json.dump(result_args, f) + except Exception as e: + logger.error(f"Error: {e}") + exit(1) diff --git a/build_effective_set_generator/scripts/main.py b/build_effective_set_generator/scripts/main.py new file mode 100644 index 000000000..ce2669fa6 --- /dev/null +++ b/build_effective_set_generator/scripts/main.py @@ -0,0 +1,26 @@ +import click +from envgenehelper import encrypt_all_cred_files_for_env, decrypt_all_cred_files_for_env, validate_creds + + +@click.group(chain=True) +def crypt_manager(): + pass + + +@crypt_manager.command("decrypt_cred_files") +def decrypt_cred_files(): + decrypt_all_cred_files_for_env() + + +@crypt_manager.command("encrypt_cred_files") +def encrypt_cred_files(): + encrypt_all_cred_files_for_env() + + +@crypt_manager.command("validate_creds") +def validate_credentials(): + validate_creds() + + +if __name__ == "__main__": + crypt_manager() diff --git a/build_pipegene/scripts/effective_set_job.py b/build_pipegene/scripts/effective_set_job.py new file mode 100644 index 000000000..d66199c86 --- /dev/null +++ b/build_pipegene/scripts/effective_set_job.py @@ -0,0 +1,126 @@ +import json +from os import getenv, environ +from pathlib import Path + +from gcip import WhenStatement, Need + +from envgenehelper import logger +from envgenehelper import cleanup_targets +from pipeline_helper import job_instance + + +def prepare_generate_effective_set_job(pipeline, full_env_name, env_name, cluster_name, params): + logger.info(f'Prepare generate-effective-set job for {full_env_name}') + logger.info(f'Cleanup_targets: {cleanup_targets}') + + app_reg_defs_job = params["APP_REG_DEFS_JOB"] + artifact_app_defs_path = params["APP_DEFS_PATH"] + artifact_reg_defs_path = params["REG_DEFS_PATH"] + sd_version = params["SD_VERSION"] + sd_data = params["SD_DATA"] + deployment_id = params["DEPLOYMENT_SESSION_ID"] + effective_set_config = params["EFFECTIVE_SET_CONFIG"] + tags = params['GITLAB_RUNNER_TAG_NAME'] + + is_local_app_def = artifact_app_defs_path and artifact_reg_defs_path and app_reg_defs_job + + base_dir = getenv('CI_PROJECT_DIR') + base_env_path = f"{base_dir}/environments/{full_env_name}" + app_defs_path = f"{base_env_path}/AppDefs" + reg_defs_path = f"{base_env_path}/RegDefs" + sboms_path = f"{base_dir}/sboms" + + sd_path = Path(f'{base_dir}/environments/{full_env_name}/Inventory/solution-descriptor/sd.yaml') + # TODO it is necessary to remove unnecessary calls, leave only script calls in such jobs! bad for gsf delivery + script = [ + '/module/scripts/handle_certs.sh', + # cert handling for java + 'mkdir -p ${CI_PROJECT_DIR}/configuration/certs/ && cp /default_cert.pem "${CI_PROJECT_DIR}/configuration/certs/default_cert.pem"', + 'for cert in "${CI_PROJECT_DIR}/configuration/certs/*" ; do [ -f "$cert" ] && keytool -import -trustcacerts -alias "$(basename "$cert")" -file "$cert" -keystore /etc/ssl/certs/keystore.jks -storepass changeit -noprompt; done', + 'python3 /module/scripts/main.py decrypt_cred_files', + f'[ -n "$APP_REG_DEFS_JOB" ] && [ -n "$APP_DEFS_PATH" ] && mkdir -p {app_defs_path} && cp -rf {artifact_app_defs_path}/* {app_defs_path}', + f'[ -n "$APP_REG_DEFS_JOB" ] && [ -n "$REG_DEFS_PATH" ] && mkdir -p {reg_defs_path} && cp -fr {artifact_reg_defs_path}/* {reg_defs_path}', + 'python3 /module/scripts/main.py validate_creds', + ] + + cmdb_cli_cmd_call = [ + f"/deployments/run-java.sh --env-id={full_env_name}", + "--envs-path=$CI_PROJECT_DIR/environments", + f"--output=$CI_PROJECT_DIR/environments/{full_env_name}/effective-set" + ] + + effective_set_config_dict = {} + if effective_set_config: + effective_set_config_dict = json.loads(effective_set_config) + + effective_set_version = effective_set_config_dict.get("version") or "v2.0" + full_sd_exists = sd_path.parent.is_dir() and sd_path.is_file() + sd_data = bool(sd_data) or bool(sd_version) + + if not (full_sd_exists and sd_data) and effective_set_version.lower() == "v1.0": + raise ValueError("Feature generation effective set for pipeline and topology context is not supported for v1.0") + + if full_sd_exists or sd_data: + cmdb_cli_cmd_call.extend([ + "--registries=${CI_PROJECT_DIR}/configuration/registry.yml", + f"--sboms-path={sboms_path}", + f"--sd-path={sd_path}", + ]) + + logger.info(f'Prepare generate_effective_set job for {full_env_name}.') + if effective_set_config: + logger.info(f"EFFECTIVE_SET_CONFIG: {effective_set_config}") + script.extend([ + f"python3 /module/scripts/handle_effective_set_config.py --effective-set-config '{effective_set_config}'", + 'extra_args=$(jq -r \'.extra_args // [] | join(" ")\' /tmp/effective_set_output.json)', + ]) + cmdb_cli_cmd_call.extend(["$extra_args"]) + if deployment_id: + cmdb_cli_cmd_call.extend([f"--extra_params=DEPLOYMENT_SESSION_ID={deployment_id}"]) + + script.append(" ".join(cmdb_cli_cmd_call)) + script.append('python3 /module/scripts/main.py encrypt_cred_files') + + generate_effective_set_params = { + "name": f'generate_effective_set.{full_env_name}', + "image": '${effective_set_generator_image}', + "stage": 'generate_effective_set', + "script": script + } + + generate_effective_set_vars = { + "CLUSTER_NAME": cluster_name, + "ENVIRONMENT_NAME": env_name, + "ENV_NAME": env_name, + "INSTANCES_DIR": "${CI_PROJECT_DIR}/environments", + "effective_set_generator_image": "$effective_set_generator_image", + "envgen_args": " -vv", + "envgen_debug": "true", + "module_config_default": "/module/templates/defaults.yaml", + "GITLAB_RUNNER_TAG_NAME": tags, + "EXCLUDE_CLEANUP_TARGETS": " ".join(cleanup_targets) + } + + needs = [] + if is_local_app_def: + # gcip library doesn't allow to create a Need object that has the same pipeline as one it runs within. + # We need to specify pipeline because generated job will be ran in child pipeline + # To work around this we temporarily change value in environment and return it after creating the Need object + real_ci_pipe_id = getenv('CI_PIPELINE_ID', '') # currect pipeline, parent of future child pipeline + environ['CI_PIPELINE_ID'] = '0000000' + needs.append(Need(job=app_reg_defs_job, pipeline=real_ci_pipe_id, artifacts=True)) + environ['CI_PIPELINE_ID'] = real_ci_pipe_id + generate_effective_set_job = job_instance(params=generate_effective_set_params, needs=needs, + vars=generate_effective_set_vars) + generate_effective_set_job.artifacts.add_paths("${CI_PROJECT_DIR}/environments/" + f"{full_env_name}") + generate_effective_set_job.artifacts.add_paths('${CI_PROJECT_DIR}/sboms') + generate_effective_set_job.artifacts.add_paths('${CI_PROJECT_DIR}/configuration/registry.y*ml') + + effective_set_expiry = effective_set_config_dict.get("effective_set_expiry") or "1 hour" + logger.info(f"effective set expiry value '{effective_set_expiry}'") + generate_effective_set_job.artifacts.expire_in = effective_set_expiry + + generate_effective_set_job.artifacts.when = WhenStatement.ALWAYS + pipeline.add_children(generate_effective_set_job) + + return generate_effective_set_job diff --git a/build_pipegene/scripts/env_build_jobs.py b/build_pipegene/scripts/env_build_jobs.py index ddae336ec..6b352afb3 100644 --- a/build_pipegene/scripts/env_build_jobs.py +++ b/build_pipegene/scripts/env_build_jobs.py @@ -53,40 +53,6 @@ def prepare_env_build_job(pipeline, is_template_test, full_env, enviroment_name, return env_build_job -def prepare_generate_effective_set_job(pipeline, environment_name, cluster_name, tags): - logger.info(f'Prepare generate_effective_set job for {cluster_name}/{environment_name}.') - generate_effective_set_params = { - "name": f'generate_effective_set.{cluster_name}/{environment_name}', - "image": '${effective_set_generator_image}', - "stage": 'generate_effective_set', - "script": ['/module/scripts/prepare.sh "generate_effective_set.yaml"', - "export env_name=$(echo $ENV_NAME | awk -F '/' '{print $NF}')", - 'env_path=$(sudo find $CI_PROJECT_DIR/environments -type d -name "$env_name")', - 'for path in $env_path; do if [ -d "$path/Credentials" ]; then sudo chmod ugo+rw $path/Credentials/*; fi; done' - ], - } - - generate_effective_set_vars = { - "CLUSTER_NAME": cluster_name, - "ENVIRONMENT_NAME": environment_name, - "INSTANCES_DIR": "${CI_PROJECT_DIR}/environments", - "effective_set_generator_image": "$effective_set_generator_image", - "envgen_args": " -vv", - "envgen_debug": "true", - "module_ansible_dir": "/module/ansible", - "module_inventory": "${CI_PROJECT_DIR}/configuration/inventory.yaml", - "module_ansible_cfg": "/module/ansible/ansible.cfg", - "module_config_default": "/module/templates/defaults.yaml", - "GITLAB_RUNNER_TAG_NAME": tags - } - generate_effective_set_job = job_instance(params=generate_effective_set_params, vars=generate_effective_set_vars) - generate_effective_set_job.artifacts.add_paths( - "${CI_PROJECT_DIR}/environments/" + f"{cluster_name}/{environment_name}") - generate_effective_set_job.artifacts.when = WhenStatement.ALWAYS - pipeline.add_children(generate_effective_set_job) - return generate_effective_set_job - - def prepare_git_commit_job(pipeline, full_env, enviroment_name, cluster_name, deployment_session_id, tags, credential_rotation_job: object = None): logger.info(f'prepare git_commit job for {full_env}.') diff --git a/build_pipegene/scripts/gitlab_ci.py b/build_pipegene/scripts/gitlab_ci.py index 7ca267798..db6f0d02d 100644 --- a/build_pipegene/scripts/gitlab_ci.py +++ b/build_pipegene/scripts/gitlab_ci.py @@ -9,10 +9,11 @@ from appregdef_render_job import prepare_appregdef_render_job from bg_manage_job import prepare_bg_manage_job from credential_rotation_job import prepare_credential_rotation_job -from env_build_jobs import prepare_env_build_job, prepare_generate_effective_set_job, prepare_git_commit_job +from env_build_jobs import prepare_env_build_job, prepare_git_commit_job from inventory_generation_job import prepare_inventory_generation_job, is_inventory_generation_needed from passport_jobs import prepare_trigger_passport_job, prepare_passport_job from process_sd_job import prepare_process_sd +from effective_set_job import prepare_generate_effective_set_job from pipeline_helper import get_gav_coordinates_from_build, find_predecessor_job from envgenehelper.collections_helper import split_multi_value_param @@ -149,8 +150,9 @@ def build_pipeline(params: dict) -> None: logger.info(f'Preparing of env_build job for {full_env_name} is skipped.') if params['GENERATE_EFFECTIVE_SET']: - jobs_map["generate_effective_set_job"] = prepare_generate_effective_set_job(pipeline, environment_name, - cluster_name, tags) + jobs_map["generate_effective_set_job"] = prepare_generate_effective_set_job(pipeline, full_env_name, + environment_name, cluster_name, + params) else: logger.info(f'Preparing of generate_effective_set job for {full_env_name} is skipped.') diff --git a/python/envgene/envgenehelper/creds_helper.py b/python/envgene/envgenehelper/creds_helper.py index d8c5c8c6c..2334a5505 100644 --- a/python/envgene/envgenehelper/creds_helper.py +++ b/python/envgene/envgenehelper/creds_helper.py @@ -1,7 +1,8 @@ import re from pathlib import Path -from envgenehelper import crypt, getenv_with_error +from envgenehelper import crypt, getenv_with_error, get_env_instances_dir, findAllYamlsInDir, openYaml, getEnvCredentialsPath +from envgenehelper.errors import ValidationError from .logger import logger @@ -219,3 +220,56 @@ def get_cred_config(): base_dir = getenv_with_error('CI_PROJECT_DIR') cred_config = crypt.decrypt_file(Path(f"{base_dir}/configuration/credentials/credentials.yml")) return cred_config + + +def validate_creds(creds_path: str = ""): + if not creds_path: + environment_name = getenv_with_error('ENVIRONMENT_NAME') + cluster_name = getenv_with_error('CLUSTER_NAME') + instances_dir = getenv_with_error('INSTANCES_DIR') + env_dir = get_env_instances_dir(environment_name, cluster_name, instances_dir) + creds_path = Path(getEnvCredentialsPath(env_dir)).parent + + credsErrors = [] + credsYamls = findAllYamlsInDir(creds_path) + logger.info(f"Starting validation of credentials") + for credListPath in credsYamls: + credListYaml = openYaml(credListPath) + for key, value in credListYaml.items() : + errorCheck = check_cred_value(key, value) + if errorCheck : + credsErrors.append(errorCheck) + if len(credsErrors) > 0: + errorMessage = "Error while validating credentials: \n" + for err in credsErrors: + errorMessage += f"\t{err}\n" + raise ValidationError(errorMessage) + + logger.info(f"Validation of credentials is completed") + + +def check_cred_value(credId, credValue) -> str: + result = "" + type = credValue["type"] + data = credValue["data"] + match type: + case _ if type == CRED_TYPE_USERPASS: + if is_envgenenullvalue(data["username"]) or is_envgenenullvalue(data["password"]): + result = f"credId: {credId} - username or password is not set" + case _ if type == CRED_TYPE_SECRET: + if is_envgenenullvalue(data["secret"]): + result = f"credId: {credId} - secret is not set" + case _ if type == CRED_TYPE_VAULT: + if is_envgenenullvalue(data["secretId"]): + result = f"credId: {credId} - secretId is not set" + case _: + result = "" + return result + + +def is_envgenenullvalue(value: object) -> bool: + if not isinstance(value, str): + return False + if value.lower() == "envgenenullvalue": + return True + return False diff --git a/scripts/utils/pipeline_parameters.py b/scripts/utils/pipeline_parameters.py index 563b70192..a6b8d0360 100644 --- a/scripts/utils/pipeline_parameters.py +++ b/scripts/utils/pipeline_parameters.py @@ -1,5 +1,7 @@ import json +import uuid from os import getenv + from envgenehelper import logger from envgenehelper.plugin_engine import PluginEngine from envgenehelper.models import TemplateVersionUpdateMode @@ -29,12 +31,14 @@ def get_pipeline_parameters() -> dict: 'NS_BUILD_FILTER': getenv("NS_BUILD_FILTER", ""), 'GITLAB_RUNNER_TAG_NAME': getenv("GITLAB_RUNNER_TAG_NAME", ""), 'RUNNER_SCRIPT_TIMEOUT': getenv("RUNNER_SCRIPT_TIMEOUT") or "10m", - 'DEPLOYMENT_SESSION_ID': getenv("DEPLOYMENT_SESSION_ID", ""), + 'DEPLOYMENT_SESSION_ID': getenv("DEPLOYMENT_SESSION_ID", "") or str(uuid.uuid4()), 'ENVGENE_LOG_LEVEL': getenv("ENVGENE_LOG_LEVEL"), "BG_STATE": getenv("BG_STATE"), "BG_MANAGE": getenv("BG_MANAGE") == "true", "APP_DEFS_PATH": getenv("APP_DEFS_PATH"), "REG_DEFS_PATH": getenv("REG_DEFS_PATH"), + "APP_REG_DEFS_JOB": getenv("APP_REG_DEFS_JOB"), + "EFFECTIVE_SET_CONFIG" : getenv("EFFECTIVE_SET_CONFIG"), "ENV_INVENTORY_CONTENT": getenv("ENV_INVENTORY_CONTENT"), "ENV_TEMPLATE_VERSION_UPDATE_MODE": getenv( "ENV_TEMPLATE_VERSION_UPDATE_MODE") or TemplateVersionUpdateMode.PERSISTENT.value, From f9d44d34b56b8572f6139697b63217c6ffb59bd8 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Thu, 12 Feb 2026 14:10:56 +0000 Subject: [PATCH 030/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index b56b1416a..fa5a04fa1 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.25.0" - DOCKER_IMAGE_TAG_ENVGENE: "1.25.0" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.25.0" + DOCKER_IMAGE_TAG_PIPEGENE: "1.26.0" + DOCKER_IMAGE_TAG_ENVGENE: "1.26.0" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.26.0" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index e4257117a..2778b1ee4 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.25.0 +version: 1.26.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 86c9a1e16..f9323f0cb 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.25.0 +version: 1.26.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index fb367c831..051e1e2da 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.25.0", + "envgene_version": "1.26.0", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From efc4c1ad54fab6a4f0539f97ca309edd9f35dae0 Mon Sep 17 00:00:00 2001 From: KamalArya <42619309+KamalArya@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:47:00 +0530 Subject: [PATCH 031/161] fix: handle multiple certs (#1000) --- base_modules/scripts/update_ca_certs.sh | 50 ------------------- build_envgene/scripts/update_ca_cert.sh | 65 +++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 55 deletions(-) delete mode 100755 base_modules/scripts/update_ca_certs.sh diff --git a/base_modules/scripts/update_ca_certs.sh b/base_modules/scripts/update_ca_certs.sh deleted file mode 100755 index e6e42d240..000000000 --- a/base_modules/scripts/update_ca_certs.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -CA_FILE="$1" - -function getLinuxDisto { - if [[ -f /etc/os-release ]]; then - # freedesktop.org and systemd - # shellcheck disable=SC1091 - . /etc/os-release - DIST=$NAME - elif type lsb_release >/dev/null 2>&1; then - # linuxbase.org - DIST=$(lsb_release -si) - elif [[ -f /etc/lsb-release ]]; then - # For some versions of Debian/Ubuntu without lsb_release command - # shellcheck disable=SC1091 - . /etc/lsb-release - DIST=$DISTRIB_ID - elif [[ -f /etc/debian_version ]]; then - # Older Debian/Ubuntu/etc. - DIST=Debian - else - # Fall back to uname, e.g. "Linux ", also works for BSD, etc. - DIST=$(uname -s) - fi - # convert to lowercase - DIST="$(tr '[:upper:]' '[:lower:]' <<<"$DIST")" -} - -function updateCertificates { - if [[ -e "${CA_FILE}" && -n "${CA_FILE}" ]]; then - getLinuxDisto - echo "Linux Distribution identified as: $DIST" - if [[ "${DIST}" == *"debian"* || "${DIST}" == *"ubuntu"* ]]; then - cp "${CA_FILE}" /usr/local/share/ca-certificates/ca.crt - update-ca-certificates --fresh >/dev/null - elif [[ "${DIST}" == *"centos"* ]]; then - cp "${CA_FILE}" /etc/pki/ca-trust/source/anchors/ca.crt - update-ca-trust - elif [[ "${DIST}" == *"alpine"* ]]; then - cat "${CA_FILE}" >>/etc/ssl/certs/ca-certificates.crt - echo "certs from $CA_FILE added to trusted root" - fi - else - echo "CA file ${CA_FILE} not found or empty" - exit 1 - fi -} - -updateCertificates diff --git a/build_envgene/scripts/update_ca_cert.sh b/build_envgene/scripts/update_ca_cert.sh index 06cbd44f7..c29a30199 100755 --- a/build_envgene/scripts/update_ca_cert.sh +++ b/build_envgene/scripts/update_ca_cert.sh @@ -1,6 +1,9 @@ #!/bin/bash CA_FILE="$1" +# Default log level to INFO if not set; normalize to uppercase for comparison +ENVGENE_LOG_LEVEL="${ENVGENE_LOG_LEVEL:-INFO}" +ENVGENE_LOG_LEVEL="$(printf '%s' "${ENVGENE_LOG_LEVEL}" | tr '[:lower:]' '[:upper:]')" function getLinuxDisto { if [[ -f /etc/os-release ]]; then @@ -25,31 +28,83 @@ function getLinuxDisto { DIST="$(tr '[:upper:]' '[:lower:]' <<< "$DIST")" } +function debugPrintCertsFromFile { + local file="$1" + local label="$2" + # Exit early unless debug is enabled + [[ "${ENVGENE_LOG_LEVEL}" != "DEBUG" ]] && return + echo "[DEBUG] === ${label} ===" + if [[ ! -e "$file" ]]; then + echo "[DEBUG] File does not exist: $file" + return + fi + local cert_num=0 + local block="" + while IFS= read -r line; do + if [[ "$line" == "-----BEGIN CERTIFICATE-----" ]]; then + block="$line" + continue + fi + if [[ -n "$block" ]]; then + block+=$'\n'"$line" + if [[ "$line" == "-----END CERTIFICATE-----" ]]; then + cert_num=$((cert_num + 1)) + echo "[DEBUG] --- Certificate #${cert_num} in ${file} ---" + echo "$block" | openssl x509 -noout -subject -issuer -dates 2>/dev/null || echo "[DEBUG] (openssl could not decode this block)" + block="" + fi + fi + done < "$file" + if [[ $cert_num -eq 0 ]]; then + echo "[DEBUG] No PEM certificate blocks found in ${file}" + else + echo "[DEBUG] Total: ${cert_num} certificate(s)" + fi + echo "[DEBUG] === End ${label} ===" +} + function updateCertificates { if [[ -e "${CA_FILE}" && ! -z "${CA_FILE}" ]]; then getLinuxDisto echo "Linux Distribution identified as: $DIST" + + # Debug: print certificates in source file BEFORE import + debugPrintCertsFromFile "${CA_FILE}" "Certificates in source file BEFORE import (${CA_FILE})" + + # Derive destination filename from source so multiple CAs do not overwrite each other; use .crt for compatibility + LOCAL_NAME="$(basename "${CA_FILE}" | sed 's/\.[^.]*$//').crt" if [[ "${DIST}" == *"debian"* || "${DIST}" == *"ubuntu"* ]]; then - cp "${CA_FILE}" /usr/local/share/ca-certificates/ + cp "${CA_FILE}" "/usr/local/share/ca-certificates/${LOCAL_NAME}" update-ca-certificates --fresh echo "certs from ${CA_FILE} added to trusted root" export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt #https://ubuntu.com/server/docs/install-a-root-ca-certificate-in-the-trust-store elif [[ "${DIST}" == *"centos"* ]]; then - cp "${CA_FILE}" /etc/pki/ca-trust/source/anchors/ca.crt + cp "${CA_FILE}" "/etc/pki/ca-trust/source/anchors/${LOCAL_NAME}" update-ca-trust echo "certs from ${CA_FILE} added to trusted root" export REQUESTS_CA_BUNDLE=/etc/pki/tls/certs/ca-bundle.crt #https://techjourney.net/update-add-ca-certificates-bundle-in-redhat-centos/ elif [[ "${DIST}" == *"alpine"* ]]; then - cat "${CA_FILE}" >> /etc/ssl/certs/ca-certificates.crt + cp "${CA_FILE}" "/usr/local/share/ca-certificates/${LOCAL_NAME}" + update-ca-certificates echo "certs from ${CA_FILE} added to trusted root" - export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt #we copy the certs to this file in line 43 + export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt elif [[ "${DIST}" == *"red hat"* ]]; then mkdir -p /etc/pki/ca-trust/source/anchors - cp "${CA_FILE}" /etc/pki/ca-trust/source/anchors/ + cp "${CA_FILE}" "/etc/pki/ca-trust/source/anchors/${LOCAL_NAME}" update-ca-trust echo "certs from ${CA_FILE} added to trusted root" export REQUESTS_CA_BUNDLE=/etc/pki/tls/certs/ca-bundle.crt #https://www.redhat.com/en/blog/configure-ca-trust-list fi + + # Debug: print certificates AFTER import (from the installed location / bundle) + if [[ "${DIST}" == *"debian"* || "${DIST}" == *"ubuntu"* ]]; then + debugPrintCertsFromFile "/usr/local/share/ca-certificates/${LOCAL_NAME}" "Certificates AFTER import (installed file /usr/local/share/ca-certificates/${LOCAL_NAME})" + elif [[ "${DIST}" == *"centos"* || "${DIST}" == *"red hat"* ]]; then + debugPrintCertsFromFile "/etc/pki/ca-trust/source/anchors/${LOCAL_NAME}" "Certificates AFTER import (installed file /etc/pki/ca-trust/source/anchors/${LOCAL_NAME})" + elif [[ "${DIST}" == *"alpine"* ]]; then + debugPrintCertsFromFile "/usr/local/share/ca-certificates/${LOCAL_NAME}" "Certificates AFTER import (installed file /usr/local/share/ca-certificates/${LOCAL_NAME})" + fi + [[ "${ENVGENE_LOG_LEVEL}" == "DEBUG" ]] && echo "[DEBUG] Certificate import completed successfully for ${CA_FILE}" else echo "CA file ${CA_FILE} not found or empty" exit 1 From fac4723276edb882f031c178ad883bf30b77d5d2 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Fri, 13 Feb 2026 05:18:59 +0000 Subject: [PATCH 032/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index fa5a04fa1..0ed040f66 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.26.0" - DOCKER_IMAGE_TAG_ENVGENE: "1.26.0" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.26.0" + DOCKER_IMAGE_TAG_PIPEGENE: "1.26.1" + DOCKER_IMAGE_TAG_ENVGENE: "1.26.1" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.26.1" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 2778b1ee4..af500fb5f 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.26.0 +version: 1.26.1 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index f9323f0cb..a6382bd20 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.26.0 +version: 1.26.1 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 051e1e2da..a955aadb4 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.26.0", + "envgene_version": "1.26.1", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 3bc11af43f3a29569545dc82d3288454322d09db Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:21:09 +0300 Subject: [PATCH 033/161] feat: Added Effective Set job to Instance github pipeline (#1016) --- .../scripts/handle_effective_set_config.py | 2 +- .../.github/workflows/Envgene.yml | 43 +++++++++++++------ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/build_effective_set_generator/scripts/handle_effective_set_config.py b/build_effective_set_generator/scripts/handle_effective_set_config.py index 0c6536c37..096ff2608 100644 --- a/build_effective_set_generator/scripts/handle_effective_set_config.py +++ b/build_effective_set_generator/scripts/handle_effective_set_config.py @@ -106,4 +106,4 @@ def handle_effective_set_config(config_str): json.dump(result_args, f) except Exception as e: logger.error(f"Error: {e}") - exit(1) + exit(1) \ No newline at end of file diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 0ed040f66..1d28f58ab 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -109,6 +109,7 @@ jobs: echo "DEPLOYMENT_TICKET_ID=${{ github.event.inputs.DEPLOYMENT_TICKET_ID }}" >> $GITHUB_ENV echo "ENV_NAMES=${{ github.event.inputs.ENV_NAMES }}" >> $GITHUB_ENV echo "ENV_BUILDER=${{ github.event.inputs.ENV_BUILDER }}" >> $GITHUB_ENV + echo "GENERATE_EFFECTIVE_SET=${{ github.event.inputs.GENERATE_EFFECTIVE_SET }}" >> $GITHUB_ENV echo "GET_PASSPORT=${{ github.event.inputs.GET_PASSPORT }}" >> $GITHUB_ENV echo "CMDB_IMPORT=${{ github.event.inputs.CMDB_IMPORT }}" >> $GITHUB_ENV echo "ENV_TEMPLATE_VERSION=${{ github.event.inputs.ENV_TEMPLATE_VERSION }}" >> $GITHUB_ENV @@ -360,21 +361,34 @@ jobs: docker run --rm -v ${{ github.workspace }}:${{ env.CI_PROJECT_DIR }} -w ${{ env.CI_PROJECT_DIR }} --env-file .env.container --user root -e HOME=/root \ ${{ env.DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR }}:${{ env.DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR }} \ bash -c " - # Export all variables using unified exporter + echo 'Executing effective set generation...' + source /module/venv/bin/activate /module/scripts/handle_certs.sh - - echo \"Executing effective set generation...\" - if ! /module/scripts/prepare.sh \"generate_effective_set.yaml\"; then - echo \"Effective set generation failed with exit code \$?\" + mkdir -p \"\${CI_PROJECT_DIR}/configuration/certs\" + [ -f /default_cert.pem ] && cp /default_cert.pem \"\${CI_PROJECT_DIR}/configuration/certs/default_cert.pem\" || true + for cert in \"\${CI_PROJECT_DIR}/configuration/certs/\"*; do [ -f \"\$cert\" ] && keytool -import -trustcacerts -alias \"\$(basename \"\$cert\")\" -file \"\$cert\" -keystore /etc/ssl/certs/keystore.jks -storepass changeit -noprompt; done + python3 /module/scripts/main.py decrypt_cred_files + base_env_path=\"\${CI_PROJECT_DIR}/environments/${{ matrix.environment }}\" + app_defs_path=\"\${base_env_path}/AppDefs\" + reg_defs_path=\"\${base_env_path}/RegDefs\" + if [ -n \"\$APP_REG_DEFS_JOB\" ] && [ -n \"\$APP_DEFS_PATH\" ]; then mkdir -p \"\$app_defs_path\" && cp -rf \"\$APP_DEFS_PATH\"/* \"\$app_defs_path\"; fi + if [ -n \"\$APP_REG_DEFS_JOB\" ] && [ -n \"\$REG_DEFS_PATH\" ]; then mkdir -p \"\$reg_defs_path\" && cp -fr \"\$REG_DEFS_PATH\"/* \"\$reg_defs_path\"; fi + python3 /module/scripts/main.py validate_creds + java_cmd=\"/deployments/run-java.sh --env-id=${{ matrix.environment }} --envs-path=\${CI_PROJECT_DIR}/environments --output=\${CI_PROJECT_DIR}/environments/${{ matrix.environment }}/effective-set --registries=\${CI_PROJECT_DIR}/configuration/registry.yml --sboms-path=\${CI_PROJECT_DIR}/sboms --sd-path=\${CI_PROJECT_DIR}/environments/${{ matrix.environment }}/Inventory/solution-descriptor/sd.yaml\" + if [ -n \"\$EFFECTIVE_SET_CONFIG\" ]; then + python3 /module/scripts/handle_effective_set_config.py --effective-set-config \"\$EFFECTIVE_SET_CONFIG\" + extra_args=\$(jq -r '.extra_args // [] | join(\" \")' /tmp/effective_set_output.json) + java_cmd=\"\$java_cmd \$extra_args\" + fi + deploy_id=\"\${DEPLOYMENT_SESSION_ID:-\$DEPLOYMENT_TICKET_ID}\" + if [ -n \"\$deploy_id\" ]; then java_cmd=\"\$java_cmd --extra_params=DEPLOYMENT_SESSION_ID=\$deploy_id\"; fi + if ! eval \"\$java_cmd\"; then + echo 'Effective set generation failed with exit code '\$? exit 1 fi - - env_path=\$(find \"\${CI_PROJECT_DIR}/environments\" -type d -name \"\$env_name\") - for path in \$env_path; do - if [ -d \"\$path/Credentials\" ]; then - chmod ugo+rw \"\$path/Credentials/\"* - fi - done + python3 /module/scripts/main.py encrypt_cred_files + env_path=\"\${CI_PROJECT_DIR}/environments/${{ matrix.environment }}\" + if [ -d \"\$env_path/Credentials\" ]; then chmod -R ugo+rw \"\$env_path/Credentials\"; fi " - name: GENERATE_EFFECTIVE_SET - Upload Generate Effective Set Package @@ -382,7 +396,10 @@ jobs: if: needs.process_environment_variables.outputs.GENERATE_EFFECTIVE_SET == 'true' with: name: generate_effective_set_${{ env.PACKAGE_NAME }} - path: environments/${{ matrix.environment }} + path: | + environments/${{ matrix.environment }} + sboms + configuration include-hidden-files: true ########################## From c19584b9f059e78fdcdf5a22cf76884ed296b16a Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Mon, 16 Feb 2026 07:32:51 +0000 Subject: [PATCH 034/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 1d28f58ab..65f7350be 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.26.1" - DOCKER_IMAGE_TAG_ENVGENE: "1.26.1" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.26.1" + DOCKER_IMAGE_TAG_PIPEGENE: "1.26.2" + DOCKER_IMAGE_TAG_ENVGENE: "1.26.2" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.26.2" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index af500fb5f..f8a67c12b 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.26.1 +version: 1.26.2 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index a6382bd20..c3141de55 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.26.1 +version: 1.26.2 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index a955aadb4..f0dad90f7 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.26.1", + "envgene_version": "1.26.2", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From cdb574fdd8fd2dedd62dbc7a82952e778f702b6b Mon Sep 17 00:00:00 2001 From: dysmon <120464230+dysmon@users.noreply.github.com> Date: Tue, 17 Feb 2026 19:34:19 +0500 Subject: [PATCH 035/161] fix: adds conditional check to avoid cert issues in effective set (#1030) --- build_pipegene/scripts/effective_set_job.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build_pipegene/scripts/effective_set_job.py b/build_pipegene/scripts/effective_set_job.py index d66199c86..b9f7e7e00 100644 --- a/build_pipegene/scripts/effective_set_job.py +++ b/build_pipegene/scripts/effective_set_job.py @@ -35,7 +35,8 @@ def prepare_generate_effective_set_job(pipeline, full_env_name, env_name, cluste script = [ '/module/scripts/handle_certs.sh', # cert handling for java - 'mkdir -p ${CI_PROJECT_DIR}/configuration/certs/ && cp /default_cert.pem "${CI_PROJECT_DIR}/configuration/certs/default_cert.pem"', + 'mkdir -p ${CI_PROJECT_DIR}/configuration/certs/', + 'if [ -f /default_cert.pem ]; then cp /default_cert.pem "${CI_PROJECT_DIR}/configuration/certs/"; fi', 'for cert in "${CI_PROJECT_DIR}/configuration/certs/*" ; do [ -f "$cert" ] && keytool -import -trustcacerts -alias "$(basename "$cert")" -file "$cert" -keystore /etc/ssl/certs/keystore.jks -storepass changeit -noprompt; done', 'python3 /module/scripts/main.py decrypt_cred_files', f'[ -n "$APP_REG_DEFS_JOB" ] && [ -n "$APP_DEFS_PATH" ] && mkdir -p {app_defs_path} && cp -rf {artifact_app_defs_path}/* {app_defs_path}', From c39732aed8d91962c059fc3938e04a45d1a105a1 Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:23:02 +0300 Subject: [PATCH 036/161] feat: Added alpine for stage2 (#1039) --- github_workflows/instance-repo-pipeline/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github_workflows/instance-repo-pipeline/Dockerfile b/github_workflows/instance-repo-pipeline/Dockerfile index 0f5e643c8..a749e2730 100644 --- a/github_workflows/instance-repo-pipeline/Dockerfile +++ b/github_workflows/instance-repo-pipeline/Dockerfile @@ -7,7 +7,7 @@ COPY github_workflows/instance-repo-pipeline/.github /opt/github RUN find /opt/github/scripts -type f -name "*.sh" -exec chmod +x {} \; #-----------Stage 2----------- -FROM scratch +FROM alpine:3.19 COPY --from=initial /opt/github /opt/github USER 1000 CMD ["sh", "-c", "sleep infinity"] \ No newline at end of file From 80aba48950dbb3724b8377b0e2041776f479b54f Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Fri, 20 Feb 2026 08:32:44 +0000 Subject: [PATCH 037/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 65f7350be..2f1b60064 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.26.2" - DOCKER_IMAGE_TAG_ENVGENE: "1.26.2" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.26.2" + DOCKER_IMAGE_TAG_PIPEGENE: "1.26.3" + DOCKER_IMAGE_TAG_ENVGENE: "1.26.3" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.26.3" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index f8a67c12b..703258d8d 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.26.2 +version: 1.26.3 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index c3141de55..72fcaf5fe 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.26.2 +version: 1.26.3 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index f0dad90f7..8f56ff950 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.26.2", + "envgene_version": "1.26.3", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 79b71a66fbb99c94afd5838ad8ac6c983c073afe Mon Sep 17 00:00:00 2001 From: miyamuraga <198181742+miyamuraga@users.noreply.github.com> Date: Fri, 20 Feb 2026 18:02:39 +0500 Subject: [PATCH 038/161] feat: support namespace filtering (#1032) --- python/envgene/envgenehelper/yaml_helper.py | 18 +++++---- scripts/build_env/render_config_env.py | 40 ++++++++++++++----- .../env04/Inventory/env_definition.yml | 5 ++- .../env_templates/test-template-2.yaml | 4 +- .../env_templates/test-template-2.yaml.j2 | 11 +++++ 5 files changed, 58 insertions(+), 20 deletions(-) create mode 100644 test_data/test_templates/env_templates/test-template-2.yaml.j2 diff --git a/python/envgene/envgenehelper/yaml_helper.py b/python/envgene/envgenehelper/yaml_helper.py index 029dcef7e..b49db0a6c 100644 --- a/python/envgene/envgenehelper/yaml_helper.py +++ b/python/envgene/envgenehelper/yaml_helper.py @@ -8,13 +8,13 @@ import jschon_tools import jsonschema import ruyaml +from jsonschema import RefResolver from ruyaml import CommentedMap, CommentedSeq from ruyaml.scalarstring import DoubleQuotedScalarString, LiteralScalarString from .file_helper import * from .json_helper import openJson from .logger import logger -from jsonschema import RefResolver def create_yaml_processor(is_safe=False) -> ruyaml.main.YAML: @@ -433,13 +433,15 @@ def convert_ordereddict_to_dict(obj): return obj -def find_all_yaml_files_by_stem(path: str): - file_paths = [] - for ext in ["yaml", "yml"]: - file_path = Path(f"{path}.{ext}") - if file_path.exists(): - file_paths.append(file_path) - return file_paths +def find_files_by_basename(path: str, extensions_priority: tuple[str] = ("yml", "yaml")) -> list[Path]: + base_path = Path(path) + found_files: list[Path] = [] + + for ext in extensions_priority: + candidate = base_path.with_suffix(f".{ext}") + if candidate.exists(): + found_files.append(candidate) + return found_files jschon.create_catalog('2020-12') diff --git a/scripts/build_env/render_config_env.py b/scripts/build_env/render_config_env.py index 6e8c02e79..cd0a36bb2 100644 --- a/scripts/build_env/render_config_env.py +++ b/scripts/build_env/render_config_env.py @@ -14,6 +14,7 @@ from jinja.replace_ansible_stuff import replace_ansible_stuff, escaping_quotation SCHEMAS_DIR = Path(__file__).resolve().parents[2] / "schemas" +TD_SCHEMA = str(SCHEMAS_DIR / "template-descriptor.schema.json") yml = create_yaml_processor() @@ -47,6 +48,7 @@ class Context(BaseModel): render_profiles_dir: Optional[str] = '' start_time: datetime | None = Field(default=None, exclude=True) + end_time: datetime | None = Field(default=None, exclude=True) class Config: extra = "allow" @@ -67,7 +69,11 @@ def use(self): try: yield self finally: - logger.debug(f"Final state: {self.dict(exclude_none=True)}") + self.end_time = datetime.now() + logger.debug( + f"Exit context at {self.end_time}. Duration: {self.end_time - self.start_time}. " + f"Final state: {self.dict(exclude_none=True)}" + ) def as_dict(self, include_none: bool = False) -> dict: return self.model_dump(exclude_none=not include_none) @@ -105,12 +111,28 @@ def generate_config(self): self.ctx.config = config def set_env_template(self): - env_template_path_stem = f'{self.ctx.templates_dir}/env_templates/{self.ctx.current_env["env_template"]}' - env_template_path = next(iter(find_all_yaml_files_by_stem(env_template_path_stem)), None) + extensions = ("yml.j2", "yaml.j2", "yml", "yaml") + env_templates_dir = Path(f'{self.ctx.templates_dir}/env_templates') + env_template_basename = env_templates_dir / self.ctx.current_env["env_template"] + suitable_files = find_files_by_basename(env_template_basename, extensions) + env_template_path = suitable_files[0] if suitable_files else None if not env_template_path: - raise ValueError(f'Template descriptor was not found in {env_template_path_stem}') - - env_template = openYaml(filePath=env_template_path, safe_load=True) + all_files = [f for f in env_templates_dir.iterdir() if f.is_file()] + remains_files = list(set(all_files) - set(suitable_files)) + raise ValueError( + f"Template descriptor not found: {env_template_basename}." + f" Expected location in template repository: {env_template_path}." + f" Allowed extensions: 'yml', 'yaml', 'yml.j2', 'yaml.j2'." + f" Found templates: {remains_files}") + + env_tmpl_final_path = env_template_path + if env_template_path.suffix.endswith("j2"): + logger.info(f"Template descriptor is {env_template_path}, rendering required") + env_tmpl_final_path = str(env_template_path).removesuffix(".j2") + self.render_from_file_to_file(env_template_path, env_tmpl_final_path) + + validate_yaml_by_scheme_or_fail(env_tmpl_final_path, TD_SCHEMA) + env_template = openYaml(filePath=env_tmpl_final_path, safe_load=True) logger.info(f"env_template = {env_template}") self.ctx.current_env_template = env_template @@ -185,11 +207,11 @@ def generate_ns_postfix(self, ns, ns_template_path, override_template_ns_name: s elif ns_name == peer_name: ns_template_name += "-peer" logger.debug(f'After appending the ns name : {ns_template_name}') - return ns_template_name + return ns_template_name def generate_solution_structure(self): - sd_path_stem = f'{self.ctx.current_env_dir}/Inventory/solution-descriptor/sd' - sd_path = next(iter(find_all_yaml_files_by_stem(sd_path_stem)), None) + sd_basename = f'{self.ctx.current_env_dir}/Inventory/solution-descriptor/sd' + sd_path = next(iter(find_files_by_basename(sd_basename)), None) solution_structure = {} if sd_path: self.ctx.sd_file_path = str(sd_path) diff --git a/test_data/test_environments/cluster01/env04/Inventory/env_definition.yml b/test_data/test_environments/cluster01/env04/Inventory/env_definition.yml index ee80de1bd..3c31100c5 100644 --- a/test_data/test_environments/cluster01/env04/Inventory/env_definition.yml +++ b/test_data/test_environments/cluster01/env04/Inventory/env_definition.yml @@ -6,7 +6,10 @@ inventory: cloudPassport: "" envTemplate: name: "test-template-2" - additionalTemplateVariables: {} + additionalTemplateVariables: + namespaces: + app: + enabled: true templateArtifact: registry: "artifactory" repository: "snapshotRepository" diff --git a/test_data/test_templates/env_templates/test-template-2.yaml b/test_data/test_templates/env_templates/test-template-2.yaml index bda1f5ca0..e143c4da1 100644 --- a/test_data/test_templates/env_templates/test-template-2.yaml +++ b/test_data/test_templates/env_templates/test-template-2.yaml @@ -3,5 +3,5 @@ tenant: "{{ templates_dir }}/env_templates/test-template-2/tenant.yml.j2" cloud: template_path: "{{ templates_dir }}/env_templates/test-template-2/cloud.yml.j2" namespaces: - - template_path: "{{ templates_dir }}/env_templates/test-template-2/Namespaces/app.yml.j2" - deploy_postfix: "app" + - template_path: "{{ templates_dir }}/env_templates/test-template-2/Namespaces/app-2.yml.j2" + deploy_postfix: "app-2" diff --git a/test_data/test_templates/env_templates/test-template-2.yaml.j2 b/test_data/test_templates/env_templates/test-template-2.yaml.j2 new file mode 100644 index 000000000..494b9bb7a --- /dev/null +++ b/test_data/test_templates/env_templates/test-template-2.yaml.j2 @@ -0,0 +1,11 @@ +--- +tenant: {{ templates_dir }}/env_templates/test-template-2/tenant.yml.j2 +cloud: + template_path: {{ templates_dir }}/env_templates/test-template-2/cloud.yml.j2 +namespaces: +{% if current_env.additionalTemplateVariables.namespaces["app"].enabled | default(False, True) %} + - template_path: {{ templates_dir }}/env_templates/test-template-2/Namespaces/app.yml.j2 + deploy_postfix: "app" +{% else %} + [] +{% endif %} From ce5af4585a715abc5a30fe0de60081bd30569380 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Fri, 20 Feb 2026 13:15:55 +0000 Subject: [PATCH 039/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 2f1b60064..8039c1c3c 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.26.3" - DOCKER_IMAGE_TAG_ENVGENE: "1.26.3" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.26.3" + DOCKER_IMAGE_TAG_PIPEGENE: "1.27.0" + DOCKER_IMAGE_TAG_ENVGENE: "1.27.0" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.27.0" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 703258d8d..158fe5afe 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.26.3 +version: 1.27.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 72fcaf5fe..c6ec3c1d4 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.26.3 +version: 1.27.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 8f56ff950..c91578ec7 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.26.3", + "envgene_version": "1.27.0", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 2960a0a491de1bdf53cb03e9a32f5e264341c352 Mon Sep 17 00:00:00 2001 From: popoveugene <42543333+popoveugene@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:26:21 +0300 Subject: [PATCH 040/161] docs: add CALCULATOR_CLI_JAVA_OPTIONS description (#1046) --- docs/envgene-repository-variables.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/envgene-repository-variables.md b/docs/envgene-repository-variables.md index c8d84579d..2e3506e0c 100644 --- a/docs/envgene-repository-variables.md +++ b/docs/envgene-repository-variables.md @@ -13,6 +13,7 @@ - [`GH_RUNNER_TAG_NAME`](#gh_runner_tag_name) - [`RUNNER_SCRIPT_TIMEOUT`](#runner_script_timeout) - [`GH_RUNNER_SCRIPT_TIMEOUT`](#gh_runner_script_timeout) + - [`CALCULATOR_CLI_JAVA_OPTIONS`](#calculator_cli_java_options) - [`DOCKER_REGISTRY` (in instance repository)](#docker_registry-in-instance-repository) - [Template EnvGene Repository](#template-envgene-repository) - [`ENV_TEMPLATE_TEST`](#env_template_test) @@ -141,6 +142,20 @@ This parameter is only available in the GitHub version of the pipeline. For more **Example**: `15` +### `CALCULATOR_CLI_JAVA_OPTIONS` + +**Description**: Java options passed to the Calculator CLI to override default settings. Used to control heap size and ForkJoinPool thread count (number of applications processed in parallel during effective set generation). + +**Default Value**: None + +**Mandatory**: No + +**Example**: + +```text +CALCULATOR_CLI_JAVA_OPTIONS="-Djava.util.concurrent.ForkJoinPool.common.parallelism=4 -Xmx2g -Xms2g" +``` + ### `DOCKER_REGISTRY` (in instance repository) **Description**: Specifies the registry where the EnvGene Docker images are located From a077ad348a5813d1057159d6efc0d67233608310 Mon Sep 17 00:00:00 2001 From: Tesma Jose <113982972+tesmarishy@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:04:16 +0530 Subject: [PATCH 041/161] fix: CALCULATOR_CLI_JAVA_OPTIONS in calclator cli (#1033) --- .../build/Dockerfile | 1 + build_pipegene/scripts/effective_set_job.py | 2 +- scripts/utils/entrypoint.sh | 33 +++++++++++++++++++ scripts/utils/pipeline_parameters.py | 1 + 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100755 scripts/utils/entrypoint.sh diff --git a/build_effective_set_generator/build/Dockerfile b/build_effective_set_generator/build/Dockerfile index 93117d9f9..a09c7ddc3 100644 --- a/build_effective_set_generator/build/Dockerfile +++ b/build_effective_set_generator/build/Dockerfile @@ -93,6 +93,7 @@ RUN addgroup ci && \ adduser -D -h /module/ -s /bin/bash -G ci ci && \ chown ci:ci -R /module /deployments && \ chmod +x /module/scripts/*.sh && \ + chmod +x /module/scripts/utils/entrypoint.sh && \ chmod 644 /module/scripts/*.py 2>/dev/null || true && \ chmod +x /usr/local/bin/sops && \ chmod 540 /deployments/run-java.sh && \ diff --git a/build_pipegene/scripts/effective_set_job.py b/build_pipegene/scripts/effective_set_job.py index b9f7e7e00..b3f9074a1 100644 --- a/build_pipegene/scripts/effective_set_job.py +++ b/build_pipegene/scripts/effective_set_job.py @@ -45,7 +45,7 @@ def prepare_generate_effective_set_job(pipeline, full_env_name, env_name, cluste ] cmdb_cli_cmd_call = [ - f"/deployments/run-java.sh --env-id={full_env_name}", + f"/module/scripts/utils/entrypoint.sh --env-id={full_env_name}", "--envs-path=$CI_PROJECT_DIR/environments", f"--output=$CI_PROJECT_DIR/environments/{full_env_name}/effective-set" ] diff --git a/scripts/utils/entrypoint.sh b/scripts/utils/entrypoint.sh new file mode 100755 index 000000000..05efa517f --- /dev/null +++ b/scripts/utils/entrypoint.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +merge_java_opts() { + base="$1" + override="$2" + + result="$base" + allowed_override="" + + for opt in $override; do + case "$opt" in + -Xms*) + result=$(echo "$result" | sed -E 's/-Xms[^ ]+//g') + allowed_override="$opt${allowed_override:+ $allowed_override}" + ;; + -Xmx*) + result=$(echo "$result" | sed -E 's/-Xmx[^ ]+//g') + allowed_override="$opt${allowed_override:+ $allowed_override}" + ;; + -Djava.util.concurrent.ForkJoinPool.common.parallelism=*) + result=$(echo "$result" | sed -E 's|-Djava.util.concurrent.ForkJoinPool.common.parallelism=[^ ]+||g') + allowed_override="$opt${allowed_override:+ $allowed_override}" + ;; + esac + done + echo "$result${allowed_override:+ $allowed_override}" +} + +if [ -n "${CALCULATOR_CLI_JAVA_OPTIONS:-}" ]; then + JAVA_OPTIONS=$(merge_java_opts "$JAVA_OPTIONS" "$CALCULATOR_CLI_JAVA_OPTIONS" | tr -s '[:space:]') +fi +export JAVA_OPTIONS +exec /deployments/run-java.sh "$@" \ No newline at end of file diff --git a/scripts/utils/pipeline_parameters.py b/scripts/utils/pipeline_parameters.py index a6b8d0360..c5092c524 100644 --- a/scripts/utils/pipeline_parameters.py +++ b/scripts/utils/pipeline_parameters.py @@ -33,6 +33,7 @@ def get_pipeline_parameters() -> dict: 'RUNNER_SCRIPT_TIMEOUT': getenv("RUNNER_SCRIPT_TIMEOUT") or "10m", 'DEPLOYMENT_SESSION_ID': getenv("DEPLOYMENT_SESSION_ID", "") or str(uuid.uuid4()), 'ENVGENE_LOG_LEVEL': getenv("ENVGENE_LOG_LEVEL"), + 'CALCULATOR_CLI_JAVA_OPTIONS' : getenv("CALCULATOR_CLI_JAVA_OPTIONS", ""), "BG_STATE": getenv("BG_STATE"), "BG_MANAGE": getenv("BG_MANAGE") == "true", "APP_DEFS_PATH": getenv("APP_DEFS_PATH"), From 3cf6620d3464ad25fb6fea043e38bd81f27daec1 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Wed, 25 Feb 2026 11:36:18 +0000 Subject: [PATCH 042/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 8039c1c3c..bb1955775 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.27.0" - DOCKER_IMAGE_TAG_ENVGENE: "1.27.0" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.27.0" + DOCKER_IMAGE_TAG_PIPEGENE: "1.27.1" + DOCKER_IMAGE_TAG_ENVGENE: "1.27.1" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.27.1" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 158fe5afe..287b3ee82 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.27.0 +version: 1.27.1 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index c6ec3c1d4..f838e68bb 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.27.0 +version: 1.27.1 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index c91578ec7..960b188ea 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.27.0", + "envgene_version": "1.27.1", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 8ef680b0e31facb28bfaff546dcdd44ad1cfc03f Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:41:45 +0300 Subject: [PATCH 043/161] feat: GSF package. Added logic to keep the pipeline_vars.yaml --- .../git-system-follower-package/scripts/init.py | 3 ++- .../git-system-follower-package/scripts/init.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/scripts/init.py b/gsf_packages/envgene_discovery_project/git-system-follower-package/scripts/init.py index e0625c116..f747891b4 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/scripts/init.py +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/scripts/init.py @@ -3,7 +3,8 @@ from git_system_follower.develop.api.templates import create_template, get_template_names # Protected files that should never be deleted -PROTECTED_FILES = {'history.log', '.gitlab-ci.yml', '.gitignore'} +# pipeline_vars: preserve if exists in repo (user customizations) +PROTECTED_FILES = {'history.log', '.gitlab-ci.yml', '.gitignore', 'gitlab-ci/pipeline_vars.yaml', 'gitlab-ci/pipeline_vars.yml'} def _delete_files_from_history(parameters: Parameters): diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/init.py b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/init.py index e0625c116..f747891b4 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/init.py +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/init.py @@ -3,7 +3,8 @@ from git_system_follower.develop.api.templates import create_template, get_template_names # Protected files that should never be deleted -PROTECTED_FILES = {'history.log', '.gitlab-ci.yml', '.gitignore'} +# pipeline_vars: preserve if exists in repo (user customizations) +PROTECTED_FILES = {'history.log', '.gitlab-ci.yml', '.gitignore', 'gitlab-ci/pipeline_vars.yaml', 'gitlab-ci/pipeline_vars.yml'} def _delete_files_from_history(parameters: Parameters): From 556bbb2433b55e191cadbc0835e3c2df872b41bd Mon Sep 17 00:00:00 2001 From: dysmon <120464230+dysmon@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:51:42 +0500 Subject: [PATCH 044/161] feat: refactor builds and generated pipeline (#1026) --- .../build/Dockerfile | 11 +++++--- build_envgene/build/Dockerfile | 26 +++---------------- .../scripts/appregdef_render_job.py | 8 +++--- .../scripts/credential_rotation_job.py | 1 - build_pipegene/scripts/effective_set_job.py | 3 +-- build_pipegene/scripts/env_build_jobs.py | 4 +-- .../scripts/inventory_generation_job.py | 1 - build_pipegene/scripts/passport_jobs.py | 12 ++++----- build_pipegene/scripts/pipeline_helper.py | 3 ++- build_pipegene/scripts/process_sd_job.py | 8 +++--- .../.github/workflows/Envgene.yml | 10 +++---- scripts/build_env/process_sd.py | 2 +- .../scripts => scripts/utils}/handle_certs.sh | 4 +-- .../utils}/update_ca_cert.sh | 0 14 files changed, 35 insertions(+), 58 deletions(-) rename {build_envgene/scripts => scripts/utils}/handle_certs.sh (86%) rename {build_envgene/scripts => scripts/utils}/update_ca_cert.sh (100%) diff --git a/build_effective_set_generator/build/Dockerfile b/build_effective_set_generator/build/Dockerfile index a09c7ddc3..84b7c127d 100644 --- a/build_effective_set_generator/build/Dockerfile +++ b/build_effective_set_generator/build/Dockerfile @@ -1,3 +1,6 @@ +######################################### +# Stage 1: Build +# Multi-stage build to reduce final image size FROM python:3.12-alpine3.19 AS build ARG RUN_JAVA_VERSION=1.3.8 @@ -35,6 +38,7 @@ RUN /module/venv/bin/pip install --no-cache-dir \ /python/envgene \ /python/artifact-searcher +# Download and install SOPS for secrets management RUN curl -fsSL \ https://github.com/mozilla/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.linux.amd64 \ -o /usr/local/bin/sops && \ @@ -47,6 +51,9 @@ RUN mkdir -p /deployments && \ COPY build_effective_set_generator/effective-set-generator/target/*.jar /deployments/app.jar +######################################### +# Stage 2: Runtime +# Lightweight runtime image with only essential dependencies FROM python:3.12-alpine3.19 AS runtime ARG JAVA_PACKAGE=openjdk17-jre-headless @@ -81,13 +88,9 @@ COPY --from=build /usr/local/bin/sops /usr/local/bin/sops COPY --from=build /deployments /deployments COPY scripts/build_env/ /build_env/scripts/build_env/ -COPY scripts/build_env/process_sd.py /module/scripts/ COPY build_effective_set_generator/scripts/ /module/scripts/ COPY scripts/utils/ /module/scripts/utils/ -# cert script should be moved to scripts/utils/ -COPY build_envgene/scripts/handle_certs.sh /module/scripts/ - # User creation + permissions RUN addgroup ci && \ adduser -D -h /module/ -s /bin/bash -G ci ci && \ diff --git a/build_envgene/build/Dockerfile b/build_envgene/build/Dockerfile index 3e6390759..88ca831c0 100644 --- a/build_envgene/build/Dockerfile +++ b/build_envgene/build/Dockerfile @@ -1,8 +1,5 @@ # checkov:skip=CKV_DOCKER_3:This build container requires root privileges for CI/CD operations -######################################### -# Stage 1: Build -# Multi-stage build to reduce final image size FROM python:3.12-alpine3.19 AS build # Install build dependencies @@ -24,13 +21,11 @@ RUN apk add --no-cache \ zip \ unzip -# Copy configuration files COPY build_envgene/build/pip.conf /etc/pip.conf COPY build_envgene/build/requirements.txt /build/requirements.txt COPY build_envgene/build/constraint.txt /build/constraint.txt COPY creds_rotation/build/requirements.txt /build/creds_rotation_requirements.txt -# Copy source code COPY python /python COPY build_envgene/scripts /module/scripts COPY scripts/bg_manage /scripts/bg_manage @@ -42,9 +37,9 @@ COPY scripts/cloud_passport/ /cloud_passport/scripts/ COPY schemas /build_env/schemas COPY scripts/utils /module/scripts/utils -# Create virtual environment and install Python packages -RUN python -m venv /module/venv -RUN /module/venv/bin/pip install --upgrade pip "setuptools<82" wheel +RUN python3 -m venv /module/venv && \ + /module/venv/bin/pip install --upgrade pip "setuptools<82" wheel + RUN /module/venv/bin/pip install --no-cache-dir --retries 10 --timeout 60 -r /build/requirements.txt RUN /module/venv/bin/pip install /python/jschon-sort && \ @@ -53,7 +48,6 @@ RUN /module/venv/bin/pip install /python/jschon-sort && \ /module/venv/bin/pip install /python/artifact-searcher && \ /module/venv/bin/pip install --no-cache-dir --no-deps -r /build/creds_rotation_requirements.txt -# Download and install SOPS for secrets management RUN wget --quiet --tries=3 \ https://github.com/mozilla/sops/releases/download/v3.9.0/sops-v3.9.0.linux.amd64 \ -O /usr/local/bin/sops && \ @@ -71,13 +65,10 @@ RUN rm -rf /module/venv/lib/python3.12/site-packages/pytest* \ /module/venv/lib/python3.12/site-packages/_pytest* 2>/dev/null || true RUN /module/venv/bin/pip cache purge -# Set permissions RUN chmod 754 /module/scripts/* && \ chmod 754 /module/creds_rotation_scripts/* -######################################### -# Stage 2: Runtime -# Lightweight runtime image with only essential dependencies + FROM python:3.12-alpine3.19 AS runtime # Install only essential runtime dependencies @@ -98,7 +89,6 @@ RUN apk add --no-cache \ unzip \ tar -# Copy everything from build stage COPY --from=build /module /module COPY --from=build /scripts/bg_manage /scripts/bg_manage COPY --from=build /usr/local/bin/sops /usr/local/bin/sops @@ -107,8 +97,6 @@ COPY --from=build /cloud_passport /cloud_passport COPY --from=build /python /python COPY --from=build /etc/pip.conf /etc/pip.conf -# Set permissions -RUN chmod +x /usr/local/bin/sops # Create directories that might be needed for CI environments # These directories are commonly used by GitHub Actions and GitLab CI @@ -127,18 +115,12 @@ RUN mkdir -p /__w/_temp/_runner_file_commands && \ RUN rm -rf /var/cache/apk/* /tmp/* /var/tmp/* /root/.cache RUN find /module/venv/lib/python3.12/site-packages -name '*.pyc' -delete RUN /module/venv/bin/pip cache purge -# Keep pip for runtime compatibility, but remove setuptools and wheel -RUN rm -rf /module/venv/lib/python3.12/site-packages/setuptools* \ - /module/venv/lib/python3.12/site-packages/wheel* 2>/dev/null || true -# Set environment ENV PATH=/module/venv/bin:$PATH \ PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 -# Health check HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD python -c "import sys; sys.exit(0)" || exit 1 -# Default command CMD ["bash"] diff --git a/build_pipegene/scripts/appregdef_render_job.py b/build_pipegene/scripts/appregdef_render_job.py index 5a3aa9a08..763a57051 100644 --- a/build_pipegene/scripts/appregdef_render_job.py +++ b/build_pipegene/scripts/appregdef_render_job.py @@ -10,14 +10,12 @@ def prepare_appregdef_render_job(pipeline, params, full_env, environment_name, c env_template_version = params.get('ENV_TEMPLATE_VERSION') is_template_test = params.get('IS_TEMPLATE_TEST') env_tmp_ver_update_mode = params.get('ENV_TEMPLATE_VERSION_UPDATE_MODE') - script = [ - '/module/scripts/handle_certs.sh', - ] - + + script = [] if env_template_version and not is_template_test: script.append('python3 /build_env/scripts/build_env/env_template/set_template_version.py') - script.append('cd /build_env; python3 /build_env/scripts/build_env/appregdef_render.py') + script.append('python3 /build_env/scripts/build_env/appregdef_render.py') appregdef_render_params = { "name": f'app_reg_def_render.{full_env}', diff --git a/build_pipegene/scripts/credential_rotation_job.py b/build_pipegene/scripts/credential_rotation_job.py index 4b40cc80c..940d0ebb8 100644 --- a/build_pipegene/scripts/credential_rotation_job.py +++ b/build_pipegene/scripts/credential_rotation_job.py @@ -9,7 +9,6 @@ def prepare_credential_rotation_job(pipeline, full_env, environment_name, cluste "image": '${envgen_image}', "stage": 'credential_rotation', "script": [ - '/module/scripts/handle_certs.sh', "python3 /module/creds_rotation_scripts/creds_rotation_handler.py", ], } diff --git a/build_pipegene/scripts/effective_set_job.py b/build_pipegene/scripts/effective_set_job.py index b3f9074a1..3941531b7 100644 --- a/build_pipegene/scripts/effective_set_job.py +++ b/build_pipegene/scripts/effective_set_job.py @@ -33,7 +33,6 @@ def prepare_generate_effective_set_job(pipeline, full_env_name, env_name, cluste sd_path = Path(f'{base_dir}/environments/{full_env_name}/Inventory/solution-descriptor/sd.yaml') # TODO it is necessary to remove unnecessary calls, leave only script calls in such jobs! bad for gsf delivery script = [ - '/module/scripts/handle_certs.sh', # cert handling for java 'mkdir -p ${CI_PROJECT_DIR}/configuration/certs/', 'if [ -f /default_cert.pem ]; then cp /default_cert.pem "${CI_PROJECT_DIR}/configuration/certs/"; fi', @@ -55,7 +54,7 @@ def prepare_generate_effective_set_job(pipeline, full_env_name, env_name, cluste effective_set_config_dict = json.loads(effective_set_config) effective_set_version = effective_set_config_dict.get("version") or "v2.0" - full_sd_exists = sd_path.parent.is_dir() and sd_path.is_file() + full_sd_exists = sd_path.is_file() sd_data = bool(sd_data) or bool(sd_version) if not (full_sd_exists and sd_data) and effective_set_version.lower() == "v1.0": diff --git a/build_pipegene/scripts/env_build_jobs.py b/build_pipegene/scripts/env_build_jobs.py index 6b352afb3..48dbf7f01 100644 --- a/build_pipegene/scripts/env_build_jobs.py +++ b/build_pipegene/scripts/env_build_jobs.py @@ -8,9 +8,8 @@ def prepare_env_build_job(pipeline, is_template_test, full_env, enviroment_name, logger.info(f'prepare env_build job for {full_env}') script = [ - '/module/scripts/handle_certs.sh', + 'cd /build_env; python3 /build_env/scripts/build_env/main.py' ] - script.append('cd /build_env; python3 /build_env/scripts/build_env/main.py') if is_template_test: script.append('env_name=$(cat "$CI_PROJECT_DIR/set_variable.txt")') @@ -62,7 +61,6 @@ def prepare_git_commit_job(pipeline, full_env, enviroment_name, cluster_name, de "image": '${envgen_image}', "stage": 'git_commit', "script": [ - '/module/scripts/handle_certs.sh', '/module/scripts/git_commit.sh', "export env_name=$(echo $ENV_NAME | awk -F '/' '{print $NF}')", 'env_path=$(sudo find $CI_PROJECT_DIR/environments -type d -name "$env_name")', diff --git a/build_pipegene/scripts/inventory_generation_job.py b/build_pipegene/scripts/inventory_generation_job.py index 312cb91f9..ac1fabb5d 100644 --- a/build_pipegene/scripts/inventory_generation_job.py +++ b/build_pipegene/scripts/inventory_generation_job.py @@ -36,7 +36,6 @@ def prepare_inventory_generation_job(pipeline, full_env_name, environment_name, "image": "${envgen_image}", "stage": "env_inventory_generation", "script": [ - '/module/scripts/handle_certs.sh', "python3 /build_env/scripts/build_env/env_inventory_generation.py", ], } diff --git a/build_pipegene/scripts/passport_jobs.py b/build_pipegene/scripts/passport_jobs.py index e7bc24d91..532d3f638 100644 --- a/build_pipegene/scripts/passport_jobs.py +++ b/build_pipegene/scripts/passport_jobs.py @@ -38,12 +38,12 @@ def prepare_passport_job(pipeline, full_env, enviroment_name, cluster_name, tags "name": f'get_passport.{full_env}', "image": '${envgen_image}', "stage": 'process_passport', - "script": ['/module/scripts/handle_certs.sh', - 'python3 /cloud_passport/scripts/main.py --env_name "$ENV_NAME",', - "export env_name=$(echo $ENV_NAME | awk -F '/' '{print $NF}')", - 'env_path=$(sudo find $CI_PROJECT_DIR/environments -type d -name "$env_name")', - 'for path in $env_path; do if [ -d "$path/Credentials" ]; then sudo chmod ugo+rw $path/Credentials/*; fi; done' - ], + "script": [ + 'python3 /cloud_passport/scripts/main.py --env_name "$ENV_NAME",', + "export env_name=$(echo $ENV_NAME | awk -F '/' '{print $NF}')", + 'env_path=$(sudo find $CI_PROJECT_DIR/environments -type d -name "$env_name")', + 'for path in $env_path; do if [ -d "$path/Credentials" ]; then sudo chmod ugo+rw $path/Credentials/*; fi; done' + ], } get_passport_params['script'].append('/module/scripts/git_commit.sh') get_passport_vars = { diff --git a/build_pipegene/scripts/pipeline_helper.py b/build_pipegene/scripts/pipeline_helper.py index e285b7d8f..9cb022d17 100644 --- a/build_pipegene/scripts/pipeline_helper.py +++ b/build_pipegene/scripts/pipeline_helper.py @@ -45,7 +45,8 @@ def job_instance(params, vars, needs=None, rules=None): job.prepend_scripts(params['before_script']) global_before = [ - 'python /module/scripts/utils/log_pipe_params.py' + 'python /module/scripts/utils/log_pipe_params.py', + '/module/scripts/utils/handle_certs.sh', ] job.prepend_scripts(*global_before) diff --git a/build_pipegene/scripts/process_sd_job.py b/build_pipegene/scripts/process_sd_job.py index d9268a101..8e9bdb3c8 100644 --- a/build_pipegene/scripts/process_sd_job.py +++ b/build_pipegene/scripts/process_sd_job.py @@ -15,16 +15,14 @@ def prepare_process_sd(pipeline, full_env, environment_name, cluster_name, artif reg_defs_path = f"{base_env_path}/RegDefs" script = [ - 'bash /module/scripts/handle_certs.sh', - 'source ~/.bashrc', f'[ -n "$APP_REG_DEFS_JOB" ] && [ -n "$APP_DEFS_PATH" ] && mkdir -p {app_defs_path} && cp -rf {artifact_app_defs_path}/* {app_defs_path}', f'[ -n "$APP_REG_DEFS_JOB" ] && [ -n "$REG_DEFS_PATH" ] && mkdir -p {reg_defs_path} && cp -fr {artifact_reg_defs_path}/* {reg_defs_path}', - 'python3 /module/scripts/process_sd.py', + 'python3 /build_env/scripts/build_env/process_sd.py', ] process_sd_set_params = { "name": f'process_sd.{full_env}', - "image": '${effective_set_generator_image}', + "image": '${envgen_image}', "stage": 'process_sd', "script": script } @@ -34,7 +32,7 @@ def prepare_process_sd(pipeline, full_env, environment_name, cluster_name, artif "ENVIRONMENT_NAME": environment_name, "ENV_NAME": environment_name, "INSTANCES_DIR": "${CI_PROJECT_DIR}/environments", - "effective_set_generator_image": "$effective_set_generator_image", + "envgen_image": "$envgen_image", "envgen_args": " -vv", "envgen_debug": "true", "GITLAB_RUNNER_TAG_NAME": tags, diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index bb1955775..250de9d24 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -277,7 +277,7 @@ jobs: source /module/venv/bin/activate echo 'Executing APP_REG_DEF_PROCESS...' python3 /module/scripts/utils/log_pipe_params.py - /module/scripts/handle_certs.sh + /module/scripts/utils/handle_certs.sh python3 /build_env/scripts/build_env/env_template/set_template_version.py python3 /build_env/scripts/build_env/appregdef_render.py " @@ -305,7 +305,7 @@ jobs: source /module/venv/bin/activate echo 'Executing PROCESS_SD...' - /module/scripts/handle_certs.sh + /module/scripts/utils/handle_certs.sh base_env_path=\"\${CI_PROJECT_DIR}/environments/${{ matrix.environment }}\" app_defs_path=\"\${base_env_path}/AppDefs\" reg_defs_path=\"\${base_env_path}/RegDefs\" @@ -337,7 +337,7 @@ jobs: source /module/venv/bin/activate echo 'Executing ENV_BUILD...' python /module/scripts/utils/log_pipe_params.py - /module/scripts/handle_certs.sh + /module/scripts/utils/handle_certs.sh cd /build_env; python3 /build_env/scripts/build_env/main.py " @@ -363,7 +363,7 @@ jobs: bash -c " echo 'Executing effective set generation...' source /module/venv/bin/activate - /module/scripts/handle_certs.sh + /module/scripts/utils/handle_certs.sh mkdir -p \"\${CI_PROJECT_DIR}/configuration/certs\" [ -f /default_cert.pem ] && cp /default_cert.pem \"\${CI_PROJECT_DIR}/configuration/certs/default_cert.pem\" || true for cert in \"\${CI_PROJECT_DIR}/configuration/certs/\"*; do [ -f \"\$cert\" ] && keytool -import -trustcacerts -alias \"\$(basename \"\$cert\")\" -file \"\$cert\" -keystore /etc/ssl/certs/keystore.jks -storepass changeit -noprompt; done @@ -412,7 +412,7 @@ jobs: bash -c " source /module/venv/bin/activate echo 'Prepare git_commit job for \${ENVIRONMENT_NAME}...' - /module/scripts/handle_certs.sh + /module/scripts/utils/handle_certs.sh # Execute git commit with proper error handling echo 'Executing git commit operations...' /module/scripts/git_commit.sh diff --git a/scripts/build_env/process_sd.py b/scripts/build_env/process_sd.py index 9aaba559b..8ff6c207d 100644 --- a/scripts/build_env/process_sd.py +++ b/scripts/build_env/process_sd.py @@ -5,9 +5,9 @@ from os import path, getenv from pathlib import Path +import yaml import envgenehelper as helper -import yaml from artifact_searcher import artifact from artifact_searcher.utils import models as artifact_models from envgenehelper.business_helper import getenv_and_log, getenv_with_error diff --git a/build_envgene/scripts/handle_certs.sh b/scripts/utils/handle_certs.sh similarity index 86% rename from build_envgene/scripts/handle_certs.sh rename to scripts/utils/handle_certs.sh index 781bc5f3a..3332cb241 100755 --- a/build_envgene/scripts/handle_certs.sh +++ b/scripts/utils/handle_certs.sh @@ -15,7 +15,7 @@ fi if [ ! -d "$certs_dir" ] || ! find "$certs_dir" -mindepth 1 -print -quit >/dev/null 2>&1; then if [ -f "$default_cert" ]; then # shellcheck disable=SC1091 - . /module/scripts/update_ca_cert.sh "$default_cert" + . /module/scripts/utils/update_ca_cert.sh "$default_cert" else log "No certificates found and default certificate does not exist: $default_cert" fi @@ -23,6 +23,6 @@ else # Iterate files safely (handles spaces/newlines) while IFS= read -r -d '' cert; do # shellcheck disable=SC1091 - . /module/scripts/update_ca_cert.sh "$cert" + . /module/scripts/utils/update_ca_cert.sh "$cert" done < <(find "$certs_dir" -mindepth 1 -maxdepth 1 -type f -print0) fi \ No newline at end of file diff --git a/build_envgene/scripts/update_ca_cert.sh b/scripts/utils/update_ca_cert.sh similarity index 100% rename from build_envgene/scripts/update_ca_cert.sh rename to scripts/utils/update_ca_cert.sh From e7cc8e1538694368bc9b346b5b702a360ef75af1 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Thu, 26 Feb 2026 06:47:57 +0000 Subject: [PATCH 045/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 250de9d24..a140e4d35 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.27.1" - DOCKER_IMAGE_TAG_ENVGENE: "1.27.1" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.27.1" + DOCKER_IMAGE_TAG_PIPEGENE: "1.28.0" + DOCKER_IMAGE_TAG_ENVGENE: "1.28.0" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.28.0" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 287b3ee82..b43b25109 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.27.1 +version: 1.28.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index f838e68bb..20e7d661b 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.27.1 +version: 1.28.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 960b188ea..fe57733ee 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.27.1", + "envgene_version": "1.28.0", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From b57f79fca1bf9fd59672083297d8a98ac0dff926 Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:31:34 +0300 Subject: [PATCH 046/161] feat: Added the instruction for Envgene Instance Github workflow (#1059) --- .../instance-repo-pipeline/README.md | 673 ++++++++++++++++++ .../assets/envgene-workflow-header.png | Bin 0 -> 409135 bytes 2 files changed, 673 insertions(+) create mode 100644 github_workflows/instance-repo-pipeline/README.md create mode 100644 github_workflows/instance-repo-pipeline/assets/envgene-workflow-header.png diff --git a/github_workflows/instance-repo-pipeline/README.md b/github_workflows/instance-repo-pipeline/README.md new file mode 100644 index 000000000..6c8a94158 --- /dev/null +++ b/github_workflows/instance-repo-pipeline/README.md @@ -0,0 +1,673 @@ +# EnvGene GitHub Workflow + +
+ +User Guide + +[![GitHub Actions](https://img.shields.io/badge/GitHub_Actions-workflow_dispatch-2088FF?style=flat-square&logo=github-actions&logoColor=white)](https://docs.github.com/en/actions) +[![Manual Trigger](https://img.shields.io/badge/trigger-manual-orange?style=flat-square)](#how-to-trigger-the-workflow) + +
+ +![EnvGene Workflow](assets/envgene-workflow-header.png) + +- [EnvGene GitHub Workflow — User Guide](#envgene-github-workflow) + - [Overview](#overview) + - [Installation](#installation) + - [Quick Start](#quick-start) + - [Workflow Structure](#workflow-structure) + - [Pipeline Steps](#pipeline-steps) + - [Workflow Dispatch Inputs](#workflow-dispatch-inputs) + - [Understanding the 10-Input Limit](#understanding-the-10-input-limit) + - [Input Reference](#input-reference) + - [GH_ADDITIONAL_PARAMS — Passing Extra Parameters](#gh_additional_params--passing-extra-parameters) + - [What Is GH_ADDITIONAL_PARAMS?](#what-is-gh_additional_params) + - [Format and Syntax](#format-and-syntax) + - [Examples](#examples) + - [JSON Values and Escaping](#json-values-and-escaping) + - [When to Use pipeline_vars.env Instead](#when-to-use-pipeline_varsenv-instead) + - [Adding New Parameters](#adding-new-parameters) + - [Option A: Add as Workflow Input (If Under the Limit)](#option-a-add-as-workflow-input-if-under-the-limit) + - [Option B: Use GH_ADDITIONAL_PARAMS](#option-b-use-gh_additional_params) + - [Option C: Use pipeline_vars.env](#option-c-use-pipeline_varsenv) + - [Adding New Jobs and Conditional Execution](#adding-new-jobs-and-conditional-execution) + - [Step 1: Ensure the Variable Is Available](#step-1-ensure-the-variable-is-available) + - [Step 2: Expose the Variable as a Job Output](#step-2-expose-the-variable-as-a-job-output) + - [Step 3: Add the Job Step with an if Condition](#step-3-add-the-job-step-with-an-if-condition) + - [Complete Example: Adding a Custom Job](#complete-example-adding-a-custom-job) + - [Parameter Priority](#parameter-priority) + - [Repository Variables (vars)](#repository-variables-vars) + - [How to Trigger the Workflow](#how-to-trigger-the-workflow) + - [Via GitHub Actions UI](#via-github-actions-ui) + - [Via GitHub API](#via-github-api) + - [Directory Structure](#directory-structure) + - [Use Case Scenarios](#use-case-scenarios) + - [Further Reading](#further-reading) + +## Overview + +The **EnvGene** workflow (`Envgene.yml`) is a GitHub Actions pipeline that automates environment generation, configuration, and deployment for the EnvGene platform. It provides the same functionality as the GitLab-based instance pipeline, adapted for GitHub Actions. + +> [!NOTE] +> The workflow is **manually triggered only** (`workflow_dispatch`). There is no automatic trigger on push or pull request. + +The workflow supports: + +- Environment inventory generation +- Application and registry definition processing +- Solution Descriptor (SD) processing +- Environment build +- Effective Set generation +- Blue-Green management +- Credential rotation +- Git commit of generated artifacts + +--- + +## Installation + +This section describes what you need to set up the EnvGene workflow in your instance repository. + +### Prerequisites + +- A GitHub repository (instance repository) with the [EnvGene instance structure](/docs/samples/instance-repository/) +- GitHub Actions enabled for the repository +- GitHub-hosted runners (or self-hosted runners with Docker available) + +### Step 1: Copy the Pipeline + +Copy the `.github` directory from this folder to the root of your instance repository: + +```bash +cp -r github_workflows/instance-repo-pipeline/.github /path/to/your/instance-repo/ +``` + +The copied structure includes the workflow, scripts, configuration files, and the `load-env-files` action. + +### Step 2: Configure Required Secrets + +Go to **Settings** → **Secrets and variables** → **Actions** → **Secrets**, and add: + +| Secret | Required | Description | +|---------------------------|-------------------|-----------------------------------------------------------------------| +| `SECRET_KEY` | When using Fernet | Fernet key for credential encryption | +| `ENVGENE_AGE_PUBLIC_KEY` | When using SOPS | Public key from EnvGene AGE key pair (SOPS encryption) | +| `ENVGENE_AGE_PRIVATE_KEY` | When using SOPS | Private key from EnvGene AGE key pair (SOPS decryption) | +| `GH_ACCESS_TOKEN` | Yes | GitHub token with `contents: write` to commit changes to repository | + +> [!NOTE] +> At least one encryption method (Fernet or SOPS) must be configured if your repository uses encrypted credentials. See [Credential Encryption](/docs/how-to/credential-encryption.md) for details. + +### Step 3: Optional — Repository Variables + +Configure variables in **Settings** → **Secrets and variables** → **Actions** → **Variables** to override defaults: + +| Variable | Default | Purpose | +|----------------------------|----------------------|---------------------------------------------------| +| `DOCKER_REGISTRY` | `ghcr.io/netcracker` | Docker registry for EnvGene images | +| `GH_RUNNER_TAG_NAME` | `ubuntu-22.04` | Runner label for workflow jobs | +| `GH_RUNNER_SCRIPT_TIMEOUT` | `10` | Job timeout in minutes | + +See [Repository Variables (vars)](#repository-variables-vars) for details. + +### Step 4: Optional — Customize Configuration + +- **`.github/configuration/config.env`** — Base pipeline configuration (e.g. `CI_PROJECT_DIR`, `GITHUB_USER_*`). Edit if you need different defaults. +- **`.github/pipeline_vars.env`** — Override pipeline parameters for debugging or recurring runs. Leave empty or add variables as needed. + +### Verifying the Setup + +1. Ensure the workflow file is at `.github/workflows/Envgene.yml`. +1. Ensure required secrets are set. +1. Trigger the workflow manually (see [Quick Start](#quick-start)) with a valid `ENV_NAMES` value. + +For initializing a new instance repository from scratch, see [Environment Instance Repository Installation Guide](/docs/how-to/envgene-maitanance.md). + +## Quick Start + +> [!TIP] +> New to EnvGene? Start with [Installation](#installation), then come back here. + +1. Ensure the pipeline is installed (see [Installation](#installation)). +1. Go to **Actions** → **EnvGene Execution** → **Run workflow**. +1. Fill in **ENV_NAMES** (e.g. `cluster-01/env-01`) and any other parameters. +1. Click **Run workflow**. + +## Workflow Structure + +The workflow consists of two main jobs: + +| Job | Purpose | +|----------------------------------|-------------------------------------------------------------------------| +| `process_environment_variables` | Parses inputs, loads config, builds matrix, exports variables | +| `envgene_execution` | Runs EnvGene steps per environment (matrix job) | + +The first job prepares all parameters and passes them to the second job via a shared `.env` artifact and job outputs. The second job runs once per environment in the matrix. + +### Pipeline Steps + +The following sections describe each step in the pipeline as defined in `Envgene.yml`. Steps marked as *conditional* run only when their condition is met. + +#### Job: `process_environment_variables` + +| Step | Description | +|------------------------------|-----------------------------------------------------------------------------| +| Repository Checkout | Checks out the repository (without persisting credentials) | +| Load environment variables | Loads `config.env` and `pipeline_vars.env` into `GITHUB_ENV` | +| Process Input Parameters | Exports workflow inputs (ENV_NAMES, ENV_BUILDER, etc.) to environment | +| Process additional variables | Parses `GH_ADDITIONAL_PARAMS` and adds each `KEY=VALUE` to environment | +| Create env_generation_params | Builds `ENV_GENERATION_PARAMS` JSON from SD/ENV_SPECIFIC_PARAMS variables | +| Multiple Environment Processing | Generates environment matrix from `ENV_NAMES` (comma/semicolon/space) | +| Create .env file | Dumps all environment variables to `.env` | +| Upload .env as artifact | Uploads `.env` for use by `envgene_execution` job | + +#### Job: `envgene_execution` (runs per environment in matrix) + +| Step | Condition | Description | +|-------------------------|------------------------------------------------|-----------------------------------------------------------------------------| +| Repository Checkout | Always | Checks out repository with full history | +| Download environment-file| Always | Downloads `.env` artifact from previous job | +| Prepare environment | Always | Restores env vars, sets `PACKAGE_NAME`, extracts `CLUSTER_NAME`/`ENV_NAME` | +| Create name for dynamic secret | Always | Sets `SECRET_NAME` for cluster-specific secrets | +| Create env file for container | Always | Exports env to `.env.container` for Docker steps | +| **BG_MANAGE** | `BG_MANAGE == 'true'` | Blue-Green operations: state management, Origin/Peer config, validation | +| **ENV_INVENTORY_GENERATION** | `ENV_INVENTORY_CONTENT` / `ENV_SPECIFIC_PARAMS` / `ENV_TEMPLATE_NAME` set | Generates Environment Inventory at `env_definition.yml` | +| **CREDENTIAL_ROTATION** | `CRED_ROTATION_PAYLOAD` not empty | Rotates credentials per payload | +| **APP_REG_DEF_PROCESS** | `ENV_BUILDER == 'true'` | Sets template version, renders App/Reg definitions, handles certs | +| **PROCESS_SD** | `SD_SOURCE_TYPE=json` + `SD_DATA` or `artifact` + `SD_VERSION` | Processes Solution Descriptor (JSON or artifact) | +| **ENV_BUILD** | `ENV_BUILDER == 'true'` | Main environment build: generates Environment Instance from templates | +| **GENERATE_EFFECTIVE_SET** | `GENERATE_EFFECTIVE_SET == 'true'` | Generates Effective Set (SBOMs, validation, deployment artifacts) | +| **GIT_COMMIT** | Always | Commits changes to repository, prepares artifacts for downstream use | + +Each conditional step (in **bold**) also uploads its output as an artifact. The `GIT_COMMIT` step always runs at the end of the pipeline. + +## Workflow Dispatch Inputs + +### Understanding the 10-Input Limit + +> [!IMPORTANT] +> GitHub Actions limits `workflow_dispatch` to **10 input parameters**. The EnvGene pipeline uses 9 of them for the most common parameters. The 10th slot is reserved for `GH_ADDITIONAL_PARAMS`, which acts as a container for all other parameters. + +This design lets you pass any number of additional parameters without hitting the limit. + +### Input Reference + +| Input | Required | Default | Type | Description | +|-------------------------|----------|-----------|--------|-------------------------------------------------------------------------| +| `ENV_NAMES` | Yes | — | string | Environment(s) to process. Format: `cluster/env` or comma-separated | +| `DEPLOYMENT_TICKET_ID` | No | `""` | string | Ticket ID used as commit message prefix | +| `ENV_TEMPLATE_VERSION` | No | `""` | string | Template version to apply (e.g. `env-template:v1.2.3`) | +| `ENV_BUILDER` | No | `"true"` | choice | Enable environment build. Options: `true`, `false` | +| `GENERATE_EFFECTIVE_SET`| No | `"false"` | choice | Enable Effective Set generation. Options: `true`, `false` | +| `GET_PASSPORT` | No | `"false"` | choice | Enable Cloud Passport discovery. Options: `true`, `false` | +| `CMDB_IMPORT` | No | `"false"` | choice | Enable CMDB export. Options: `true`, `false` | +| `GH_ADDITIONAL_PARAMS` | No | `""` | string | Comma-separated key-value pairs for all other parameters | + +For full parameter semantics, see [Instance Pipeline Parameters](/docs/instance-pipeline-parameters.md). + +## GH_ADDITIONAL_PARAMS — Passing Extra Parameters + +### What Is GH_ADDITIONAL_PARAMS? + +`GH_ADDITIONAL_PARAMS` is a single string input that carries all pipeline parameters that are not exposed as separate workflow inputs. It is parsed by `.github/scripts/process_additional_variables.sh`, which adds each `KEY=VALUE` pair to the workflow environment. + +Use it for parameters such as: + +- `BG_MANAGE`, `BG_STATE` — Blue-Green operations +- `SD_SOURCE_TYPE`, `SD_VERSION`, `SD_DATA` — Solution Descriptor +- `ENV_SPECIFIC_PARAMS`, `ENV_TEMPLATE_NAME` — Environment configuration +- `EFFECTIVE_SET_CONFIG` — Effective Set options +- `CRED_ROTATION_PAYLOAD` — Credential rotation +- Any other parameter from [Instance Pipeline Parameters](/docs/instance-pipeline-parameters.md) + +### Format and Syntax + +**Format:** `KEY1=VALUE1,KEY2=VALUE2,KEY3=VALUE3` + +**Rules:** + +- Pairs are separated by commas. +- Each pair is `KEY=VALUE` (no spaces around `=`). +- Keys and values are trimmed of leading/trailing whitespace. +- Empty pairs are ignored. + +### Examples + +**Simple values:** + +```text +BG_MANAGE=true,SD_SOURCE_TYPE=artifact,SD_VERSION=my-app:v1.0 +``` + +**With JSON (escape double quotes):** + +```text +EFFECTIVE_SET_CONFIG={\"version\": \"v2.0\", \"app_chart_validation\": \"false\"} +``` + +**Multiple parameters:** + +```text +SD_SOURCE_TYPE=json,SD_DATA=[{\"version\":2.1,\"type\":\"solutionDeploy\"}],ENV_SPECIFIC_PARAMS={\"tenantName\":\"my-tenant\"} +``` + +**Blue-Green state:** + +```text +BG_MANAGE=true,BG_STATE={\"controllerNamespace\":\"bss-controller\",\"originNamespace\":{\"name\":\"bss-origin\",\"state\":\"active\"}} +``` + +### JSON Values and Escaping + +For JSON values: + +1. Escape internal double quotes: `\"` instead of `"`. +1. Be aware that commas inside JSON are used as pair separators. If your JSON contains commas, the parser may split it incorrectly. + +> [!CAUTION] +> **Workaround for complex JSON:** Use `pipeline_vars.env` (see below) or pass the parameter via the GitHub API with proper escaping. Commas inside JSON values may cause incorrect parsing. + +### When to Use pipeline_vars.env Instead + +Use `.github/pipeline_vars.env` when: + +- You have complex JSON with many commas. +- You want to keep sensitive or long values out of the UI. +- You need the same values across many runs (e.g. for debugging). + +Variables in `pipeline_vars.env` must be in standard `KEY=VALUE` format. Do **not** wrap them in `GH_ADDITIONAL_PARAMS`. + +## Adding New Parameters + +### Option A: Add as Workflow Input (If Under the Limit) + +If you have fewer than 10 inputs and want a dedicated UI field: + +1. Add the input under `on.workflow_dispatch.inputs` in `Envgene.yml`: + +```yaml +on: + workflow_dispatch: + inputs: + # ... existing inputs ... + MY_NEW_PARAM: + required: false + default: "" + type: string + description: "Description of the parameter" +``` + +1. Add a line in the "Process Input Parameters" step to export it: + +```yaml +echo "MY_NEW_PARAM=${{ github.event.inputs.MY_NEW_PARAM }}" >> $GITHUB_ENV +``` + +1. If the parameter controls job execution, add it to `process_environment_variables.outputs` (see [Adding New Jobs](#adding-new-jobs-and-conditional-execution)). + +### Option B: Use GH_ADDITIONAL_PARAMS + +1. Pass the parameter in `GH_ADDITIONAL_PARAMS`, e.g. `MY_NEW_PARAM=value`. +1. It will be parsed and added to `GITHUB_ENV` automatically. +1. If you need it for conditional steps, add it to the job outputs (see below). + +### Option C: Use pipeline_vars.env + +1. Add the variable to `.github/pipeline_vars.env`: + +```text +MY_NEW_PARAM=my_value +``` + +1. It will be loaded by the `load-env-files` action. +1. If you need it for conditional steps, add it to the job outputs. + +## Adding New Jobs and Conditional Execution + +To add a new step that runs only when a parameter is set, follow these steps. + +### Step 1: Ensure the Variable Is Available + +The variable must be present in `GITHUB_ENV` after the `process_environment_variables` job. It can come from: + +- A workflow input (and the "Process Input Parameters" step) +- `GH_ADDITIONAL_PARAMS` (parsed by `process_additional_variables.sh`) +- `pipeline_vars.env` or `config.env` (loaded by `load-env-files`) + +### Step 2: Expose the Variable as a Job Output + +Add the variable to the `outputs` of `process_environment_variables` in `Envgene.yml`: + +```yaml +jobs: + process_environment_variables: + outputs: + env_matrix: ${{ steps.matrix-generator.outputs.env_matrix }} + # ... existing outputs ... + MY_NEW_FEATURE: ${{ env.MY_NEW_FEATURE }} +``` + +Without this, the next job cannot use it in `if` conditions. + +### Step 3: Add the Job Step with an if Condition + +Add your step inside the `envgene_execution` job with an `if`: + +```yaml +- name: MY_NEW_JOB + if: needs.process_environment_variables.outputs.MY_NEW_FEATURE == 'true' + run: | + # Your commands here +``` + +**Common condition patterns:** + +| Condition type | Example | +|---------------------|-------------------------------------------------------------------------| +| Equals string | `needs.process_environment_variables.outputs.MY_VAR == 'true'` | +| Not empty | `needs.process_environment_variables.outputs.MY_VAR != ''` | +| Logical OR | `(condition1) \|\| (condition2)` | +| Logical AND | `(condition1) && (condition2)` | +| Multiple conditions | `outputs.ENV_BUILDER == 'true' && outputs.SD_VERSION != ''` | + +### Complete Example: Adding a Custom Job + +Assume you want a step that runs only when `RUN_CUSTOM_VALIDATION=true`. + +**1. Pass the parameter** via `GH_ADDITIONAL_PARAMS`: + +```text +RUN_CUSTOM_VALIDATION=true +``` + +**2. Add the output** in `Envgene.yml`: + +```yaml +process_environment_variables: + outputs: + # ... existing ... + RUN_CUSTOM_VALIDATION: ${{ env.RUN_CUSTOM_VALIDATION }} +``` + +**3. Add the step** in `envgene_execution`: + +```yaml +- name: CUSTOM_VALIDATION + if: needs.process_environment_variables.outputs.RUN_CUSTOM_VALIDATION == 'true' + run: | + echo "Running custom validation..." + # Your validation logic +``` + +## Parameter Priority + +When the same parameter is set in multiple places, the effective value is chosen by this order (highest first): + +1. Workflow input parameters (UI or API) +1. `pipeline_vars.env` +1. Repository variables (`vars`) +1. Organization variables + +## Repository Variables (vars) + +Repository variables are configured in **Settings → Secrets and variables → Actions → Variables** (repository-level) or at the organization level. They are referenced in the workflow as `vars.VARIABLE_NAME` and are available to all workflow runs. + +### Variables Used by the Workflow + +| Variable | Purpose | Default when empty | +|----------------------------|------------------------------------------|----------------------| +| `DOCKER_REGISTRY` | Docker registry base for EnvGene images | `ghcr.io/netcracker` | +| `GH_RUNNER_TAG_NAME` | Runner label for jobs (e.g. ubuntu-22.04)| `ubuntu-22.04` | +| `GH_RUNNER_SCRIPT_TIMEOUT` | Job timeout in minutes | `10` | + +### How to Add Repository Variables + +1. Go to your repository on GitHub. +1. Open **Settings** → **Secrets and variables** → **Actions**. +1. Open the **Variables** tab. +1. Click **New repository variable**. +1. Enter the name (e.g. `DOCKER_REGISTRY`) and value. +1. Click **Add variable**. + +### When Variables Are Empty or Missing + +The workflow uses fallback values when a variable is not set or is empty. For example: + +```yaml +runs-on: ${{ vars.GH_RUNNER_TAG_NAME || 'ubuntu-22.04' }} +DOCKER_IMAGE_NAME_ENVGENE: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-envgene" +timeout-minutes: ${{ fromJSON(vars.GH_RUNNER_SCRIPT_TIMEOUT || '10') }} +``` + +- If `vars.GH_RUNNER_TAG_NAME` is empty or missing → `ubuntu-22.04` is used. +- If `vars.DOCKER_REGISTRY` is empty or missing → `ghcr.io/netcracker` is used. +- If `vars.GH_RUNNER_SCRIPT_TIMEOUT` is empty or missing → `10` is used. + +> [!TIP] +> You do not need to define these variables for the workflow to run; defaults are applied automatically. + +### Adding Custom Variables + +To use your own variables in the workflow: + +1. Add the variable in **Settings → Secrets and variables → Actions → Variables**. +1. Reference it in `Envgene.yml` as `${{ vars.MY_CUSTOM_VAR }}`. +1. For optional variables with a default, use: `${{ vars.MY_CUSTOM_VAR || 'default_value' }}`. + +For a full list of supported repository variables, see [EnvGene Repository Variables](/docs/envgene-repository-variables.md). + +## How to Trigger the Workflow + +### Via GitHub Actions UI + +1. Open your repository on GitHub. +1. Go to **Actions**. +1. Select **EnvGene Execution**. +1. Click **Run workflow**. +1. Choose the branch, fill in parameters, and run. + +### Via GitHub API + +
+Click to expand API example + +```bash +curl -X POST \ + -H "Authorization: token " \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos///actions/workflows/Envgene.yml/dispatches \ + -d '{ + "ref": "main", + "inputs": { + "ENV_NAMES": "cluster-01/env-01", + "ENV_BUILDER": "true", + "GENERATE_EFFECTIVE_SET": "true", + "DEPLOYMENT_TICKET_ID": "QBSHP-0001", + "GH_ADDITIONAL_PARAMS": "EFFECTIVE_SET_CONFIG={\"version\": \"v2.0\", \"app_chart_validation\": \"false\"}" + } + }' +``` + +Replace ``, ``, ``, and `main` as needed. + +
+ +## Directory Structure + +```text +instance-repo-pipeline/ +├── README.md # This file +└── .github/ + ├── actions/ + │ └── load-env-files/ # Loads .env files into GITHUB_ENV + ├── configuration/ + │ └── config.env # Base pipeline configuration + ├── docs/ + │ └── README.md # Additional usage notes + ├── scripts/ + │ ├── generate_env_matrix.sh # Builds environment matrix from ENV_NAMES + │ ├── process_additional_variables.sh # Parses GH_ADDITIONAL_PARAMS + │ ├── process_matrix_iteration.sh # Extracts cluster/env from matrix + │ └── create_env_generation_params.sh # Builds ENV_GENERATION_PARAMS JSON + ├── workflows/ + │ └── Envgene.yml # Main workflow definition + └── pipeline_vars.env # Optional overrides (template, often empty) +``` + +--- + +## Use Case Scenarios + +This section shows typical scenarios with example parameters and what happens when you run the workflow. + +### Scenario 1: Full Deployment (Environment Build + Effective Set) + +**Goal:** Build the environment and generate the Effective Set for deployment. + +| Parameter | Value | +|-----------------------|--------------------------| +| `ENV_NAMES` | `prod-cluster/prod-01` | +| `ENV_BUILDER` | `true` | +| `GENERATE_EFFECTIVE_SET` | `true` | +| `DEPLOYMENT_TICKET_ID`| `QBSHP-1234` | + +**Steps that run:** APP_REG_DEF_PROCESS → ENV_BUILD → GENERATE_EFFECTIVE_SET → GIT_COMMIT + +**Result:** Environment Instance is generated, Effective Set is created in `environments/prod-cluster/prod-01/effective-set/`, changes are committed to the repository. + +--- + +### Scenario 2: Environment Build Only (No Effective Set) + +**Goal:** Regenerate the Environment Instance without generating the Effective Set (e.g. for validation or template updates). + +| Parameter | Value | +|-------------|------------------------| +| `ENV_NAMES` | `dev-cluster/dev-01` | +| `ENV_BUILDER` | `true` | + +**Steps that run:** APP_REG_DEF_PROCESS → ENV_BUILD → GIT_COMMIT + +**Result:** Environment Instance is regenerated and committed. GENERATE_EFFECTIVE_SET is skipped. + +--- + +### Scenario 3: Update Template Version and Rebuild + +**Goal:** Switch to a new template version and rebuild the environment. + +| Parameter | Value | +|-----------------------|-------------------------------| +| `ENV_NAMES` | `prod-cluster/prod-01` | +| `ENV_BUILDER` | `true` | +| `ENV_TEMPLATE_VERSION`| `env-template:v2.1.0` | + +**Steps that run:** APP_REG_DEF_PROCESS (updates template version) → ENV_BUILD → GIT_COMMIT + +**Result:** `env_definition.yml` is updated with the new template version, environment is rebuilt with the new template, changes are committed. + +--- + +### Scenario 4: Blue-Green Operation + +**Goal:** Perform a Blue-Green operation (e.g. warmup, state change). + +| Parameter | Value | +|-----------------------|------------------------| +| `ENV_NAMES` | `prod-cluster/prod-01` | +| `GH_ADDITIONAL_PARAMS` | `BG_MANAGE=true,BG_STATE={...}` (see below) | + +**Example `GH_ADDITIONAL_PARAMS` value:** + +```text +BG_MANAGE=true,BG_STATE={\"controllerNamespace\":\"bss-ctrl\",\"originNamespace\":{\"name\":\"bss-origin\",\"state\":\"ACTIVE\",\"version\":\"v1.0\"},\"peerNamespace\":{\"name\":\"bss-peer\",\"state\":\"CANDIDATE\",\"version\":\"v1.1\"},\"updateTime\":\"2024-01-15T10:00:00Z\"} +``` + +**Steps that run:** BG_MANAGE → GIT_COMMIT + +**Result:** BG state is validated, state files are updated in the repository, namespace objects are copied if warmup. No ENV_BUILD or Effective Set. + +--- + +### Scenario 5: Credential Rotation + +**Goal:** Rotate credentials for an environment without rebuilding. + +| Parameter | Value | +|------------------------|--------------------------------------------| +| `ENV_NAMES` | `prod-cluster/prod-01` | +| `GH_ADDITIONAL_PARAMS` | `CRED_ROTATION_PAYLOAD={...}` (see below) | + +**Example `GH_ADDITIONAL_PARAMS` value:** + +```text +CRED_ROTATION_PAYLOAD={\"credentials\":[{\"name\":\"db-password\",\"newValue\":\"\"}]} +``` + +**Steps that run:** CREDENTIAL_ROTATION → GIT_COMMIT + +**Result:** Credentials are updated per payload, changes are committed. See [Credential Rotation](/docs/features/cred-rotation.md) for full payload format. + +--- + +### Scenario 6: Process Solution Descriptor from Artifact + +**Goal:** Fetch SD from an artifact and merge it into the repository. + +| Parameter | Value | +|-----------------------|-----------------------------------------------------------------------| +| `ENV_NAMES` | `prod-cluster/prod-01` | +| `GH_ADDITIONAL_PARAMS` | `SD_SOURCE_TYPE=artifact,SD_VERSION=my-solution:v1.2.3,SD_REPO_MERGE_MODE=replace` | + +**Steps that run:** PROCESS_SD → GIT_COMMIT + +**Result:** SD is downloaded from the artifact registry, merged (or replaced) into `environments/prod-cluster/prod-01/Inventory/solution-descriptor/sd.yaml`, committed. + +--- + +### Scenario 7: Generate New Environment Inventory + +**Goal:** Create a new Environment Inventory (`env_definition.yml`) for a new environment. + +| Parameter | Value | +|-----------------------|-----------------------------------------------------------------------| +| `ENV_NAMES` | `new-cluster/new-env` | +| `GH_ADDITIONAL_PARAMS` | `ENV_INVENTORY_INIT=true,ENV_TEMPLATE_NAME=my-env-template` | + +**Steps that run:** ENV_INVENTORY_GENERATION → GIT_COMMIT + +**Result:** New `env_definition.yml` is created at `environments/new-cluster/new-env/Inventory/`, committed. See [Environment Inventory Generation](/docs/features/env-inventory-generation.md). + +--- + +### Scenario 8: Multiple Environments in One Run + +**Goal:** Process several environments with the same parameters. + +| Parameter | Value | +|-------------|--------------------------------------| +| `ENV_NAMES` | `cluster-01/env-01,cluster-01/env-02,cluster-02/env-01` | +| `ENV_BUILDER` | `true` | + +**Steps that run:** For each environment in the matrix: APP_REG_DEF_PROCESS → ENV_BUILD → GIT_COMMIT (parallel jobs) + +**Result:** Three separate `envgene_execution` jobs run in parallel, each processes one environment. All changes are committed in a single workflow run. + +--- + +## Further Reading + +| Document | Description | +|----------|--------------| +| [Instance Pipeline Parameters](/docs/instance-pipeline-parameters.md) | Full parameter reference | +| [EnvGene Pipelines](/docs/envgene-pipelines.md) | Pipeline flow and job descriptions | +| [Blue-Green Deployment](/docs/features/blue-green-deployment.md) | BG-related parameters | +| [SD Processing](/docs/use-cases/sd-processing.md) | Solution Descriptor use cases | + +--- + +
+ +EnvGene GitHub Workflow — Part of the Qubership EnvGene platform + +
diff --git a/github_workflows/instance-repo-pipeline/assets/envgene-workflow-header.png b/github_workflows/instance-repo-pipeline/assets/envgene-workflow-header.png new file mode 100644 index 0000000000000000000000000000000000000000..cbd93dc86156043ddd6f66820c8447afa254b450 GIT binary patch literal 409135 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�KlHI@Jnbc->lGU?iC|>U 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�U2l&1~bj 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

DR&~wDLK!A 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%0DPnyFT_^jJ<__K^`3EfCFtg`(X2E1Z2_S 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ⅈ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 literal 0 HcmV?d00001 From 772c47e27d15a106fda7a72345987dc558c14beb Mon Sep 17 00:00:00 2001 From: Tesma Jose <113982972+tesmarishy@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:06:26 +0530 Subject: [PATCH 047/161] fix: added disabled condition for cloud services if not found in cloud passport. (#1055) --- scripts/build_env/cloud_passport.py | 9 +++++++++ .../test_environments/bgd-cluster/bgd-env/cloud.yml | 6 +++--- test_data/test_environments/cluster01/env01/cloud.yml | 6 +++--- test_data/test_environments/cluster01/env03/cloud.yml | 6 +++--- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/scripts/build_env/cloud_passport.py b/scripts/build_env/cloud_passport.py index 05c5e5e00..f96185ce9 100644 --- a/scripts/build_env/cloud_passport.py +++ b/scripts/build_env/cloud_passport.py @@ -45,6 +45,8 @@ def process_cloud_definition(cloudPassportYaml, env_dir, comment) : process_and_update_key("maasUrl", cloudYaml["maasConfig"], "MAAS_SERVICE_ADDRESS", maasPassportYaml, comment) process_and_update_key("maasInternalAddress", cloudYaml["maasConfig"], "MAAS_INTERNAL_ADDRESS", maasPassportYaml, comment) del cloudPassportYaml["maas"] + else: + store_value_to_yaml(cloudYaml["maasConfig"], "enable", False) if "vault" in cloudPassportYaml : vaultPassportYaml = cloudPassportYaml["vault"] vaultUrl = vaultPassportYaml["VAULT_ADDR"] @@ -57,6 +59,8 @@ def process_cloud_definition(cloudPassportYaml, env_dir, comment) : store_value_to_yaml(cloudYaml["vaultConfig"], "enable", False, comment) store_value_to_yaml(cloudYaml["vaultConfig"], "url", "", comment) del cloudPassportYaml["vault"] + else: + store_value_to_yaml(cloudYaml["vaultConfig"], "enable", False) if "dbaas" in cloudPassportYaml : dbaasPassportYaml = cloudPassportYaml["dbaas"] if "dbaasConfigs" not in cloudYaml or len(cloudYaml["dbaasConfigs"]) != 1 : @@ -68,6 +72,9 @@ def process_cloud_definition(cloudPassportYaml, env_dir, comment) : process_and_update_key("apiUrl", dbaasConfigYaml, "API_DBAAS_ADDRESS", dbaasPassportYaml, comment) process_and_update_key("aggregatorUrl", dbaasConfigYaml, "DBAAS_AGGREGATOR_ADDRESS", dbaasPassportYaml, comment) del cloudPassportYaml["dbaas"] + else: + for cfg in cloudYaml["dbaasConfigs"]: + store_value_to_yaml(cfg, "enable", False) if "consul" in cloudPassportYaml : consulPassportYaml = cloudPassportYaml["consul"] consulConfigYaml = cloudYaml["consulConfig"] @@ -78,6 +85,8 @@ def process_cloud_definition(cloudPassportYaml, env_dir, comment) : # CONSUL_ENABLED variable should be both in consul section and in deploy parameters store_value_to_yaml(cloudYaml["deployParameters"], "CONSUL_ENABLED", f"{consulConfigYaml['enabled']}".lower(), comment) del cloudPassportYaml["consul"] + else: + store_value_to_yaml(cloudYaml["consulConfig"], "enabled", False) # adding rest of cloud passport parameters to cloud deploy parameters logger.debug(f"Rest of params from cloud passport are: \n{dump_as_yaml_format(cloudPassportYaml)}") mergeDeployParametersFromPassport(cloudPassportYaml, cloudYaml, comment) diff --git a/test_data/test_environments/bgd-cluster/bgd-env/cloud.yml b/test_data/test_environments/bgd-cluster/bgd-env/cloud.yml index 3936201d9..4e7e338bc 100644 --- a/test_data/test_environments/bgd-cluster/bgd-env/cloud.yml +++ b/test_data/test_environments/bgd-cluster/bgd-env/cloud.yml @@ -14,7 +14,7 @@ dbMode: "db" databases: [] maasConfig: credentialsId: "maas" - enable: true + enable: false maasUrl: "http://maas.None" maasInternalAddress: "http://maas.maas:8888" vaultConfig: @@ -23,12 +23,12 @@ vaultConfig: url: "" dbaasConfigs: - credentialsId: "dbaas" - enable: true + enable: false apiUrl: "http://dbaas.dbaas:8888" aggregatorUrl: "https://dbaas.None" consulConfig: tokenSecret: "consul-token" - enabled: true + enabled: false publicUrl: "https://consul.None" internalUrl: "http://consul.consul:8888" deployParameters: diff --git a/test_data/test_environments/cluster01/env01/cloud.yml b/test_data/test_environments/cluster01/env01/cloud.yml index 2ed02be7d..84d87c2ae 100644 --- a/test_data/test_environments/cluster01/env01/cloud.yml +++ b/test_data/test_environments/cluster01/env01/cloud.yml @@ -15,7 +15,7 @@ databases: [] mergeDeployParametersAndE2EParameters: false maasConfig: credentialsId: "maas" - enable: true + enable: false maasUrl: "http://maas.qubership.org" maasInternalAddress: "http://maas.maas:8888" vaultConfig: @@ -24,12 +24,12 @@ vaultConfig: url: "" dbaasConfigs: - credentialsId: "dbaas" - enable: true + enable: false apiUrl: "http://dbaas.dbaas:8888" aggregatorUrl: "https://dbaas.qubership.org" consulConfig: tokenSecret: "consul-token" - enabled: true + enabled: false publicUrl: "https://consul.qubership.org" internalUrl: "http://consul.consul:8888" deployParameters: diff --git a/test_data/test_environments/cluster01/env03/cloud.yml b/test_data/test_environments/cluster01/env03/cloud.yml index 40e534ba9..78d604201 100644 --- a/test_data/test_environments/cluster01/env03/cloud.yml +++ b/test_data/test_environments/cluster01/env03/cloud.yml @@ -14,7 +14,7 @@ dbMode: "db" databases: [] maasConfig: credentialsId: "maas" - enable: true + enable: false maasUrl: "http://maas." maasInternalAddress: "http://maas.maas:8888" vaultConfig: @@ -23,12 +23,12 @@ vaultConfig: url: "" dbaasConfigs: - credentialsId: "dbaas" - enable: true + enable: false apiUrl: "http://dbaas.dbaas:8888" aggregatorUrl: "https://dbaas." consulConfig: tokenSecret: "consul-token" - enabled: true + enabled: false publicUrl: "https://consul." internalUrl: "http://consul.consul:8888" deployParameters: From 30369a739c947bca7d094bffe8ea224130ed2451 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Mon, 2 Mar 2026 10:38:39 +0000 Subject: [PATCH 048/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index a140e4d35..3125031d8 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.28.0" - DOCKER_IMAGE_TAG_ENVGENE: "1.28.0" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.28.0" + DOCKER_IMAGE_TAG_PIPEGENE: "1.28.1" + DOCKER_IMAGE_TAG_ENVGENE: "1.28.1" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.28.1" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index b43b25109..00815130b 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.28.0 +version: 1.28.1 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 20e7d661b..40df9530e 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.28.0 +version: 1.28.1 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index fe57733ee..c9358cc72 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.28.0", + "envgene_version": "1.28.1", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 52bcd06b9f06b392564a1d1b1798163d55079543 Mon Sep 17 00:00:00 2001 From: popoveugene <42543333+popoveugene@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:46:22 +0300 Subject: [PATCH 049/161] docs: make awsRegion mandatory (#1060) --- docs/envgene-objects.md | 4 ++-- schemas/artifact-definition-v2.schema.json | 15 +++++++++++++++ schemas/regdef-v2.schema.json | 15 +++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/envgene-objects.md b/docs/envgene-objects.md index e49160dc9..9f0654f97 100644 --- a/docs/envgene-objects.md +++ b/docs/envgene-objects.md @@ -1511,7 +1511,7 @@ registry: # `gcp`: `federation` or `service_account` # `azure`: `oauth2` authMethod: enum [ secret, assume_role, federation, service_account, oauth2, user_pass ] - # Optional + # Mandatory # Region of the AWS cloud # Used with `provider: aws` only awsRegion: string @@ -1909,7 +1909,7 @@ authConfig: # `gcp`: `federation` or `service_account` # `azure`: `oauth2` authMethod: enum [ secret, assume_role, federation, service_account, oauth2, user_pass ] - # Optional + # Mandatory # Region of the AWS cloud # Used with `provider: aws` only awsRegion: string diff --git a/schemas/artifact-definition-v2.schema.json b/schemas/artifact-definition-v2.schema.json index dde0fd065..2cd6e2d56 100644 --- a/schemas/artifact-definition-v2.schema.json +++ b/schemas/artifact-definition-v2.schema.json @@ -217,6 +217,21 @@ }, "required": [ "credentialsId" + ], + "allOf": [ + { + "if": { + "properties": { + "provider": { + "const": "aws" + } + }, + "required": ["provider"] + }, + "then": { + "required": ["awsRegion"] + } + } ] }, "MavenConfig": { diff --git a/schemas/regdef-v2.schema.json b/schemas/regdef-v2.schema.json index 7f1a30249..106ba441a 100644 --- a/schemas/regdef-v2.schema.json +++ b/schemas/regdef-v2.schema.json @@ -140,6 +140,21 @@ }, "required": [ "credentialsId" + ], + "allOf": [ + { + "if": { + "properties": { + "provider": { + "const": "aws" + } + }, + "required": ["provider"] + }, + "then": { + "required": ["awsRegion"] + } + } ] }, "DockerConfig": { From 187f3253a6f6664a487b752171cfde9e7ba9be3f Mon Sep 17 00:00:00 2001 From: popoveugene <42543333+popoveugene@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:14:50 +0300 Subject: [PATCH 050/161] docs: update isntance pipeline parameters (#1061) --- docs/instance-pipeline-parameters.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/instance-pipeline-parameters.md b/docs/instance-pipeline-parameters.md index 171ea4fb7..4c1fdc03d 100644 --- a/docs/instance-pipeline-parameters.md +++ b/docs/instance-pipeline-parameters.md @@ -129,7 +129,7 @@ This parameter serves as a configuration for an extension point. Integration wit **Allowed values**: - `PERSISTENT` (default) - Applies the standard behavior: the pipeline updates the template version in Environment Inventory by modifying `envTemplate.artifact` (or `envTemplate.templateArtifact.artifact.version`) in `env_definition.yml`, and records the template artifact version actually applied during the run in `generatedVersions.generateEnvironmentLatestVersion` in the same file. + Applies the standard behavior: the pipeline updates the template version in Environment Inventory by modifying `envTemplate.artifact` (or `envTemplate.templateArtifact.artifact.version`) in `env_definition.yml`. - `TEMPORARY` Applies `ENV_TEMPLATE_VERSION` **only for the current pipeline execution** and **does not** update `envTemplate.artifact` (or `envTemplate.templateArtifact.artifact.version`) in `env_definition.yml`. @@ -294,6 +294,8 @@ Consumer-specific pipeline context components registered in EnvGene: **Description**: Session-scoped parameters injected into the Effective Set during parameter calculation. Custom Params are not persisted across parameter calculation sessions, have the highest priority in the parameter resolution hierarchy, and are treated as sensitive. +`CUSTOM_PARAMS` is only applied when [`GENERATE_EFFECTIVE_SET`](#generate_effective_set) is `true`. If `GENERATE_EFFECTIVE_SET` is `false`, the `generate_effective_set` job does not run and `CUSTOM_PARAMS` has no effect. + EnvGene passes the value unchanged to the Calculator CLI via `--custom-params`. See [Calculator CLI](/docs/features/calculator-cli.md) for how Custom Params are applied to the Effective Set. **Format**: A string containing a JSON object (JSON-in-string). The JSON object must conform to the [schema](/schemas/custom-params.schema.json). @@ -311,6 +313,11 @@ EnvGene passes the value unchanged to the Calculator CLI via `--custom-params`. } ``` +> [!NOTE] +> +> 1. `` can be complex, i.e. a map or a list +> 2. All keys are optional + **Default Value**: None **Mandatory**: No From 8a1e87e0d9c110bd30954f925391f15cce817bfc Mon Sep 17 00:00:00 2001 From: Nurlybek Kamelov <79522742+GlimmerCape@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:24:33 +0500 Subject: [PATCH 051/161] feat: add support of multiple templates for BGD cases (#987) --- .../envgene/envgenehelper/business_helper.py | 66 +++++-- .../envgenehelper/collections_helper.py | 33 +++- scripts/bg_manage/bg_manage.py | 6 +- scripts/build_env/appregdef_render.py | 6 +- scripts/build_env/build_env.py | 117 ++++++----- .../env_template/process_env_template.py | 109 +++++++---- scripts/build_env/main.py | 40 ++-- scripts/build_env/render_config_env.py | 185 ++++++++++++------ .../tests/app_reg_defs/test_appregdefs.py | 39 ++-- .../tests/env-build/test_paramset_sorting.py | 22 ++- .../tests/env-build/test_render_envs.py | 35 ++-- .../tests/env-template/test_env_template.py | 4 +- .../Credentials/credentials.yml | 22 +++ .../Inventory/env_definition.yml | 20 ++ .../Namespaces/app-origin/namespace.yml | 22 +++ .../Namespaces/app-peer/namespace.yml | 23 +++ .../Namespaces/bg-controller/namespace.yml | 16 ++ .../bgd-ns-artifacts-env/bg_domain.yml | 12 ++ .../bgd-ns-artifacts-env/cloud.yml | 48 +++++ .../bgd-ns-artifacts-env/tenant.yml | 17 ++ .../env_templates/bgd-ns-artifacts.yaml | 14 ++ .../bgd-ns-artifacts/Namespaces/app.yml.j2 | 20 ++ .../env_templates/bgd-ns-artifacts.yaml | 14 ++ .../bgd-ns-artifacts/Namespaces/app.yml.j2 | 20 ++ .../env_templates/bgd-ns-artifacts.yaml | 14 ++ .../bgd-ns-artifacts/Namespaces/app.yml.j2 | 21 ++ .../parameters/paramset-A.yaml | 4 + 27 files changed, 719 insertions(+), 230 deletions(-) create mode 100644 test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/Credentials/credentials.yml create mode 100644 test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/Inventory/env_definition.yml create mode 100644 test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/Namespaces/app-origin/namespace.yml create mode 100644 test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/Namespaces/app-peer/namespace.yml create mode 100644 test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/Namespaces/bg-controller/namespace.yml create mode 100644 test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/bg_domain.yml create mode 100644 test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/cloud.yml create mode 100644 test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/tenant.yml create mode 100644 test_data/test_templates/env_templates/bgd-ns-artifacts.yaml create mode 100644 test_data/test_templates/env_templates/bgd-ns-artifacts/Namespaces/app.yml.j2 create mode 100644 test_data/test_templates_origin/env_templates/bgd-ns-artifacts.yaml create mode 100644 test_data/test_templates_origin/env_templates/bgd-ns-artifacts/Namespaces/app.yml.j2 create mode 100644 test_data/test_templates_peer/env_templates/bgd-ns-artifacts.yaml create mode 100644 test_data/test_templates_peer/env_templates/bgd-ns-artifacts/Namespaces/app.yml.j2 create mode 100644 test_data/test_templates_peer/parameters/paramset-A.yaml diff --git a/python/envgene/envgenehelper/business_helper.py b/python/envgene/envgenehelper/business_helper.py index 7ac7733f0..cd61f8c41 100644 --- a/python/envgene/envgenehelper/business_helper.py +++ b/python/envgene/envgenehelper/business_helper.py @@ -1,5 +1,6 @@ +from enum import auto, StrEnum import re -from dataclasses import dataclass, field +from dataclasses import InitVar, dataclass, field from os import getenv from pathlib import Path from typing import overload @@ -27,6 +28,8 @@ DEFAULT_PASSPORT_DIR_NAME = "cloud-passport" INV_GEN_CREDS_PATH = "Inventory/credentials/inventory_generation_creds.yml" +TEMPLATE_DIR_PATTERN = re.compile(r'/from_(\w+_)?template/') + def find_env_instances_dir(env_name, instances_dir): logger.debug(f"Searching for directory {env_name} in {instances_dir}") @@ -166,7 +169,8 @@ def getTemplateArtifactName(env_definition_yaml): return gav["artifact_id"] -def getEnvDefinition(env_dir): +def getEnvDefinition(env_dir = None): + env_dir = env_dir or get_current_env_dir_from_env_vars() env_definition_path = getEnvDefinitionPath(env_dir) if not check_file_exists(env_definition_path): raise ReferenceError(f"Environment definition for env {env_dir} is not found in {env_definition_path}") @@ -367,16 +371,36 @@ def find_cloud_name_from_passport(source_env_dir, all_instances_dir): else: return "" +class NamespaceRole(StrEnum): + COMMON = auto() + ORIGIN = auto() + PEER = auto() + +def get_namespace_role(ns_name: str, bgd_object: dict | None = None) -> NamespaceRole: + if not bgd_object: + bgd_object = get_bgd_object() + if not bgd_object: + return NamespaceRole.COMMON + if bgd_object['originNamespace']['name'] == ns_name: + return NamespaceRole.ORIGIN + if bgd_object['peerNamespace']['name'] == ns_name: + return NamespaceRole.PEER + return NamespaceRole.COMMON @dataclass class NamespaceFile: path: Path name: str = field(init=False) + postfix: str = field(init=False) definition_path: Path = field(init=False) + role: NamespaceRole = field(init=False) + bgd: InitVar[dict | None] = None - def __post_init__(self): + def __post_init__(self, bgd: dict | None): self.definition_path = self.path.joinpath('namespace.yml') self.name = openYaml(self.definition_path)['name'] + self.postfix = self.path.name + self.role = get_namespace_role(self.name, bgd) def get_namespaces_path(env_dir: Path | None = None) -> Path: @@ -385,17 +409,6 @@ def get_namespaces_path(env_dir: Path | None = None) -> Path: logger.debug(namespaces_path) return namespaces_path - -def get_namespaces(env_dir: Path | None = None) -> list[NamespaceFile]: - namespaces_path = get_namespaces_path(env_dir) - if not check_dir_exists(str(namespaces_path)): - return [] - namespace_paths = [p for p in namespaces_path.iterdir() if p.is_dir()] - namespaces = [NamespaceFile(path=p) for p in namespace_paths] - logger.debug(namespaces) - return namespaces - - def get_bgd_path(env_dir: Path | None = None) -> Path: env_dir = env_dir or get_current_env_dir_from_env_vars() bgd_path = env_dir.joinpath('bg_domain.yml') @@ -408,3 +421,28 @@ def get_bgd_object(env_dir: Path | None = None) -> CommentedMap: bgd_object = openYaml(bgd_path, allow_default=True) logger.debug(bgd_object) return bgd_object + +def get_namespaces(env_dir: Path | None = None) -> list[NamespaceFile]: + namespaces_path = get_namespaces_path(env_dir) + if not check_dir_exists(str(namespaces_path)): + return [] + namespace_paths = [p for p in namespaces_path.iterdir() if p.is_dir()] + bgd = get_bgd_object(env_dir) + namespaces = [NamespaceFile(path=p, bgd=bgd) for p in namespace_paths] + logger.debug(namespaces) + return namespaces + +def get_template_dirs(base_dir: str | None = None) -> dict[NamespaceRole, str]: + base_dir = base_dir if base_dir else getenv_with_error('CI_PROJECT_DIR') + result = {} + result[NamespaceRole.COMMON] = f"{base_dir}/tmp/templates" + origin_template_path = f"{base_dir}/tmp/origin/templates" + if check_dir_exists(origin_template_path): + result[NamespaceRole.ORIGIN] = origin_template_path + peer_template_path = f"{base_dir}/tmp/peer/templates" + if check_dir_exists(peer_template_path): + result[NamespaceRole.PEER] = peer_template_path + return result + +def is_from_template_dir(file_path: str) -> bool: + return bool(TEMPLATE_DIR_PATTERN.search(file_path)) diff --git a/python/envgene/envgenehelper/collections_helper.py b/python/envgene/envgenehelper/collections_helper.py index 6676da92f..557e9d382 100644 --- a/python/envgene/envgenehelper/collections_helper.py +++ b/python/envgene/envgenehelper/collections_helper.py @@ -3,6 +3,7 @@ from .yaml_helper import yaml import copy from .logger import logger +from enum import Enum def merge_lists(list1, list2) : if len(list2) > 0 : @@ -14,14 +15,30 @@ def merge_lists(list1, list2) : def is_primitive(obj): return isinstance(obj, primitives) -def dump_as_yaml_format(collection) : - if collection and isinstance(collection, dict): - tmp = copy.deepcopy(collection) +def _convert_enums(obj): + if isinstance(obj, Enum): + return obj.value + if isinstance(obj, dict): + return { + _convert_enums(k): _convert_enums(v) + for k, v in obj.items() + } + if isinstance(obj, list): + return [_convert_enums(v) for v in obj] + if isinstance(obj, tuple): + return tuple(_convert_enums(v) for v in obj) + if isinstance(obj, set): + return {_convert_enums(v) for v in obj} + return obj + + +def dump_as_yaml_format(collection): + converted = _convert_enums(collection) + if converted and isinstance(converted, dict): stream = StringIO() - yaml.dump(tmp, stream) + yaml.dump(converted, stream) return stream.getvalue() - else: - return pformat(collection) + return pformat(converted) def get_merged_param_value(key, source_dict, override_dict): if isinstance(override_dict[key], dict): @@ -97,10 +114,10 @@ def _compare_dicts_recurse(source: object, target: object, path: DictPath, diff_ diff_paths.append(path.copy()) def split_multi_value_param(param: str)-> list[str]: - + if not param: return [] - + param = param.strip() if not param: return [] diff --git a/scripts/bg_manage/bg_manage.py b/scripts/bg_manage/bg_manage.py index be4246302..206280824 100644 --- a/scripts/bg_manage/bg_manage.py +++ b/scripts/bg_manage/bg_manage.py @@ -4,7 +4,7 @@ from enum import auto, Enum from pathlib import Path -from envgenehelper.business_helper import get_current_env_dir_from_env_vars, getenv_with_error, get_namespaces, get_bgd_object +from envgenehelper.business_helper import get_current_env_dir_from_env_vars, getenv_with_error, get_namespaces, get_bgd_object, NamespaceRole from envgenehelper.file_helper import deleteFileIfExists from envgenehelper.yaml_helper import openYaml from envgenehelper import logger, writeYamlToFile @@ -106,10 +106,10 @@ def get_current_state() -> Pair: continue multiple_state_files_err_msg = f"Multiple state files found in {ENV_PATH}" - if role == "origin": + if role == NamespaceRole.ORIGIN: if origin_state != S.NONE: raise ValueError(multiple_state_files_err_msg + " for 'origin'") origin_state = state_enum - elif role == "peer": + elif role == NamespaceRole.PEER: if peer_state != S.NONE: raise ValueError(multiple_state_files_err_msg + " for 'peer'") peer_state = state_enum diff --git a/scripts/build_env/appregdef_render.py b/scripts/build_env/appregdef_render.py index f9690aa12..7f4551c70 100644 --- a/scripts/build_env/appregdef_render.py +++ b/scripts/build_env/appregdef_render.py @@ -14,7 +14,7 @@ output_dir = f"{base_dir}/environments" render_dir = f"/tmp/render/{env_name}" - templates_dir = f"{base_dir}/tmp/templates" + templates_dirs = get_template_dirs() env_dir = get_env_instances_dir(env_name, cluster_name, instances_dir) cloud_passport_file_path = find_cloud_passport_definition(env_dir, instances_dir) @@ -23,7 +23,7 @@ "cluster_name": cluster_name, "output_dir": output_dir, "current_env_dir": render_dir, - "templates_dir": templates_dir, + "templates_dirs": templates_dirs, "cloud_passport_file_path": cloud_passport_file_path, "env_instances_dir": env_dir } @@ -40,4 +40,4 @@ if src.exists(): shutil.move(src, dst) - update_generated_versions(env_dir, BUILD_ENV_TAG, template_version) + update_generated_versions(env_dir, BUILD_ENV_TAG, template_version[NamespaceRole.COMMON]) diff --git a/scripts/build_env/build_env.py b/scripts/build_env/build_env.py index 4fb10c395..1490bb243 100644 --- a/scripts/build_env/build_env.py +++ b/scripts/build_env/build_env.py @@ -1,15 +1,14 @@ import os -import sys import copy -import json import yaml import re import pathlib +from pathlib import Path + from envgenehelper import * from resource_profiles import processResourceProfiles from schema_validation import checkEnvSpecificParametersBySchema from cloud_passport import process_cloud_passport -from pathlib import Path # const GENERATED_HEADER = "The contents of this file is generated from template artifact: %s.\nContents will be overwritten by next generation.\nPlease modify this contents only for development purposes or as workaround." @@ -26,25 +25,39 @@ def find_namespaces(dir): return result -def processFileList(mask, dict, dirPointer): - fileList = list(dirPointer.rglob(mask)) - for f in fileList: - filePath = str(f) - # envSpecific = false will be update later during templates parsing - key = extractNameFromFile(filePath) - if key in dict: - dict[extractNameFromFile(filePath)].append({"filePath": filePath, "envSpecific": False}) - else: - dict[extractNameFromFile(filePath)] = [{"filePath": filePath, "envSpecific": False}] - return dict +def _get_excluded_dirs_for_role(role: NamespaceRole, origin_template_exists: bool, peer_template_exists: bool) -> list: + common_dir = 'from_template' + origin_dir = 'from_origin_template' + peer_dir = 'from_peer_template' + if role == NamespaceRole.ORIGIN and origin_template_exists: + return [common_dir, peer_dir] + elif role == NamespaceRole.PEER and peer_template_exists: + return [common_dir, origin_dir] + else: + return [origin_dir, peer_dir] -def createParamsetsMap(dir): +def create_paramset_map(dir: str, role: NamespaceRole, + origin_template_exists: bool, peer_template_exists: bool) -> dict: + excluded_dirs = _get_excluded_dirs_for_role(role, origin_template_exists, peer_template_exists) result = {} - dirPointer = pathlib.Path(dir) + dir_pointer = pathlib.Path(dir) masks = ["*.json", "*.yml", "*.yaml", "*.j2"] + for mask in masks: - result = processFileList(mask, result, dirPointer) + for f in dir_pointer.rglob(mask): + file_path = str(f) + if any(excluded_dir in file_path for excluded_dir in excluded_dirs): + continue + key = extractNameFromFile(file_path) + entry = {"filePath": file_path, "envSpecific": False} + if key in result: + result[key].append(entry) + else: + result[key] = [entry] + + logger.info(f"Created {role.name}-specific paramset map: excluded dirs {excluded_dirs}, " + f"origin_template_exists={origin_template_exists}, peer_template_exists={peer_template_exists}") logger.debug(f'List of {dir} paramsets: \n %s', dump_as_yaml_format(result)) return result @@ -184,11 +197,11 @@ def sort_paramsets_with_same_name(entries: list[dict]) -> list[dict]: # Lower sort keys are processed first, later values override earlier ones def sort_key(e): path = e["filePath"] - if "from_template" in path: - return 0, path # Template processed first (can be overridden) - elif "from_instance" in path: - return 2, path # Env-specific instance processed last (highest priority) - return 1, path # Root/cluster instance processed after template + if "from_instance" in path: + return 2, path + elif is_from_template_dir(path): + return 0, path + return 1, path return sorted(entries, key=sort_key) @@ -265,7 +278,7 @@ def convertParameterSetsToParameters(templatePath, paramsTemplate, paramsetsTag, else: paramSetAppParams = [] paramsetDefinitionComment = "paramset: " + paramSetName + " version: " + str( - paramSetVersion) + " source: " + ("template" if "from_template" in paramSetFile else "instance") + paramSetVersion) + " source: " + ("template" if is_from_template_dir(paramSetFile) else "instance") # process parameters in ParamSet for k in paramSetParameters: # get value with potential merge of dicts @@ -457,14 +470,23 @@ def process_additional_template_parameters(render_env_dir, source_env_dir, all_i logger.info(f"No shared templates variables are defined in: {envDefinitionPath}") -def getTemplateNameFromNamespacePath(namespacePath): - path = pathlib.Path(namespacePath) - return path.parent.name - - def build_env(env_name, env_instances_dir, parameters_dir, env_template_dir, resource_profiles_dir, - env_specific_resource_profile_map, all_instances_dir, render_context): - paramset_map = createParamsetsMap(parameters_dir) + env_specific_resource_profile_map, all_instances_dir, render_context, templates_dirs=None): + # Check which role-specific templates were downloaded + templates_dirs = templates_dirs or {} + origin_template_exists = NamespaceRole.ORIGIN in templates_dirs + peer_template_exists = NamespaceRole.PEER in templates_dirs + logger.info(f"Templates dirs: {list(templates_dirs.keys())}, " + f"origin_exists={origin_template_exists}, peer_exists={peer_template_exists}") + + # Create role-specific paramset maps + origin_paramset_map = create_paramset_map(parameters_dir, NamespaceRole.ORIGIN, + origin_template_exists, peer_template_exists) + peer_paramset_map = create_paramset_map(parameters_dir, NamespaceRole.PEER, + origin_template_exists, peer_template_exists) + common_paramset_map = create_paramset_map(parameters_dir, NamespaceRole.COMMON, + origin_template_exists, peer_template_exists) + env_dir = env_template_dir + "/" + env_name logger.info(f"Env name: {env_name}") logger.info(f"Env dir: {env_dir}") @@ -483,7 +505,7 @@ def build_env(env_name, env_instances_dir, parameters_dir, env_template_dir, res # pathes tenantTemplatePath = env_dir + "/tenant.yml" cloudTemlatePath = env_dir + "/cloud.yml" - namespaceTemplates = find_namespaces(env_dir) + namespaces = get_namespaces(Path(env_dir)) # env specific parameters map - will be filled with env specific parameters during template processing env_specific_parameters_map = {} env_specific_parameters_map["namespaces"] = {} @@ -502,7 +524,7 @@ def build_env(env_name, env_instances_dir, parameters_dir, env_template_dir, res "cloud", env_instances_dir, cloud_schema, - paramset_map, + common_paramset_map, env_specific_parameters_map["cloud"], resource_profiles_map=needed_resource_profiles_map, header_text=generated_header_text, @@ -515,7 +537,7 @@ def build_env(env_name, env_instances_dir, parameters_dir, env_template_dir, res "cloud", env_instances_dir, cloud_schema, - paramset_map, + common_paramset_map, env_specific_parameters_map["cloud"], resource_profiles_map=needed_resource_profiles_map, header_text=generated_header_text, @@ -523,22 +545,27 @@ def build_env(env_name, env_instances_dir, parameters_dir, env_template_dir, res # process namespaces template_namespace_names = [] - # iterate through namespace definitions and create namespace parameters - for templatePath in namespaceTemplates: - logger.info(f"Processing namespace: {templatePath}") - templateName = getTemplateNameFromNamespacePath(templatePath) - template_namespace_names.append(templateName) - initParametersStructure(env_specific_parameters_map["namespaces"], templateName) + for ns in namespaces: + logger.info(f"Processing namespace: {ns.definition_path}") + template_namespace_names.append(ns.postfix) + initParametersStructure(env_specific_parameters_map['namespaces'], ns.postfix) + + if ns.role == NamespaceRole.ORIGIN: + ns_paramset_map = origin_paramset_map + elif ns.role == NamespaceRole.PEER: + ns_paramset_map = peer_paramset_map + else: + ns_paramset_map = common_paramset_map processTemplate( - templatePath, - templateName, + ns.definition_path, + ns.postfix, env_instances_dir, namespace_schema, - paramset_map, - env_specific_parameters_map["namespaces"][templateName], + ns_paramset_map, + env_specific_parameters_map['namespaces'][ns.postfix], resource_profiles_map=needed_resource_profiles_map, - header_text=generated_header_text) - + header_text=generated_header_text, + ) logger.info(f"EnvSpecific parameters are: \n{dump_as_yaml_format(env_specific_parameters_map)}") checkEnvSpecificParametersBySchema(env_dir, env_specific_parameters_map, template_namespace_names) diff --git a/scripts/build_env/env_template/process_env_template.py b/scripts/build_env/env_template/process_env_template.py index d4c91ff80..efe3b9a94 100644 --- a/scripts/build_env/env_template/process_env_template.py +++ b/scripts/build_env/env_template/process_env_template.py @@ -7,23 +7,29 @@ from artifact_searcher.utils.models import FileExtension, Credentials, Registry, Application from env_template.template_testing import run_env_test_setup from envgenehelper import getEnvDefinition, fetch_cred_value, getAppDefinitionPath -from envgenehelper import openYaml, getenv_with_error, logger +from envgenehelper import openYaml, getenv_with_error, logger, get_or_create_nested_yaml_attribute from envgenehelper import unpack_archive, get_cred_config, check_dir_exist_and_create +from envgenehelper.business_helper import NamespaceRole +from envgenehelper.yaml_helper import get_nested_yaml_attribute_or_fail from render_config_env import render_obj_by_context, Context -ARTIFACT_DEST = f"{tempfile.gettempdir()}/artifact.zip" +def parse_artifact_appver(env_definition: dict, attribute_str: str) -> list[str]: + try: + get_nested_yaml_attribute_or_fail(env_definition, attribute_str) + exists = True + except ValueError: + exists = False + appver = str(get_or_create_nested_yaml_attribute(env_definition, attribute_str, "")) -def parse_artifact_appver(env_definition: dict) -> [str, str]: - artifact_appver = env_definition.get('envTemplate', {}).get('artifact') - if not artifact_appver: - raise ValueError(f"Environment template artifact is empty or missing from env_definition: {env_definition}") - logger.info(f"Environment template artifact version: {artifact_appver}") - return artifact_appver.split(':') + if exists and not appver: + raise ValueError(f"{attribute_str} is empty or missing from env_definition: {env_definition}") + logger.info(f"Artifact version in {attribute_str}: {appver}") + return appver.split(":") -def get_registry_creds(registry: Registry) -> Credentials: - cred_config = render_creds() + +def get_registry_creds(registry: Registry, cred_config: dict) -> Credentials: cred_id = registry.credentials_id if cred_id: username = cred_config[cred_id]['data'].get('username') @@ -57,19 +63,11 @@ def validate_url(url, group_id, artifact_id, version): # logic resolving template by artifact definition -def resolve_artifact_new_logic(env_definition: dict, template_dest: str) -> str: - app_name, app_version = parse_artifact_appver(env_definition) - - base_dir = getenv_with_error('CI_PROJECT_DIR') - artifact_path = getAppDefinitionPath(base_dir, app_name) - if not artifact_path: - raise FileNotFoundError(f"No artifact definition file found for {app_name} with .yaml or .yml extension") - app_def = Application.model_validate(openYaml(artifact_path)) - cred = get_registry_creds(app_def.registry) +async def resolve_artifact_new_logic(app_def: Application, app_version: str, template_dest: str, cred: Credentials) -> str: template_url = None resolved_version = app_version - dd_artifact_info = asyncio.run(artifact.check_artifact_async(app_def, FileExtension.JSON, app_version, cred)) + dd_artifact_info = await artifact.check_artifact_async(app_def, FileExtension.JSON, app_version, cred) if dd_artifact_info: logger.info("Loading environment template artifact info from deployment descriptor...") dd_url, dd_repo = dd_artifact_info @@ -90,15 +88,16 @@ def resolve_artifact_new_logic(env_definition: dict, template_dest: str) -> str: else: logger.info("Loading environment template artifact from zip directly...") group_id, artifact_id, version = app_def.group_id, app_def.artifact_id, app_version - artifact_info = asyncio.run(artifact.check_artifact_async(app_def, FileExtension.ZIP, app_version, cred)) + artifact_info = await artifact.check_artifact_async(app_def, FileExtension.ZIP, app_version, cred) if artifact_info: template_url, _ = artifact_info validate_url(template_url, group_id, artifact_id, version) if "-SNAPSHOT" in app_version: resolved_version = extract_snapshot_version(template_url, app_version) logger.info(f"Environment template url has been resolved: {template_url}") - artifact.download(template_url, ARTIFACT_DEST, cred) - unpack_archive(ARTIFACT_DEST, template_dest) + artifact_dest = tempfile.mkstemp(suffix='.zip')[1] + artifact.download(template_url, artifact_dest, cred) + unpack_archive(artifact_dest, template_dest) return resolved_version @@ -112,7 +111,7 @@ def render_creds() -> dict: # logic resolving template by exact coordinates and repo, deprecated -def resolve_artifact_old_logic(env_definition: dict, template_dest: str) -> str: +async def resolve_artifact_old_logic(env_definition: dict, template_dest: str, cred_config: dict, registry_dict: dict) -> str: template_artifact = env_definition['envTemplate']['templateArtifact'] artifact_info = template_artifact['artifact'] @@ -123,13 +122,10 @@ def resolve_artifact_old_logic(env_definition: dict, template_dest: str) -> str: repo_type = template_artifact['templateRepository'] registry_name = template_artifact['registry'] - registry_dict = openYaml( - Path(f"{getenv_with_error('CI_PROJECT_DIR')}/configuration/registry.yml")) # another registry model registry = registry_dict[registry_name] repo_url = registry.get(repo_type) dd_repo_url = registry.get(dd_repo_type) - cred_config = render_creds() repository_username = fetch_cred_value(registry.get("username"), cred_config) repository_password = fetch_cred_value(registry.get("password"), cred_config) cred = Credentials(username=repository_username, password=repository_password) @@ -157,27 +153,56 @@ def resolve_artifact_old_logic(env_definition: dict, template_dest: str) -> str: if "-SNAPSHOT" in dd_version: resolved_version = extract_snapshot_version(template_url, dd_version) logger.info(f"Environment template url has been resolved: {template_url}") - artifact.download(template_url, ARTIFACT_DEST, cred) - unpack_archive(ARTIFACT_DEST, template_dest) + artifact_dest = tempfile.mkstemp(suffix='.zip')[1] + artifact.download(template_url, artifact_dest, cred) + unpack_archive(artifact_dest, template_dest) return resolved_version -def process_env_template() -> str: +def process_env_template() -> dict: env_template_test = os.getenv("ENV_TEMPLATE_TEST", "").lower() == "true" if env_template_test: run_env_test_setup() + + env_definition = getEnvDefinition() + + appvers = { + NamespaceRole.COMMON: parse_artifact_appver(env_definition, 'envTemplate.artifact'), + NamespaceRole.ORIGIN: parse_artifact_appver(env_definition, 'envTemplate.bgNsArtifacts.origin'), + NamespaceRole.PEER: parse_artifact_appver(env_definition, 'envTemplate.bgNsArtifacts.peer'), + } + + tasks = {} project_dir = getenv_with_error('CI_PROJECT_DIR') - template_dest = f"{project_dir}/tmp" - cluster = getenv_with_error("CLUSTER_NAME") - environment = getenv_with_error("ENVIRONMENT_NAME") - env_dir = Path(f"{project_dir}/environments/{cluster}/{environment}") - env_definition = getEnvDefinition(env_dir) + cred_config = render_creds() - check_dir_exist_and_create(template_dest) + for template_type, appver in appvers.items(): + if template_type == NamespaceRole.COMMON: + template_dest = f'{project_dir}/tmp' + else: + template_dest = f'{project_dir}/tmp/{template_type}' - if 'artifact' in env_definition.get('envTemplate', {}): - logger.info("Use template resolving new logic") - return resolve_artifact_new_logic(env_definition, template_dest) - else: - logger.info("Use template resolving old logic") - return resolve_artifact_old_logic(env_definition, template_dest) + if not (len(appver) >= 2 and bool(appver[0]) and bool(appver[1])): + if template_type != NamespaceRole.COMMON: + continue + registry_dict = openYaml(Path(f"{project_dir}/configuration/registry.yml")) + + logger.info('Using template resolving old logic') + tasks[template_type] = resolve_artifact_old_logic(env_definition, template_dest, cred_config, registry_dict) + continue + + app_name, app_version = appver[0], appver[1] + artifact_path = getAppDefinitionPath(project_dir, app_name) + if not artifact_path: + raise FileNotFoundError(f"No artifact definition file found for {app_name}") + app_def = Application.model_validate(openYaml(artifact_path)) + cred = get_registry_creds(app_def.registry, cred_config) + + logger.info(f'Use template resolving new logic for {appver}') + tasks[template_type] = resolve_artifact_new_logic(app_def, app_version, template_dest, cred) + + async def resolve_all(): + results = await asyncio.gather(*tasks.values()) + return dict(zip(tasks.keys(), results)) + + return asyncio.run(resolve_all()) diff --git a/scripts/build_env/main.py b/scripts/build_env/main.py index 8205d9441..fb12545a0 100644 --- a/scripts/build_env/main.py +++ b/scripts/build_env/main.py @@ -17,7 +17,7 @@ ENV_SPECIFIC_RESOURCE_PROFILE_SCHEMA = "schemas/resource-profile.schema.json" -def prepare_folders_for_rendering(env_name, cluster_name, source_env_dir, templates_dir, render_dir, +def prepare_folders_for_rendering(env_name, cluster_name, source_env_dir, templates_dirs, render_dir, render_parameters_dir, render_profiles_dir, output_dir): # clearing folders delete_dir(render_dir) @@ -28,8 +28,14 @@ def prepare_folders_for_rendering(env_name, cluster_name, source_env_dir, templa # clearing instances dir cleanup_resulting_dir(Path(output_dir) / cluster_name / env_name) # copying parameters from templates and instances - check_dir_exist_and_create(f'{render_parameters_dir}/from_template') - copy_path(f'{templates_dir}/parameters', f'{render_parameters_dir}/from_template') + for template_type, template_path in templates_dirs.items(): + if not (template_path and check_dir_exists(f'{template_path}/parameters')): + continue + if template_type == NamespaceRole.COMMON: + param_dir_name = 'from_template' + else: + param_dir_name = f'from_{template_type}_template' + copy_path(f'{template_path}/parameters', f'{render_parameters_dir}/{param_dir_name}') cluster_path = getDirName(source_env_dir) instances_dir = getDirName(cluster_path) check_dir_exist_and_create(f'{render_parameters_dir}/from_instance') @@ -37,7 +43,7 @@ def prepare_folders_for_rendering(env_name, cluster_name, source_env_dir, templa copy_path(f'{cluster_path}/parameters', render_parameters_dir) copy_path(f'{source_env_dir}/{INVENTORY_DIR_NAME}/parameters', f'{render_parameters_dir}/from_instance') # copying all template resource profiles - copy_path(f'{templates_dir}/resource_profiles', render_profiles_dir) + copy_path(f'{templates_dirs[NamespaceRole.COMMON]}/resource_profiles', render_profiles_dir) return render_env_dir @@ -95,7 +101,7 @@ def handle_template_override(render_dir): deleteFile(file) -def build_environment(env_name, cluster_name, templates_dir, source_env_dir, all_instances_dir, output_dir, work_dir): +def build_environment(env_name, cluster_name, templates_dirs, source_env_dir, all_instances_dir, output_dir, work_dir): # defining folders that will be used during generation base_dir = getenv_with_error('CI_PROJECT_DIR') render_dir = f"{base_dir}/tmp/render" @@ -108,7 +114,7 @@ def build_environment(env_name, cluster_name, templates_dir, source_env_dir, all shutil.copytree(get_namespaces_path(), os.path.join(work_dir,'build_env','tmp','initial_namespaces_content','Namespaces'), dirs_exist_ok=True) # preparing folders for generation - render_env_dir = prepare_folders_for_rendering(env_name, cluster_name, source_env_dir, templates_dir, render_dir, + render_env_dir = prepare_folders_for_rendering(env_name, cluster_name, source_env_dir, templates_dirs, render_dir, render_parameters_dir, render_profiles_dir, output_dir) pre_process_env_before_rendering(render_env_dir, source_env_dir, all_instances_dir) # get deployer parameters @@ -160,7 +166,8 @@ def build_environment(env_name, cluster_name, templates_dir, source_env_dir, all envvars["env"] = env_name # Keep as string for file paths envvars["current_env"] = current_env # Object for Jinja2 templates that need current_env.environmentName envvars["cluster_name"] = cluster_name - envvars["templates_dir"] = templates_dir + envvars["templates_dirs"] = templates_dirs + envvars["templates_dir"] = templates_dirs.get(NamespaceRole.COMMON, '') envvars["env_instances_dir"] = getAbsPath(render_env_dir) envvars["render_dir"] = getAbsPath(render_dir) envvars["render_parameters_dir"] = getAbsPath(render_parameters_dir) @@ -174,7 +181,7 @@ def build_environment(env_name, cluster_name, templates_dir, source_env_dir, all env_specific_resource_profile_map = get_env_specific_resource_profiles(source_env_dir, all_instances_dir, ENV_SPECIFIC_RESOURCE_PROFILE_SCHEMA) build_env(env_name, source_env_dir, render_parameters_dir, render_dir, render_profiles_dir, - env_specific_resource_profile_map, all_instances_dir, render_context) + env_specific_resource_profile_map, all_instances_dir, render_context, templates_dirs) resulting_dir = post_process_env_after_rendering(env_name, render_env_dir, source_env_dir, all_instances_dir, output_dir) @@ -263,22 +270,23 @@ def validate_parameter_files(param_files): return errors -def render_environment(env_name, cluster_name, templates_dir, all_instances_dir, output_dir, work_dir): +def render_environment(env_name, cluster_name, templates_dirs, all_instances_dir, output_dir, work_dir): logger.info(f'env: {env_name}') logger.info(f'cluster_name: {cluster_name}') - logger.info(f'templates_dir: {templates_dir}') + logger.info(f'templates_dirs: {templates_dirs}') logger.info(f'instances_dir: {all_instances_dir}') logger.info(f'output_dir: {output_dir}') logger.info(f'work_dir: {work_dir}') check_environment_is_valid_or_fail(env_name, cluster_name, all_instances_dir, validate_env_definition_by_schema=True) - # searching for env directory in instances - validate_parameters(templates_dir, all_instances_dir, cluster_name, env_name) + for _, template_dir in templates_dirs.items(): + if template_dir: + validate_parameters(template_dir, all_instances_dir, cluster_name, env_name) env_dir = get_env_instances_dir(env_name, cluster_name, all_instances_dir) logger.info(f"Environment {env_name} directory is {env_dir}") - resulting_env_dir = build_environment(env_name, cluster_name, templates_dir, env_dir, all_instances_dir, + resulting_env_dir = build_environment(env_name, cluster_name, templates_dirs, env_dir, all_instances_dir, output_dir, work_dir) create_credentials(resulting_env_dir, env_dir, all_instances_dir) apply_ns_build_filter() @@ -288,11 +296,11 @@ def render_environment(env_name, cluster_name, templates_dir, all_instances_dir, base_dir = getenv_with_error('CI_PROJECT_DIR') cluster = getenv_with_error("CLUSTER_NAME") environment = getenv_with_error("ENVIRONMENT_NAME") - g_templates_dir = f"{base_dir}/tmp/templates" + g_template_dirs = get_template_dirs() g_all_instances_dir = f"{base_dir}/environments" g_output_dir = f"{base_dir}/environments" g_work_dir = get_parent_dir_for_dir(g_all_instances_dir) - + decrypt_all_cred_files_for_env() - render_environment(environment, cluster, g_templates_dir, g_all_instances_dir, g_output_dir, g_work_dir) + render_environment(environment, cluster, g_template_dirs, g_all_instances_dir, g_output_dir, g_work_dir) encrypt_all_cred_files_for_env() diff --git a/scripts/build_env/render_config_env.py b/scripts/build_env/render_config_env.py index cd0a36bb2..034b85946 100644 --- a/scripts/build_env/render_config_env.py +++ b/scripts/build_env/render_config_env.py @@ -5,7 +5,7 @@ from deepmerge import always_merger from envgenehelper import * -from envgenehelper.business_helper import get_bgd_object, get_namespaces +from envgenehelper.business_helper import get_bgd_object, get_namespaces, get_namespace_role, NamespaceRole from envgenehelper.validation import ensure_valid_fields, ensure_required_keys from jinja2 import Template, TemplateError from pydantic import BaseModel, Field @@ -23,13 +23,15 @@ class Context(BaseModel): env: Optional[str] = '' render_dir: Optional[str] = '' cloud_passport: OrderedDict = Field(default_factory=OrderedDict) - templates_dir: Optional[Path] = None + templates_dirs: dict = Field(default_factory=dict) output_dir: Optional[str] = '' cluster_name: Optional[str] = '' env_definition: OrderedDict = Field(default_factory=OrderedDict) current_env: OrderedDict = Field(default_factory=OrderedDict) current_env_dir: Optional[str] = '' current_env_template: OrderedDict = Field(default_factory=OrderedDict) + peer_env_template: OrderedDict = Field(default_factory=OrderedDict) + origin_env_template: OrderedDict = Field(default_factory=OrderedDict) tenant: Optional[str] = '' env_template: OrderedDict = Field(default_factory=OrderedDict) env_instances_dir: Optional[str] = '' @@ -110,20 +112,16 @@ def generate_config(self): logger.info(f"config = {config}") self.ctx.config = config - def set_env_template(self): + def find_env_template_in_dir(self, template_dir, env_template_name): + if not template_dir: + return None, [] extensions = ("yml.j2", "yaml.j2", "yml", "yaml") - env_templates_dir = Path(f'{self.ctx.templates_dir}/env_templates') - env_template_basename = env_templates_dir / self.ctx.current_env["env_template"] + env_templates_dir = Path(f'{template_dir}/env_templates') + env_template_basename = env_templates_dir / env_template_name suitable_files = find_files_by_basename(env_template_basename, extensions) env_template_path = suitable_files[0] if suitable_files else None if not env_template_path: - all_files = [f for f in env_templates_dir.iterdir() if f.is_file()] - remains_files = list(set(all_files) - set(suitable_files)) - raise ValueError( - f"Template descriptor not found: {env_template_basename}." - f" Expected location in template repository: {env_template_path}." - f" Allowed extensions: 'yml', 'yaml', 'yml.j2', 'yaml.j2'." - f" Found templates: {remains_files}") + return None, suitable_files env_tmpl_final_path = env_template_path if env_template_path.suffix.endswith("j2"): @@ -133,9 +131,35 @@ def set_env_template(self): validate_yaml_by_scheme_or_fail(env_tmpl_final_path, TD_SCHEMA) env_template = openYaml(filePath=env_tmpl_final_path, safe_load=True) + logger.info(f"Loaded env_template from {env_tmpl_final_path}") + return env_template, suitable_files + + def set_env_templates(self): + env_template_name = self.ctx.current_env["env_template"] + + env_templates_dir = Path(f'{self.ctx.templates_dirs.get(NamespaceRole.COMMON)}/env_templates') + env_template_basename = env_templates_dir / env_template_name + env_template, suitable_files = self.find_env_template_in_dir(self.ctx.templates_dirs.get(NamespaceRole.COMMON), env_template_name) + if not env_template: + all_files = [f for f in env_templates_dir.iterdir() if f.is_file()] + remains_files = list(set(all_files) - set(suitable_files)) + raise ValueError( + f"Template descriptor not found: {env_template_basename}." + f" Expected location in template repository: {env_template_basename}" + f" Allowed extensions: 'yml', 'yaml', 'yml.j2', 'yaml.j2'." + f" Found templates: {remains_files}") logger.info(f"env_template = {env_template}") self.ctx.current_env_template = env_template + if self.ctx.templates_dirs: + peer_template, _ = self.find_env_template_in_dir(self.ctx.templates_dirs.get(NamespaceRole.PEER), env_template_name) + if peer_template: + self.ctx.peer_env_template = peer_template + + origin_template, _ = self.find_env_template_in_dir(self.ctx.templates_dirs.get(NamespaceRole.ORIGIN), env_template_name) + if origin_template: + self.ctx.origin_env_template = origin_template + def setup_base_context(self, extra_env: dict): all_vars = dict(os.environ) self.ctx.update(extra_env) @@ -181,33 +205,57 @@ def validate_bgd(self): raise ValueError(f'Next namespaces were not found in available namespaces: {mismatch}') logger.info('Validation was successful') - def generate_ns_postfix(self, ns, ns_template_path, override_template_ns_name: str | None = None) -> str: - deploy_postfix = ns.get("deploy_postfix") - if deploy_postfix: - ns_template_name = deploy_postfix - else: - # get base name(deploy postfix) without extensions - ns_template_name = self.get_template_name(ns_template_path) - - ns_name = None - if override_template_ns_name: - ns_name = override_template_ns_name - elif ns_template_path: - rendered_ns = self.render_from_file_to_obj(ns_template_path) - ns_name = rendered_ns.get("name") - bgd = get_bgd_object(Path(f'{self.ctx.current_env_dir}')) - logger.debug(f'bgd object before comparing with ns: {bgd}') - if not bgd: - return ns_template_name - - origin_name = bgd["originNamespace"]["name"] - peer_name = bgd["peerNamespace"]["name"] - if ns_name == origin_name: - ns_template_name += "-origin" - elif ns_name == peer_name: - ns_template_name += "-peer" - logger.debug(f'After appending the ns name : {ns_template_name}') - return ns_template_name + def _get_bgd_suffix(self, ns_name: str | None) -> str: + if not ns_name: + return "" + bgd = get_bgd_object(Path(self.ctx.current_env_dir)) + role = get_namespace_role(ns_name, bgd) + return {NamespaceRole.ORIGIN: "-origin", NamespaceRole.PEER: "-peer"}.get(role, "") + + def _get_ns_name_for_bgd(self, ns: dict, ns_template_path: str) -> str | None: + override_name = self._fetch_template_override_name(ns) + if override_name: + return override_name + if ns_template_path: + return self.render_from_file_to_obj(ns_template_path).get("name") + return None + + def _get_template_dir_for_role(self, role: NamespaceRole) -> Path: + if role == NamespaceRole.ORIGIN and self.ctx.templates_dirs.get(NamespaceRole.ORIGIN): + return Path(self.ctx.templates_dirs[NamespaceRole.ORIGIN]) + if role == NamespaceRole.PEER and self.ctx.templates_dirs.get(NamespaceRole.PEER): + return Path(self.ctx.templates_dirs[NamespaceRole.PEER]) + return self.ctx.templates_dirs[NamespaceRole.COMMON] + + def _get_env_template_for_role(self, role: NamespaceRole) -> dict: + if role == NamespaceRole.ORIGIN and self.ctx.origin_env_template: + return self.ctx.origin_env_template + if role == NamespaceRole.PEER and self.ctx.peer_env_template: + return self.ctx.peer_env_template + return self.ctx.current_env_template + + def _resolve_template_path(self, template_path_expr: str, templates_dir: Path = None) -> str: + ctx = self.ctx.as_dict() + if templates_dir is not None: + ctx["templates_dir"] = str(templates_dir) + return Template(template_path_expr).render(ctx) + + def _find_ns_config_by_name(self, env_template: dict, ns_name: str, templates_dir: Path = None) -> dict | None: + for ns in env_template.get("namespaces", []): + override_name = self._fetch_template_override_name(ns) + if override_name == ns_name: + return ns + ns_template_path = self._resolve_template_path(ns["template_path"], templates_dir) + if ns_template_path: + rendered = self.render_from_file_to_obj(ns_template_path) + if rendered.get("name") == ns_name: + return ns + return None + + def generate_ns_postfix(self, ns: dict, ns_template_path: str) -> str: + base_name = ns.get("deploy_postfix") or self.get_template_name(ns_template_path) + ns_name = self._get_ns_name_for_bgd(ns, ns_template_path) + return base_name + self._get_bgd_suffix(ns_name) def generate_solution_structure(self): sd_basename = f'{self.ctx.current_env_dir}/Inventory/solution-descriptor/sd' @@ -310,30 +358,43 @@ def generate_bgd_file(self): return self.render_from_file_to_file(Template(template).render(self.ctx.as_dict()), target_path) - def fetch_template_override_name(self, ns) -> str: - override_namespace_content = ns.get("template_override") - if override_namespace_content: - rendered = create_jinja_env().from_string(str(override_namespace_content)).render(self.ctx.as_dict()) - if rendered: - template_name = readYaml(rendered) - return template_name.get("name") - return "" + def _fetch_template_override_name(self, ns: dict) -> str: + template_override = ns.get("template_override") + if not template_override: + return "" + rendered = create_jinja_env().from_string(str(template_override)).render(self.ctx.as_dict()) + if not rendered: + return "" + return readYaml(rendered).get("name", "") - def generate_namespace_file(self): + def generate_namespace_files(self): context = self.ctx.as_dict() - namespaces = self.ctx.current_env_template["namespaces"] - for ns in namespaces: + bgd = get_bgd_object(Path(self.ctx.current_env_dir)) + + for ns in self.ctx.current_env_template["namespaces"]: ns_template_path = Template(ns["template_path"]).render(context) - override_template_ns_name = self.fetch_template_override_name(ns) - deploy_postfx = self.generate_ns_postfix(ns, ns_template_path, override_template_ns_name) - logger.info(f"Generate Namespace yaml for {deploy_postfx}") - current_env_dir = self.ctx.current_env_dir - ns_dir = f'{current_env_dir}/Namespaces/{deploy_postfx}' - namespace_file = f'{ns_dir}/namespace.yml' - self.render_from_file_to_file(ns_template_path, namespace_file) + postfix = self.generate_ns_postfix(ns, ns_template_path) + + ns_name = self._get_ns_name_for_bgd(ns, ns_template_path) + role = get_namespace_role(ns_name, bgd) if ns_name else NamespaceRole.COMMON + + role_templates_dir = self._get_template_dir_for_role(role) + role_env_template = self._get_env_template_for_role(role) + + effective_ns = ns + effective_template_path = ns_template_path + + if role != NamespaceRole.COMMON and role_env_template is not self.ctx.current_env_template: + role_ns_config = self._find_ns_config_by_name(role_env_template, ns_name, role_templates_dir) + if role_ns_config: + effective_ns = role_ns_config + effective_template_path = self._resolve_template_path(role_ns_config["template_path"], role_templates_dir) + logger.info(f"Using {role.name} template for namespace {ns_name}") - self.generate_override_template(ns.get("template_override"), Path(f'{ns_dir}/namespace.yml_override'), - deploy_postfx) + logger.info(f"Generate Namespace yaml for {postfix}") + ns_dir = Path(self.ctx.current_env_dir) / "Namespaces" / postfix + self.render_from_file_to_file(effective_template_path, str(ns_dir / "namespace.yml")) + self.generate_override_template(effective_ns.get("template_override"), ns_dir / "namespace.yml_override", postfix) def calculate_cloud_name(self) -> str: inv = self.ctx.env_definition["inventory"] @@ -521,7 +582,7 @@ def process_app_reg_defs(self, env_name: str, extra_env: dict): self.setup_base_context(extra_env) current_env_dir = self.ctx.current_env_dir - templates_dir = self.ctx.templates_dir + templates_dir = self.ctx.templates_dirs[NamespaceRole.COMMON] patterns = ["*.yaml.j2", "*.yml.j2", "*.j2", "*.yaml", "*.yml"] appdef_templates = self.find_templates(f"{templates_dir}/appdefs", patterns) regdef_templates = self.find_templates(f"{templates_dir}/regdefs", patterns) @@ -567,13 +628,13 @@ def render_config_env(self, env_name: str, extra_env: dict): current_env_dir = f'{self.ctx.render_dir}/{self.ctx.env}' self.ctx.current_env_dir = current_env_dir - self.set_env_template() + self.set_env_templates() self.generate_bgd_file() self.generate_solution_structure() self.generate_tenant_file() self.generate_cloud_file() - self.generate_namespace_file() + self.generate_namespace_files() self.generate_composite_structure() env_specific_schema = self.ctx.current_env_template.get("envSpecificSchema") diff --git a/scripts/build_env/tests/app_reg_defs/test_appregdefs.py b/scripts/build_env/tests/app_reg_defs/test_appregdefs.py index 2d77b3cd9..e3df35a57 100644 --- a/scripts/build_env/tests/app_reg_defs/test_appregdefs.py +++ b/scripts/build_env/tests/app_reg_defs/test_appregdefs.py @@ -7,6 +7,7 @@ from render_config_env import EnvGenerator from envgenehelper.test_helpers import TestHelpers +from envgenehelper.business_helper import NamespaceRole class TestAppRegDefRendering: @@ -14,20 +15,20 @@ class TestAppRegDefRendering: @pytest.fixture(scope="class", autouse=True) def setup_test_environment(self, request): cls = request.cls - + test_base = Path(__file__).parents[4] / "test_data" / "test_app_reg_defs" cls.test_data_dir = test_base cls.cluster_name = "cluster-01" cls.env_name = "env-01" - + cls.output_dir = Path("/tmp/appregdefs") cls.output_dir.mkdir(parents=True, exist_ok=True) - + os.environ["CLUSTER_NAME"] = cls.cluster_name os.environ["ENVIRONMENT_NAME"] = cls.env_name - + yield - + os.environ.pop("CLUSTER_NAME", None) os.environ.pop("ENVIRONMENT_NAME", None) @@ -43,17 +44,19 @@ def _setup_render_dir(self) -> Path: def _get_render_context(self, test_number: str) -> dict: render_dir = self.output_dir / "render" / self.env_name - + test_case_dir = self._get_test_case_dir(test_number) - + env_dir = test_case_dir / "environments" / self.cluster_name / self.env_name templates_dir = test_case_dir / "templates" - + templates_dirs = {'common': str(templates_dir)} + return { "cluster_name": self.cluster_name, "output_dir": str(test_case_dir / "environments"), "current_env_dir": str(render_dir), "templates_dir": str(templates_dir), + "templates_dirs": templates_dirs, "cloud_passport_file_path": "", "env_instances_dir": str(env_dir) } @@ -63,11 +66,11 @@ def _verify_rendered_files(self, test_number: str, render_dir: Path): test_case_dir = self._get_test_case_dir(test_number) expected_appdefs = test_case_dir / "expected" / "appdefs" expected_regdefs = test_case_dir / "expected" / "regdefs" - + TestHelpers.assert_dirs_content(expected_appdefs, render_dir / "AppDefs") TestHelpers.assert_dirs_content(expected_regdefs, render_dir / "RegDefs") - - POSITIVE_CASES = [ + + POSITIVE_CASES = [ "TC-001-001", "TC-001-002", "TC-001-003", @@ -77,29 +80,29 @@ def _verify_rendered_files(self, test_number: str, render_dir: Path): "TC-001-008", ] - @pytest.mark.parametrize("test_number", POSITIVE_CASES) + @pytest.mark.parametrize("test_number", POSITIVE_CASES) def test_positive_basic_appdef_rendering(self, test_number): self._setup_render_dir() - + render_context = EnvGenerator() context_vars = self._get_render_context(test_number) render_context.process_app_reg_defs(self.env_name, context_vars) - + render_dir = Path(context_vars["current_env_dir"]) self._verify_rendered_files(test_number, render_dir) - + NEGATIVE_CASES = { "TC-001-010": TemplateSyntaxError, "TC-001-011": ValueError, "TC-001-012": ValueError, } - + @pytest.mark.parametrize("test_number,expected_exception", NEGATIVE_CASES.items()) def test_negative_appregdef_rendering(self, test_number, expected_exception): self._setup_render_dir() - + render_context = EnvGenerator() context_vars = self._get_render_context(test_number) with pytest.raises(expected_exception): render_context.process_app_reg_defs(self.env_name, context_vars) - \ No newline at end of file + diff --git a/scripts/build_env/tests/env-build/test_paramset_sorting.py b/scripts/build_env/tests/env-build/test_paramset_sorting.py index 7c0552d65..0099ccb50 100644 --- a/scripts/build_env/tests/env-build/test_paramset_sorting.py +++ b/scripts/build_env/tests/env-build/test_paramset_sorting.py @@ -1,4 +1,4 @@ -import pytest +from envgenehelper.business_helper import is_from_template_dir class TestSortParamsetsWithSameName: @@ -7,11 +7,12 @@ class TestSortParamsetsWithSameName: def sort_paramsets_with_same_name(entries: list[dict]) -> list[dict]: def sort_key(e): path = e["filePath"] - if "from_template" in path: - return 0, path - elif "from_instance" in path: + if "from_instance" in path: return 2, path + elif is_from_template_dir(path): + return 0, path return 1, path + return sorted(entries, key=sort_key) def test_all_three_levels(self): @@ -34,6 +35,19 @@ def test_template_and_instance(self): assert "from_template" in sorted_entries[0]["filePath"] assert "from_instance" in sorted_entries[1]["filePath"] + def test_origin_peer_templates(self): + entries = [ + {"filePath": "/tmp/render/parameters/from_instance/test.yml", "envSpecific": True}, + {"filePath": "/tmp/render/parameters/from_template/test.yml", "envSpecific": False}, + {"filePath": "/tmp/render/parameters/from_peer_template/test.yml", "envSpecific": False}, + {"filePath": "/tmp/render/parameters/from_origin_template/test.yml", "envSpecific": False}, + ] + sorted_entries = self.sort_paramsets_with_same_name(entries) + assert "from_origin_template" in sorted_entries[0]["filePath"] + assert "from_peer_template" in sorted_entries[1]["filePath"] + assert "from_template" in sorted_entries[2]["filePath"] + assert "from_instance" in sorted_entries[3]["filePath"] + def test_multiple_files_sorted_alphabetically(self): entries = [ {"filePath": "/tmp/render/parameters/from_template/z_params.yml", "envSpecific": False}, diff --git a/scripts/build_env/tests/env-build/test_render_envs.py b/scripts/build_env/tests/env-build/test_render_envs.py index a01f2ac6c..6f165b78a 100644 --- a/scripts/build_env/tests/env-build/test_render_envs.py +++ b/scripts/build_env/tests/env-build/test_render_envs.py @@ -2,23 +2,25 @@ import pytest from envgenehelper import * +from envgenehelper.business_helper import NamespaceRole -from main import render_environment, cleanup_resulting_dir +from main import render_environment from envgenehelper.test_helpers import TestHelpers from tests.base_test import BaseTest test_data = [ # (cluster_name, environment_name, template) - ("cluster-01", "env-01", "composite-prod"), - ("cluster-01", "env-02", "composite-dev"), - ("cluster-01", "env-03", "composite-dev"), - ("cluster-01", "env-04", "simple"), - ("cluster01", "env01", "test-01"), - ("cluster01", "env03", "test-template-1"), - ("cluster01", "env04", "test-template-2"), - ("bgd-cluster", "bgd-env", "bgd"), - ("cluster03", "rpo-replacement-mode", "simple"), + ("cluster-01", "env-01", "composite-prod", {}), + ("cluster-01", "env-02", "composite-dev", {}), + ("cluster-01", "env-03", "composite-dev", {}), + ("cluster-01", "env-04", "simple", {}), + ("cluster01", "env01", "test-01", {}), + ("cluster01", "env03", "test-template-1", {}), + ("cluster01", "env04", "test-template-2", {}), + ("bgd-cluster", "bgd-env", "bgd", {}), + ("bgd-cluster", "bgd-ns-artifacts-env", "bgd-ns-artifacts", {NamespaceRole.PEER: "test_data/test_templates_peer", NamespaceRole.ORIGIN: "test_data/test_templates_origin"}), + ("cluster03", "rpo-replacement-mode", "simple", {}), ] @@ -27,18 +29,23 @@ class TestEnvBuild(BaseTest): def change_test_dir(self, monkeypatch): monkeypatch.chdir(self.base_dir) - @pytest.mark.parametrize("cluster_name, env_name, version", test_data) - def test_render_envs(self, cluster_name, env_name, version): - g_templates_dir = str((self.test_data_dir / "test_templates").resolve()) + @pytest.mark.parametrize("cluster_name, env_name, version, extra_templates", test_data) + def test_render_envs(self, cluster_name, env_name, version, extra_templates): + g_templates_dirs = { + NamespaceRole.COMMON: str((self.test_data_dir / "test_templates").resolve()) + } + g_templates_dirs.update(extra_templates) + g_inventory_dir = str((self.test_data_dir / "test_environments").resolve()) g_output_dir = str((self.base_dir / "/tmp/test_environments").resolve()) os.environ['CI_COMMIT_REF_NAME'] = "branch_name" environ['FULL_ENV_NAME'] = cluster_name + '/' + env_name - render_environment(env_name, cluster_name, g_templates_dir, g_inventory_dir, g_output_dir, self.test_data_dir) + render_environment(env_name, cluster_name, g_templates_dirs, g_inventory_dir, g_output_dir, self.test_data_dir) source_dir = f"{g_inventory_dir}/{cluster_name}/{env_name}" generated_dir = f"{g_output_dir}/{cluster_name}/{env_name}" files_to_compare = get_all_files_in_dir(source_dir) logger.info(dump_as_yaml_format(files_to_compare)) TestHelpers.assert_dirs_content(source_dir, generated_dir, True, False) + diff --git a/scripts/build_env/tests/env-template/test_env_template.py b/scripts/build_env/tests/env-template/test_env_template.py index 1d0256351..c6425aec4 100644 --- a/scripts/build_env/tests/env-template/test_env_template.py +++ b/scripts/build_env/tests/env-template/test_env_template.py @@ -1,4 +1,4 @@ -from os import environ +from os import environ, getenv from pathlib import Path import pytest @@ -97,6 +97,7 @@ def mock_aio_response(): def set_env(name: str): environ["ENVIRONMENT_NAME"] = name + environ["FULL_ENV_NAME"] = f"{getenv("CLUSTER_NAME")}/{getenv("ENVIRONMENT_NAME")}" def mock_metadata(aio_mock, url=METADATA_URL, repeat=1): @@ -147,6 +148,7 @@ def init_env(self): environ.pop("CI_PROJECT_DIR", None) environ.pop("CLUSTER_NAME", None) environ.pop("ENVIRONMENT_NAME", None) + environ.pop("FULL_ENV_NAME", None) @responses.activate def test_new_logic_with_dd(self, mock_aio_response): diff --git a/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/Credentials/credentials.yml b/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/Credentials/credentials.yml new file mode 100644 index 000000000..d66d4ef15 --- /dev/null +++ b/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/Credentials/credentials.yml @@ -0,0 +1,22 @@ +bgdomaincred: # bg domain bgd-ns-artifacts-env-bg-domain + type: "secret" + data: + secret: "envgeneNullValue" # FillMe +cloud-deploy-sa-token: # cloud passport: test-cloud-passport version: 1.5 + type: "secret" + data: + secret: "token-placeholder-123" +consul-token: # cloud test-solution-structure + type: "secret" + data: + secret: "envgeneNullValue" # FillMe +dbaas: # cloud test-solution-structure + type: "usernamePassword" + data: + username: "envgeneNullValue" # FillMe + password: "envgeneNullValue" # FillMe +maas: # cloud test-solution-structure + type: "usernamePassword" + data: + username: "envgeneNullValue" # FillMe + password: "envgeneNullValue" # FillMe diff --git a/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/Inventory/env_definition.yml b/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/Inventory/env_definition.yml new file mode 100644 index 000000000..2113c553e --- /dev/null +++ b/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/Inventory/env_definition.yml @@ -0,0 +1,20 @@ +inventory: + environmentName: "bgd-ns-artifacts-env" + clusterUrl: "test-val.com" + tenantName: "test-tenant" + deployer: "test-deployer" + cloudName: "test-solution-structure" + cloudPassport: "test-cloud-passport" +envTemplate: + name: "bgd-ns-artifacts" + bgNsArtifacts: + origin: "bgd-ns-artifacts:bgd-ns-artifacts" + peer: "bgd-ns-artifacts:bgd-ns-artifacts" + additionalTemplateVariables: {} + sharedTemplateVariables: [] + envSpecificParamsets: {} + envSpecificTechnicalParamsets: {} + sharedMasterCredentialFiles: [] + artifact: "bgd-ns-artifacts:bgd-ns-artifacts" +generatedVersions: + generateEnvironmentLatestVersion: "bgd-ns-artifacts:bgd-ns-artifacts" diff --git a/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/Namespaces/app-origin/namespace.yml b/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/Namespaces/app-origin/namespace.yml new file mode 100644 index 000000000..2d3a68533 --- /dev/null +++ b/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/Namespaces/app-origin/namespace.yml @@ -0,0 +1,22 @@ +# The contents of this file is generated from template artifact: bgd-ns-artifacts. +# Contents will be overwritten by next generation. +# Please modify this contents only for development purposes or as workaround. +name: "bgd-ns-artifacts-env-origin-app" +credentialsId: "" +isServerSideMerge: false +labels: + - "Instance-bgd-ns-artifacts-env" +cleanInstallApprovalRequired: false +mergeDeployParametersAndE2EParameters: false +deployParameters: + APP_NAMESPACE: "bgd-ns-artifacts-env-app" + ENVGENE_CONFIG_REF_NAME: "branch_name" + ENVGENE_CONFIG_TAG: "No Ref tag" + ROLE_SPECIFIC_PARAM: "ORIGIN" +e2eParameters: + APP_NAMESPACE: "bgd-ns-artifacts-env-app" +technicalConfigurationParameters: + APP_NAMESPACE: "bgd-ns-artifacts-env-app" +deployParameterSets: [] +e2eParameterSets: [] +technicalConfigurationParameterSets: [] diff --git a/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/Namespaces/app-peer/namespace.yml b/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/Namespaces/app-peer/namespace.yml new file mode 100644 index 000000000..5d4e25e4f --- /dev/null +++ b/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/Namespaces/app-peer/namespace.yml @@ -0,0 +1,23 @@ +# The contents of this file is generated from template artifact: bgd-ns-artifacts. +# Contents will be overwritten by next generation. +# Please modify this contents only for development purposes or as workaround. +name: "bgd-ns-artifacts-env-peer-app" +credentialsId: "" +isServerSideMerge: false +labels: + - "Instance-bgd-ns-artifacts-env" +cleanInstallApprovalRequired: false +mergeDeployParametersAndE2EParameters: false +deployParameters: + APP_NAMESPACE: "bgd-ns-artifacts-env-app" + ENVGENE_CONFIG_REF_NAME: "branch_name" + ENVGENE_CONFIG_TAG: "No Ref tag" + ROLE_SPECIFIC_PARAM: "PEER" + param-A: "value from peer template version of paramset" # paramset: paramset-A version: n/a source: template +e2eParameters: + APP_NAMESPACE: "bgd-ns-artifacts-env-app" +technicalConfigurationParameters: + APP_NAMESPACE: "bgd-ns-artifacts-env-app" +deployParameterSets: [] +e2eParameterSets: [] +technicalConfigurationParameterSets: [] diff --git a/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/Namespaces/bg-controller/namespace.yml b/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/Namespaces/bg-controller/namespace.yml new file mode 100644 index 000000000..b691aa2d4 --- /dev/null +++ b/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/Namespaces/bg-controller/namespace.yml @@ -0,0 +1,16 @@ +# The contents of this file is generated from template artifact: bgd-ns-artifacts. +# Contents will be overwritten by next generation. +# Please modify this contents only for development purposes or as workaround. +name: "bgd-ns-artifacts-env-bg-controller" +credentialsId: "" +isServerSideMerge: false +labels: + - "Instance-bgd-ns-artifacts-env" +cleanInstallApprovalRequired: false +mergeDeployParametersAndE2EParameters: false +deployParameters: {} +e2eParameters: {} +technicalConfigurationParameters: {} +deployParameterSets: [] +e2eParameterSets: [] +technicalConfigurationParameterSets: [] diff --git a/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/bg_domain.yml b/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/bg_domain.yml new file mode 100644 index 000000000..fb00a5539 --- /dev/null +++ b/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/bg_domain.yml @@ -0,0 +1,12 @@ +name: "bgd-ns-artifacts-env-bg-domain" +type: bgdomain +originNamespace: + name: "bgd-ns-artifacts-env-origin-app" + type: namespace +peerNamespace: + name: "bgd-ns-artifacts-env-peer-app" + type: namespace +controllerNamespace: + name: "bgd-ns-artifacts-env-bg-controller" + type: namespace + credentials: bgdomaincred diff --git a/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/cloud.yml b/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/cloud.yml new file mode 100644 index 000000000..980276436 --- /dev/null +++ b/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/cloud.yml @@ -0,0 +1,48 @@ +# The contents of this file is generated from template artifact: bgd-ns-artifacts. +# Contents will be overwritten by next generation. +# Please modify this contents only for development purposes or as workaround. +name: "test-solution-structure" +apiUrl: "tmp.cloud.com" # cloud passport: test-cloud-passport version: 1.5 +apiPort: "0000" # cloud passport: test-cloud-passport version: 1.5 +privateUrl: "test-host.managed.tmp.cloud" # cloud passport: test-cloud-passport version: 1.5 +publicUrl: "test-host.managed.tmp.cloud" # cloud passport: test-cloud-passport version: 1.5 +dashboardUrl: "https://dashboard.test-host.managed.tmp.cloud" # cloud passport: test-cloud-passport version: 1.5 +labels: [] +defaultCredentialsId: "cloud-deploy-sa-token" # cloud passport: test-cloud-passport version: 1.5 +protocol: "https" # cloud passport: test-cloud-passport version: 1.5 +dbMode: "db" +databases: [] +maasConfig: + credentialsId: "maas" + enable: false + maasUrl: "http://maas.None" + maasInternalAddress: "http://maas.maas:8888" +vaultConfig: + credentialsId: "" + enable: false + url: "" +dbaasConfigs: + - credentialsId: "dbaas" + enable: false + apiUrl: "http://dbaas.dbaas:8888" + aggregatorUrl: "https://dbaas.None" +consulConfig: + tokenSecret: "consul-token" + enabled: false + publicUrl: "https://consul.None" + internalUrl: "http://consul.consul:8888" +deployParameters: + CLOUD_DASHBOARD_URL: "https://dashboard.test-host.managed.tmp.cloud" # cloud passport: test-cloud-passport version: 1.5 + CMDB_URL: "https://test-host.managed.tmp.cloud" # cloud passport: test-cloud-passport version: 1.5 + GRAFANA_UI_URL: "https://test-host.managed.tmp.cloud" # cloud passport: test-cloud-passport version: 1.5 + GRAYLOG_UI_URL: "https://test-host.managed.tmp.cloud" # cloud passport: test-cloud-passport version: 1.5 + TRACING_UI_URL: "https://test-host.managed.tmp.cloud" # cloud passport: test-cloud-passport version: 1.5 + VALUE: "true" # cloud passport: test-cloud-passport version: 1.5 +e2eParameters: + CMDB_NAME: "test-deployer" + TEMPLATE_NAME: "bgd-ns-artifacts" +technicalConfigurationParameters: {} +deployParameterSets: [] +e2eParameterSets: [] +technicalConfigurationParameterSets: [] +productionMode: false # cloud passport: test-cloud-passport version: 1.5 diff --git a/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/tenant.yml b/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/tenant.yml new file mode 100644 index 000000000..93914e1fe --- /dev/null +++ b/test_data/test_environments/bgd-cluster/bgd-ns-artifacts-env/tenant.yml @@ -0,0 +1,17 @@ +# The contents of this file is generated from template artifact: bgd-ns-artifacts. +# Contents will be overwritten by next generation. +# Please modify this contents only for development purposes or as workaround. +name: "test-tenant" +registryName: "default" +description: "" +owners: "" +gitRepository: "" +defaultBranch: "" +credential: "" +labels: [] +globalE2EParameters: + pipelineDefaultRecipients: "" + recipientsStrategy: "merge" + mergeTenantsAndE2EParameters: false + environmentParameters: {} +deployParameters: {} diff --git a/test_data/test_templates/env_templates/bgd-ns-artifacts.yaml b/test_data/test_templates/env_templates/bgd-ns-artifacts.yaml new file mode 100644 index 000000000..716cda109 --- /dev/null +++ b/test_data/test_templates/env_templates/bgd-ns-artifacts.yaml @@ -0,0 +1,14 @@ +--- +tenant: "{{ templates_dir }}/env_templates/bgd/tenant.yml.j2" +cloud: + template_path: "{{ templates_dir }}/env_templates/bgd/cloud.yml.j2" +namespaces: + - template_path: "{{ templates_dir }}/env_templates/bgd-ns-artifacts/Namespaces/app.yml.j2" + template_override: + name: "{{ current_env.name }}-peer-app" + - template_path: "{{ templates_dir }}/env_templates/bgd-ns-artifacts/Namespaces/app.yml.j2" + template_override: + name: "{{ current_env.name }}-origin-app" + - template_path: "{{ templates_dir }}/env_templates/bgd/Namespaces/bg-controller.yml.j2" +bg_domain: "{{ templates_dir }}/env_templates/bgd/bg_domain.yml.j2" +parametersets: [] diff --git a/test_data/test_templates/env_templates/bgd-ns-artifacts/Namespaces/app.yml.j2 b/test_data/test_templates/env_templates/bgd-ns-artifacts/Namespaces/app.yml.j2 new file mode 100644 index 000000000..9c1cf3a9e --- /dev/null +++ b/test_data/test_templates/env_templates/bgd-ns-artifacts/Namespaces/app.yml.j2 @@ -0,0 +1,20 @@ +--- +name: "{{current_env.name}}-bss" +labels: +- "Instance-{{current_env.name}}" +credentialsId: "" +isServerSideMerge: false +cleanInstallApprovalRequired: false +mergeDeployParametersAndE2EParameters: false +deployParameters: + ENVGENE_CONFIG_REF_NAME: "{{ lookup('ansible.builtin.env', 'CI_COMMIT_REF_NAME')| default('No Ref Name') }}" + ENVGENE_CONFIG_TAG: "{{ lookup('ansible.builtin.env', 'CI_COMMIT_TAG')| default('No Ref tag') }}" + APP_NAMESPACE: "{{current_env.name}}-app" + ROLE_SPECIFIC_PARAM: "COMMON" +e2eParameters: + APP_NAMESPACE: "{{current_env.name}}-app" +technicalConfigurationParameters: + APP_NAMESPACE: "{{current_env.name}}-app" +deployParameterSets: [] +e2eParameterSets: [] +technicalConfigurationParameterSets: [] diff --git a/test_data/test_templates_origin/env_templates/bgd-ns-artifacts.yaml b/test_data/test_templates_origin/env_templates/bgd-ns-artifacts.yaml new file mode 100644 index 000000000..716cda109 --- /dev/null +++ b/test_data/test_templates_origin/env_templates/bgd-ns-artifacts.yaml @@ -0,0 +1,14 @@ +--- +tenant: "{{ templates_dir }}/env_templates/bgd/tenant.yml.j2" +cloud: + template_path: "{{ templates_dir }}/env_templates/bgd/cloud.yml.j2" +namespaces: + - template_path: "{{ templates_dir }}/env_templates/bgd-ns-artifacts/Namespaces/app.yml.j2" + template_override: + name: "{{ current_env.name }}-peer-app" + - template_path: "{{ templates_dir }}/env_templates/bgd-ns-artifacts/Namespaces/app.yml.j2" + template_override: + name: "{{ current_env.name }}-origin-app" + - template_path: "{{ templates_dir }}/env_templates/bgd/Namespaces/bg-controller.yml.j2" +bg_domain: "{{ templates_dir }}/env_templates/bgd/bg_domain.yml.j2" +parametersets: [] diff --git a/test_data/test_templates_origin/env_templates/bgd-ns-artifacts/Namespaces/app.yml.j2 b/test_data/test_templates_origin/env_templates/bgd-ns-artifacts/Namespaces/app.yml.j2 new file mode 100644 index 000000000..7e950d1b0 --- /dev/null +++ b/test_data/test_templates_origin/env_templates/bgd-ns-artifacts/Namespaces/app.yml.j2 @@ -0,0 +1,20 @@ +--- +name: "{{current_env.name}}-bss" +labels: +- "Instance-{{current_env.name}}" +credentialsId: "" +isServerSideMerge: false +cleanInstallApprovalRequired: false +mergeDeployParametersAndE2EParameters: false +deployParameters: + ENVGENE_CONFIG_REF_NAME: "{{ lookup('ansible.builtin.env', 'CI_COMMIT_REF_NAME')| default('No Ref Name') }}" + ENVGENE_CONFIG_TAG: "{{ lookup('ansible.builtin.env', 'CI_COMMIT_TAG')| default('No Ref tag') }}" + APP_NAMESPACE: "{{current_env.name}}-app" + ROLE_SPECIFIC_PARAM: "ORIGIN" +e2eParameters: + APP_NAMESPACE: "{{current_env.name}}-app" +technicalConfigurationParameters: + APP_NAMESPACE: "{{current_env.name}}-app" +deployParameterSets: [] +e2eParameterSets: [] +technicalConfigurationParameterSets: [] diff --git a/test_data/test_templates_peer/env_templates/bgd-ns-artifacts.yaml b/test_data/test_templates_peer/env_templates/bgd-ns-artifacts.yaml new file mode 100644 index 000000000..716cda109 --- /dev/null +++ b/test_data/test_templates_peer/env_templates/bgd-ns-artifacts.yaml @@ -0,0 +1,14 @@ +--- +tenant: "{{ templates_dir }}/env_templates/bgd/tenant.yml.j2" +cloud: + template_path: "{{ templates_dir }}/env_templates/bgd/cloud.yml.j2" +namespaces: + - template_path: "{{ templates_dir }}/env_templates/bgd-ns-artifacts/Namespaces/app.yml.j2" + template_override: + name: "{{ current_env.name }}-peer-app" + - template_path: "{{ templates_dir }}/env_templates/bgd-ns-artifacts/Namespaces/app.yml.j2" + template_override: + name: "{{ current_env.name }}-origin-app" + - template_path: "{{ templates_dir }}/env_templates/bgd/Namespaces/bg-controller.yml.j2" +bg_domain: "{{ templates_dir }}/env_templates/bgd/bg_domain.yml.j2" +parametersets: [] diff --git a/test_data/test_templates_peer/env_templates/bgd-ns-artifacts/Namespaces/app.yml.j2 b/test_data/test_templates_peer/env_templates/bgd-ns-artifacts/Namespaces/app.yml.j2 new file mode 100644 index 000000000..1ac9e9c61 --- /dev/null +++ b/test_data/test_templates_peer/env_templates/bgd-ns-artifacts/Namespaces/app.yml.j2 @@ -0,0 +1,21 @@ +--- +name: "{{current_env.name}}-bss" +labels: +- "Instance-{{current_env.name}}" +credentialsId: "" +isServerSideMerge: false +cleanInstallApprovalRequired: false +mergeDeployParametersAndE2EParameters: false +deployParameters: + ENVGENE_CONFIG_REF_NAME: "{{ lookup('ansible.builtin.env', 'CI_COMMIT_REF_NAME')| default('No Ref Name') }}" + ENVGENE_CONFIG_TAG: "{{ lookup('ansible.builtin.env', 'CI_COMMIT_TAG')| default('No Ref tag') }}" + APP_NAMESPACE: "{{current_env.name}}-app" + ROLE_SPECIFIC_PARAM: "PEER" +e2eParameters: + APP_NAMESPACE: "{{current_env.name}}-app" +technicalConfigurationParameters: + APP_NAMESPACE: "{{current_env.name}}-app" +deployParameterSets: +- paramset-A +e2eParameterSets: [] +technicalConfigurationParameterSets: [] diff --git a/test_data/test_templates_peer/parameters/paramset-A.yaml b/test_data/test_templates_peer/parameters/paramset-A.yaml new file mode 100644 index 000000000..00b398d3b --- /dev/null +++ b/test_data/test_templates_peer/parameters/paramset-A.yaml @@ -0,0 +1,4 @@ +--- +name: "paramset-A" +parameters: + param-A: "value from peer template version of paramset" From 3866bf4dd70aec50495c99ca1a470c857032f83b Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Tue, 3 Mar 2026 07:46:25 +0000 Subject: [PATCH 052/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 3125031d8..6baa4f5cf 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.28.1" - DOCKER_IMAGE_TAG_ENVGENE: "1.28.1" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.28.1" + DOCKER_IMAGE_TAG_PIPEGENE: "1.29.0" + DOCKER_IMAGE_TAG_ENVGENE: "1.29.0" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.29.0" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 00815130b..2c2de7140 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.28.1 +version: 1.29.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 40df9530e..d34707900 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.28.1 +version: 1.29.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index c9358cc72..71dffe2c2 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.28.1", + "envgene_version": "1.29.0", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 21fcb2ebecdc45bbbc6c63261913227c2389f2e6 Mon Sep 17 00:00:00 2001 From: popoveugene <42543333+popoveugene@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:13:33 +0300 Subject: [PATCH 053/161] docs: add rp tutorial (#1063) * docs: add rp tutorial * docs: wip --- AGENTS.md | 127 +++++- README.md | 17 +- docs/README.md | 5 + docs/features/resource-profile.md | 8 +- docs/how-to/configure-resource-profiles.md | 46 +- docs/instance-pipeline-parameters.md | 14 +- docs/tutorials/resource-profiles.md | 463 +++++++++++++++++++++ 7 files changed, 620 insertions(+), 60 deletions(-) create mode 100644 docs/tutorials/resource-profiles.md diff --git a/AGENTS.md b/AGENTS.md index e2426c03f..d955ae96e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,6 +75,63 @@ Content... - Remove special characters - Example: `### Step 1: Install Tools` → `#step-1-install-tools` +#### Dashes + +**CRITICAL: Always use a regular hyphen-minus (`-`) as a dash in prose. Never use em dashes (`—`) or en dashes (`–`).** + +❌ **INCORRECT:** + +```markdown +EnvGene searches these locations — from bottom to top — and uses the first match. +``` + +✅ **CORRECT:** + +```markdown +EnvGene searches these locations - from bottom to top - and uses the first match. +``` + +**Why:** Em dashes are a typographic convention that varies by locale and style guide. A plain hyphen-minus is universally readable, renders consistently across all Markdown renderers, and avoids accidental character encoding issues. + +--- + +#### Callouts (Notes, Warnings, Tips) + +**CRITICAL: Always use GitHub-flavored Markdown native callout syntax, not bold-text workarounds.** + +Available types: `NOTE`, `TIP`, `IMPORTANT`, `WARNING`, `CAUTION`. + +❌ **INCORRECT:** + +```markdown +> **Note:** EnvGene also supports dot-notation keys. + +> **Warning:** This will overwrite existing values. +``` + +✅ **CORRECT:** + +```markdown +> [!NOTE] +> EnvGene also supports dot-notation keys. + +> [!WARNING] +> This will overwrite existing values. + +> [!TIP] +> Use cluster-wide scope to avoid repetition across environments. + +> [!IMPORTANT] +> The `name` field must exactly match the filename without the extension. + +> [!CAUTION] +> Setting `mergeEnvSpecificResourceProfiles: false` replaces the template override entirely. +``` + +**Why:** Native callouts render with icons and colour highlighting on GitHub and other renderers; bold-text variants are plain blockquotes. + +--- + #### Tables **CRITICAL: All Markdown tables MUST have vertically aligned pipe characters (`|`).** @@ -108,10 +165,10 @@ Content... **How to achieve alignment:** -1. **Keep cell content concise** — Long text makes alignment difficult -2. **Simplify when possible** — Remove examples from cells if they make text too long -3. **Uniform width per column** — Each cell in a column should have the same width (add trailing spaces) -4. **Don't add spaces endlessly** — If alignment fails repeatedly, the problem is content length, not spacing +1. **Keep cell content concise** - Long text makes alignment difficult +2. **Simplify when possible** - Remove examples from cells if they make text too long +3. **Uniform width per column** - Each cell in a column should have the same width (add trailing spaces) +4. **Don't add spaces endlessly** - If alignment fails repeatedly, the problem is content length, not spacing ##### Common Mistake @@ -151,6 +208,68 @@ Content... --- +## Object Examples in Documentation + +### Source of Truth for Object Schemas + +**CRITICAL: Never invent object structures. Always derive examples from authoritative sources.** + +The two authoritative sources are: + +- **`docs/envgene-objects.md`** - human-readable descriptions, field explanations, and canonical examples for all EnvGene objects +- **`schemas/`** - JSON Schema files that define required fields, allowed values, and types + +#### Rules + +1. **Before writing any YAML/JSON example** for an EnvGene object, read the corresponding entry in `docs/envgene-objects.md` AND the matching schema file under `schemas/`. +2. **Validate every example against the schema**: all fields marked `"required"` in the schema must be present; no fields may be included that do not exist in the schema (unless `additionalProperties: true`). +3. **Do not guess**: if an object is not described in `docs/envgene-objects.md` and has no schema file, write explicitly: + + > No schema or description found for this object in `docs/envgene-objects.md` or `schemas/`. Cannot provide a validated example. + +4. **Do not add fictional fields** such as `type:` or `applications:` to objects that have no such fields in their schema. +5. **Use real field names**: cross-check field names and allowed enum values against the schema. Do not invent field names based on intuition. + +#### How much of the object to show + +In tutorials and how-to guides, show only the **relevant part** of the object, not the full structure. Use `# ...` comments to signal omitted fields so the reader knows the snippet is intentionally incomplete. + +- **Reference docs** → show the full object. +- **Tutorials / how-to guides** → show only the fields being explained; collapse the rest with `# ...`. + +This keeps examples focused on the concept being taught and avoids becoming outdated when unrelated fields change. + +#### ❌ INCORRECT - invented fields and unnecessary noise + +```yaml +# Namespace template - WRONG: invented fields, full object shown in tutorial context +name: "{{ current_env.name }}-bss" +type: namespace # does not exist in namespace.schema.json +applications: # does not exist in namespace.schema.json + - name: "Cloud-BSS" +credentialsId: "" +isServerSideMerge: false +cleanInstallApprovalRequired: false +mergeDeployParametersAndE2EParameters: false +deployParameterSets: + - "bss" +``` + +#### ✅ CORRECT - focused snippet, validated field names, omissions annotated + +```yaml +# Namespace template - only the relevant section is shown +--- +name: "{{ current_env.environmentName }}-bss" +# ... other required fields (see schemas/namespace.schema.json) ... +profile: + name: dev-bss-override + baseline: dev +# ... deployParameterSets, e2eParameters, etc. ... +``` + +--- + ## Documentation Structure (Diátaxis Framework) This repository follows the [Diátaxis documentation framework](https://diataxis.fr/). diff --git a/README.md b/README.md index 420a362bd..b18f42040 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,13 @@ - [System Requirements](#system-requirements) - [Basic Usage](#basic-usage) - [📚 Documentation](#-documentation) - - [Getting Started](#getting-started) - - [Core Concepts](#core-concepts) - - [How-To Guides](#how-to-guides) - - [Advanced Features](#advanced-features) - - [Examples \& Samples](#examples--samples) - - [Development](#development) + - [Getting Started](#getting-started) + - [Tutorials](#tutorials) + - [Core Concepts](#core-concepts) + - [How-To Guides](#how-to-guides) + - [Advanced Features](#advanced-features) + - [Examples \& Samples](#examples--samples) + - [Development](#development) - [🤝 Contributing](#-contributing) - [📄 License](#-license) @@ -112,6 +113,10 @@ After the pipeline finishes, the Environment configuration will be generated and - [**Quick Start Guide**](#-quick-start) - Create your first Environment +### Tutorials + +- [**Managing Resource Profiles**](/docs/tutorials/resource-profiles.md) - End-to-end walkthrough: from Baseline to Template Override to Environment-Specific Override, including `template_override`, `overrides-parent`, and result verification + ### Core Concepts - [**EnvGene Objects**](/docs/envgene-objects.md) - What are EnvGene objects and how they work diff --git a/docs/README.md b/docs/README.md index 9a7f5c67d..139dbcd69 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,6 +2,7 @@ - [EnvGene Documentation](#envgene-documentation) - [Getting Started](#getting-started) + - [Tutorials](#tutorials) - [Core Concepts](#core-concepts) - [How-To Guides](#how-to-guides) - [Advanced Features](#advanced-features) @@ -12,6 +13,10 @@ - [**Quick Start Guide**](/README.md#getting-started) - Create your first Environment +## Tutorials + +- [**Managing Resource Profiles**](/docs/tutorials/resource-profiles.md) - End-to-end walkthrough: from Baseline to Template Override to Environment-Specific Override, including `template_override`, `overrides-parent`, and result verification + ## Core Concepts - [**EnvGene Objects**](/docs/envgene-objects.md) - What are EnvGene objects and how they work diff --git a/docs/features/resource-profile.md b/docs/features/resource-profile.md index 25b4bfc80..2ed663e7f 100644 --- a/docs/features/resource-profile.md +++ b/docs/features/resource-profile.md @@ -1,7 +1,7 @@ # Resource Profiles - [Resource Profiles](#resource-profiles) - - [Proposed Approach](#proposed-approach) + - [Overview](#overview) - [Resource Profile Processing During Environment Generation](#resource-profile-processing-during-environment-generation) - [Combination Logic](#combination-logic) - [Naming Rules for Resource Profile Override](#naming-rules-for-resource-profile-override) @@ -9,7 +9,7 @@ - [Merging Logic](#merging-logic) - [Resolving Dot Notation](#resolving-dot-notation) -## Proposed Approach +## Overview Performance deployment parameters like `CPU_LIMIT` and `MEMORY_REQUEST` are grouped separately into Resource Profiles. This makes it manage separately these parameters apart from all other deployment parameters. @@ -97,7 +97,7 @@ Which mode is used is controlled by the `inventory.config.mergeEnvSpecificResour inventory: config: # Optional. Default value - `true` - # If `true`, environment-specific Resource Profile Overrides defined in envTemplate.envSpecificParamsets + # If `true`, environment-specific Resource Profile Overrides defined in envTemplate.envSpecificResourceProfiles # are merged with Resource Profile Overrides from the Environment Template # If `false`, they completely replace the Environment Template's Resource Profile Overrides mergeEnvSpecificResourceProfiles: boolean @@ -121,7 +121,7 @@ The Environment-Specific Resource Profile Override is merged **into** the Templa - If the service exists in both: - For each `parameter` in the template's service, check for a parameter with the same `name` in the target. - If the parameter is missing in the target, add the entire parameter from the template. - - If the parameter exists in both, update the parameter in the target by overwriting its `value` with the one from the template. + - If the parameter exists in both, keep the parameter value from the environment-specific override (env-specific has higher priority). In this mode, the resulting [Resource Profile Override](/docs/envgene-objects.md#resource-profile-override) will have the same name as the [Template Resource Profile Override](/docs/envgene-objects.md#template-resource-profile-override). diff --git a/docs/how-to/configure-resource-profiles.md b/docs/how-to/configure-resource-profiles.md index 42ab24766..baf3c285c 100644 --- a/docs/how-to/configure-resource-profiles.md +++ b/docs/how-to/configure-resource-profiles.md @@ -16,7 +16,6 @@ - [Use Case 1: Development vs Production Profiles](#use-case-1-development-vs-production-profiles) - [Use Case 2: Cluster-Wide Resource Scaling](#use-case-2-cluster-wide-resource-scaling) - [Use Case 3: Single Environment Hot Fix](#use-case-3-single-environment-hot-fix) - - [Best Practices](#best-practices) - [Verification](#verification) - [Related Documentation](#related-documentation) @@ -93,13 +92,13 @@ Update your Cloud or Namespace template to reference the profile: **Example:** `/templates/namespaces/core.yaml` ```yaml -name: "{{ current_env.name }}-core" -type: namespace +--- +name: "{{ current_env.environmentName }}-core" +# ... other required fields ... profile: name: "dev-core-profile" -applications: - - name: "Cloud-Core" - # ... application configuration ... + baseline: "dev" +# ... other required fields ... ``` ### Step 3: Commit and Publish Template @@ -288,7 +287,7 @@ applications: value: "3000m" ``` -All environments in `prod-cluster-eu` will inherit this profile automatically via **file location priority** (cluster-level overrides apply to all environments within that cluster unless overridden at environment level). +Each environment in `prod-cluster-eu` that references `eu-prod-scaling` via `envTemplate.envSpecificResourceProfiles` in its `env_definition.yml` will use this file. EnvGene finds it automatically via location priority - no need to copy the file per environment, but the reference in `env_definition.yml` is still required. ### Use Case 3: Single Environment Hot Fix @@ -316,37 +315,6 @@ Update `env_definition.yml` for `prod-env-03` only. --- -## Best Practices - -1. **Use Template Profiles for Defaults** - - Define baseline profiles in Template Repository - - Reference appropriate profiles for environment types (dev, staging, prod) - -2. **Use Environment Specific Overrides for Exceptions** - - Only override when environment needs differ from template defaults - - Document why specific values are needed - -3. **Organize by Scope** - - - **Environment-specific**: High-traffic environments, special requirements - - **Cluster-wide**: Regional or infrastructure-based differences - - **Global**: Cross-cluster standards - -4. **Use Descriptive Names** - - Include environment type, purpose, or special notes - - Examples: `prod-high-traffic`, `eu-cluster-baseline`, `hotfix-temp-scaling` - -5. **Document Decisions** - - Add `description` field explaining why values were chosen - - Link to performance test results or incidents if applicable - -6. **Version Control Best Practices** - - Keep resource profiles in version control - - Review changes carefully (resource changes affect costs and stability) - - Test in lower environments first - ---- - ## Verification After configuring resource profiles, verify they are applied correctly: @@ -360,7 +328,7 @@ After configuring resource profiles, verify they are applied correctly: Look for the Resource Profile Override in the generated output: ```text - Namespaces//resource_profiles/.yml + environments///Profiles/.yml ``` 3. **Verify Merge Result:** diff --git a/docs/instance-pipeline-parameters.md b/docs/instance-pipeline-parameters.md index 4c1fdc03d..82285064e 100644 --- a/docs/instance-pipeline-parameters.md +++ b/docs/instance-pipeline-parameters.md @@ -501,13 +501,13 @@ See details in [Namespace Render Filtering](/docs/features/namespace-render-filt } ``` -| Attribute | Mandatory | Description | Default | Example | -|-------------------|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|---------| -| `namespace` | Mandatory | The name of the Namespace where the parameter to be modified is defined | None | `env-1-platform-monitoring` | -| `application` | Optional | The name of the Application (sub-resource under `namespace`) where the parameter to be modified is defined. Cannot be used with `pipeline` context | None | `MONITORING` | -| `context` | Mandatory | The context of the parameter being modified. Valid values: `pipeline`, `deployment`, `runtime` | None | `deployment` | -| `parameter_key` | Mandatory | The name (key) of the parameter to be modified | None | `login` or `db.connection.password` | -| `parameter_value` | Mandatory | New value (plaintext or encrypted). Envgene, depending on the value of the [`crypt`](/docs/envgene-configs.md#configyml) attribute, will either decrypt, encrypt, or leave the value unchanged. If an encrypted value is passed, it must be encrypted with a key that Envgene can decrypt. | None | `admin` | +| Attribute | Mandatory | Description | Default | Example | +|-------------------|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|-------------------------------------| +| `namespace` | Mandatory | The name of the Namespace where the parameter to be modified is defined | None | `env-1-platform-monitoring` | +| `application` | Optional | The name of the Application (sub-resource under `namespace`) where the parameter to be modified is defined. Cannot be used with `pipeline` context | None | `MONITORING` | +| `context` | Mandatory | The context of the parameter being modified. Valid values: `pipeline`, `deployment`, `runtime` | None | `deployment` | +| `parameter_key` | Mandatory | The name (key) of the parameter to be modified | None | `login` or `db.connection.password` | +| `parameter_value` | Mandatory | New value (plaintext or encrypted). Envgene, depending on the value of the [`crypt`](/docs/envgene-configs.md#configyml) attribute, will either decrypt, encrypt, or leave the value unchanged. If an encrypted value is passed, it must be encrypted with a key that Envgene can decrypt | None | `admin` | **Default Value**: None diff --git a/docs/tutorials/resource-profiles.md b/docs/tutorials/resource-profiles.md new file mode 100644 index 000000000..cd085c01c --- /dev/null +++ b/docs/tutorials/resource-profiles.md @@ -0,0 +1,463 @@ +# Tutorial: Managing Resource Profiles + +- [Tutorial: Managing Resource Profiles](#tutorial-managing-resource-profiles) + - [What You Will Learn](#what-you-will-learn) + - [Prerequisites](#prerequisites) + - [Scenario](#scenario) + - [Step 1: Understand the Resource Profile Hierarchy](#step-1-understand-the-resource-profile-hierarchy) + - [Step 2: Create a Template Resource Profile Override](#step-2-create-a-template-resource-profile-override) + - [2.1 Create the override file](#21-create-the-override-file) + - [2.2 Reference the profile in a Namespace template](#22-reference-the-profile-in-a-namespace-template) + - [Step 3: Use `template_override` to Differentiate Template Profiles](#step-3-use-template_override-to-differentiate-template-profiles) + - [Step 4: Use `overrides-parent` in an Inherited Template](#step-4-use-overrides-parent-in-an-inherited-template) + - [Step 5: Add an Environment-Specific Override in the Instance Repository](#step-5-add-an-environment-specific-override-in-the-instance-repository) + - [5.1 Choose the right scope](#51-choose-the-right-scope) + - [5.2 Create the env-specific override file](#52-create-the-env-specific-override-file) + - [5.3 Reference the override in `env_definition.yml`](#53-reference-the-override-in-env_definitionyml) + - [Step 6: Override the Override for a Single Environment](#step-6-override-the-override-for-a-single-environment) + - [Step 7: Verify the Result](#step-7-verify-the-result) + - [7.1 Profile Override file in `Profiles/`](#71-profile-override-file-in-profiles) + - [7.2 Effective Set](#72-effective-set) + - [Summary](#summary) + +## What You Will Learn + +By the end of this tutorial you will know how to: + +- Read and understand a Resource Profile Baseline distributed with an application artifact +- Create a Template Resource Profile Override in the Template Repository +- Apply different profiles to different environment types without duplicating templates +- Add an environment-specific override in the Instance Repository +- Override the override for a single environment + +## Prerequisites + +- A working Template Repository with Cloud and at least one Namespace template +- A working Instance Repository with at least one environment +- Basic familiarity with EnvGene template and instance repository structure + +## Scenario + +You manage a solution called BSS that consists of one application (`Cloud-BSS`) with a service called `bss-processor`. The solution is deployed to two types of environments: `dev` and `prod`. You want to: + +1. Apply modest, developer-friendly resource limits to all dev environments. +2. Apply production-grade resource limits to all prod environments. +3. Temporarily double the CPU limit for a single environment called `prod-01` during a load test. + +## Step 1: Understand the Resource Profile Hierarchy + +EnvGene manages performance parameters (CPU, memory, replicas, etc.) through a three-level hierarchy: + +| Level | Name | Where it lives | Who controls it | +|-------|----------------------------------------------------|------------------------------------------------------------|-----------------------| +| 1 | Resource Profile **Baseline** | Application SBOM artifact | Application developer | +| 2 | **Template** Resource Profile Override | Template Repository `/templates/resource_profiles/` | Template configurator | +| 3 | **Environment-Specific** Resource Profile Override | Instance Repository `/environments/.../resource_profiles/` | Instance configurator | + +Each level overrides the previous one. The Baseline is the starting point; it ships inside the application (SBOM) and is selected by the name defined in the Cloud or Namespace template (e.g. `dev`, `prod`). You do not edit it. + +A typical Baseline embedded in the application looks like this (shown for orientation only - you cannot change it): + +```yaml +# Baseline name: "dev" +CPU_REQUEST: 100m +CPU_LIMIT: 500m +MEMORY_REQUEST: 256Mi +MEMORY_LIMIT: 512Mi +REPLICAS: 1 +``` + +> [!NOTE] +> EnvGene also supports dot-notation keys (e.g. `resources.limits.cpu`) in Baselines and Override files +> and resolves them into nested YAML structures. + +## Step 2: Create a Template Resource Profile Override + +A Template Resource Profile Override lets you tune the Baseline values for all environments that use the same template - without touching the application artifact itself. + +### 2.1 Create the override file + +Create two files in the Template Repository, one for `dev` and one for `prod`: + +**`/templates/resource_profiles/dev-bss-override.yml`** + +```yaml +name: "dev-bss-override" +baseline: "dev" +description: "Dev resource profile for BSS processor" +applications: + - name: "Cloud-BSS" + services: + - name: "bss-processor" + parameters: + - name: "CPU_REQUEST" + value: "100m" + - name: "CPU_LIMIT" + value: "500m" + - name: "MEMORY_REQUEST" + value: "256Mi" + - name: "MEMORY_LIMIT" + value: "512Mi" + - name: "REPLICAS" + value: 1 +``` + +**`/templates/resource_profiles/prod-bss-override.yml`** + +```yaml +name: "prod-bss-override" +baseline: "prod" +description: "Production resource profile for BSS processor" +applications: + - name: "Cloud-BSS" + services: + - name: "bss-processor" + parameters: + - name: "CPU_REQUEST" + value: "2000m" + - name: "CPU_LIMIT" + value: "4000m" + - name: "MEMORY_REQUEST" + value: "2Gi" + - name: "MEMORY_LIMIT" + value: "4Gi" + - name: "REPLICAS" + value: 5 +``` + +> The `name` field **must** exactly match the filename without the extension. +> The `baseline` field is informational only and is not processed by EnvGene. + +### 2.2 Reference the profile in a Namespace template + +Open your BSS Namespace template and add the `profile` section: + +**`/templates/env_templates/dev/Namespaces/bss.yml.j2`** + +```yaml +--- +name: "{{ current_env.environmentName }}-bss" +# ... other required fields ... +profile: + name: dev-bss-override + baseline: dev +# ... other required fields ... +``` + +**`/templates/env_templates/prod/Namespaces/bss.yml.j2`** + +```yaml +--- +name: "{{ current_env.environmentName }}-bss" +# ... other required fields ... +profile: + name: prod-bss-override + baseline: prod +# ... other required fields ... +``` + +EnvGene reads `profile.name` from the rendered template and looks up the file `.yml` (or `.yaml`) inside `/templates/resource_profiles/`. + +## Step 3: Use `template_override` to Differentiate Template Profiles + +The two-file approach from Step 2 works, but maintaining separate template files that differ only in the `profile` section does not scale. An alternative is to keep a **single**, profile-neutral base template and assign the profile at the descriptor level using `template_override`. + +Replace the two namespace templates from Step 2 with one shared file: + +**`/templates/env_templates/base/Namespaces/bss.yml.j2`** + +```yaml +--- +name: "{{ current_env.environmentName }}-bss" +# ... other required fields ... +# no profile section +``` + +Then create two Template Descriptors that reference it with different profiles: + +**`/templates/env_templates/dev.yml`** + +```yaml +tenant: "{{ templates_dir }}/env_templates/base/tenant.yml.j2" +cloud: "{{ templates_dir }}/env_templates/base/cloud.yml.j2" +namespaces: + - template_path: "{{ templates_dir }}/env_templates/base/Namespaces/bss.yml.j2" + template_override: + profile: + name: "dev-bss-override" + baseline: "dev" +``` + +**`/templates/env_templates/prod.yml`** + +```yaml +tenant: "{{ templates_dir }}/env_templates/base/tenant.yml.j2" +cloud: "{{ templates_dir }}/env_templates/base/cloud.yml.j2" +namespaces: + - template_path: "{{ templates_dir }}/env_templates/base/Namespaces/bss.yml.j2" + template_override: + profile: + name: "prod-bss-override" + baseline: "prod" +``` + +When EnvGene generates an Environment Instance it renders `bss.yml.j2`, then merges the `template_override` block into the result. The final Namespace object gets `profile.name: dev-bss-override` (or `prod-bss-override`), overriding whatever the template originally defined. + +`template_override` supports Jinja expressions, so you can also compute profile names dynamically from environment variables. + +## Step 4: Use `overrides-parent` in an Inherited Template + +A different situation arises when you consume a component from an **external artifact** - a parent template referenced via `parent-templates` that you cannot edit directly. `overrides-parent` is the right mechanism for this case: it lets a child Template Descriptor swap or augment the parent's profile without touching the parent artifact. + +Suppose a parent template (`bss-product-template:2.0.0`) ships with a namespace whose `profile.name` is `default-bss-override`. A child (project-level) template wants to apply `project-bss-override` instead. + +In the **child** Template Descriptor: + +```yaml +parent-templates: + default-bss: bss-product-template:2.0.0 + +namespaces: + - name: "{env}-bss" + parent: default-bss + overrides-parent: + profile: + override-profile-name: "project-bss-override" + parent-profile-name: "default-bss-override" + baseline-profile-name: "dev" + merge-with-parent: true +``` + +Key fields explained: + +| Field | Required | Description | +|-------------------------|----------|-------------------------------------------------------| +| `override-profile-name` | Optional | Profile file in the **child** template repository | +| `parent-profile-name` | Optional | Profile from the **parent** template to override | +| `baseline-profile-name` | Optional | Baseline name to set | +| `merge-with-parent` | Optional | `true`: merge into parent; `false`: replace (default) | + +When `merge-with-parent: true` the child's `project-bss-override` values are **merged into** the parent's `default-bss-override`. Only the parameters listed in `project-bss-override` are overwritten; all other parent parameters are preserved. + +When `merge-with-parent: false` the parent's profile is completely replaced by `project-bss-override`. + +The `overrides-parent` mechanism works for Cloud templates too - place the same `profile` sub-block under `cloud.overrides-parent`. + +## Step 5: Add an Environment-Specific Override in the Instance Repository + +You can further adjust resource parameters for a specific environment in the Instance Repository without modifying the Template Repository at all. + +### 5.1 Choose the right scope + +Place the override file in the location that matches the desired scope: + +| Location | Scope | Use When | +|-----------------------------------------------------------------|-----------------|------------------------------| +| `/environments///Inventory/resource_profiles/` | One environment | Env-specific tuning | +| `/environments//resource_profiles/` | Cluster-wide | All envs in cluster | +| `/environments/resource_profiles/` | Global | All envs in the repository | + +When an environment references a file by name (via `envSpecificResourceProfiles`), EnvGene searches these three locations **from most specific to most general** and uses the first file it finds with that name. + +**How to decide:** + +Ask yourself: *which environments need this override?* + +- **One specific environment** - use the environment-specific path. Changes stay isolated and do not affect any neighbor +- **All environments in a cluster** - use the cluster-wide path. One file covers all environments in that cluster without repeating the file per environment +- **All environments in the repository** - use the global path + +Prefer the **broadest scope that still makes sense** to avoid duplicating files. + +> [!NOTE] +> The file must still be **referenced by name** in each environment's `env_definition.yml` via `envTemplate.envSpecificResourceProfiles`. The location only determines *which file with that name* is actually used when EnvGene finds multiple files with the same name at different levels. + + + +> [!WARNING] +> Placing files with the **same name** at different levels (e.g. a cluster-wide file and an env-specific file with the identical name) is technically supported but makes troubleshooting harder: reading `bss: "prod-cluster-bss"` in `env_definition.yml` gives no indication that a shadow file exists and silently takes priority. **Prefer distinct names** and reference the correct name explicitly - this makes the active override immediately obvious from the inventory alone. + +### 5.2 Create the env-specific override file + +You want all production environments to use 6 replicas. Create a cluster-wide override: + +**`/environments/prod-cluster/resource_profiles/prod-cluster-bss.yml`** + +```yaml +name: "prod-cluster-bss" +baseline: "prod" +description: "Cluster-wide production tuning for BSS" +applications: + - name: "Cloud-BSS" + services: + - name: "bss-processor" + parameters: + - name: "REPLICAS" + value: 6 + - name: "MEMORY_LIMIT" + value: "6Gi" +``` + +### 5.3 Reference the override in `env_definition.yml` + +Update the environment inventory for each environment in `prod-cluster`. Because the file is at cluster level, any environment that references `prod-cluster-bss` will find it automatically via location priority. + +**`/environments/prod-cluster/prod-01/Inventory/env_definition.yml`** + +```yaml +envTemplate: + envSpecificResourceProfiles: + bss: "prod-cluster-bss" +``` + +The key (`bss`) must match the **namespace template name** as defined in the environment template. The value is the filename without the extension. + +By default (`mergeEnvSpecificResourceProfiles: true`) EnvGene **merges** the env-specific file into the template override. The `REPLICAS: 6` and `MEMORY_LIMIT: 6Gi` from `prod-cluster-bss` are applied on top of `prod-bss-override`; all other parameters from the template override are preserved. The resulting Resource Profile Override keeps the name of the Template Override. + +To **replace** the template override entirely instead of merging, set: + +```yaml +inventory: + config: + mergeEnvSpecificResourceProfiles: false +``` + +In this case the resulting Resource Profile Override keeps the name of the env-specific file. + +## Step 6: Override the Override for a Single Environment + +`prod-01` needs to double its CPU limit for an upcoming load test, while all other `prod-cluster` environments keep the cluster-wide profile. + +The recommended approach is to give the env-specific file a **distinct name** and reference it explicitly in `env_definition.yml`. This makes the active override immediately visible in the inventory without having to inspect the filesystem. + +Create the file at the environment-specific path: + +**`/environments/prod-cluster/prod-01/Inventory/resource_profiles/prod-01-bss-loadtest.yml`** + +```yaml +name: "prod-01-bss-loadtest" +baseline: "prod" +description: "Temporary load-test profile for prod-01" +applications: + - name: "Cloud-BSS" + services: + - name: "bss-processor" + parameters: + - name: "REPLICAS" + value: 6 + - name: "CPU_REQUEST" + value: "4000m" + - name: "CPU_LIMIT" + value: "8000m" + - name: "MEMORY_LIMIT" + value: "6Gi" +``` + +Then update `env_definition.yml` for `prod-01` to point to the new file: + +**`/environments/prod-cluster/prod-01/Inventory/env_definition.yml`** + +```yaml +envTemplate: + envSpecificResourceProfiles: + bss: "prod-01-bss-loadtest" +``` + +EnvGene finds `prod-01-bss-loadtest.yml` at the env-specific path and applies it. All other environments in `prod-cluster` still reference `prod-cluster-bss` and are unaffected. + +> [!WARNING] +> An alternative is to create the env-specific file with the **same name** as the cluster-wide file (`prod-cluster-bss.yml`) and skip the `env_definition.yml` change entirely. EnvGene will silently pick up the more specific copy due to location priority. Avoid this pattern: when reading the inventory you cannot tell which file is actually active, which makes incidents significantly harder to diagnose. + +## Step 7: Verify the Result + +After committing your changes, trigger environment generation and inspect two artifacts. + +### 7.1 Profile Override file in `Profiles/` + +The name of the resulting file in `Profiles/` depends on the combination mode: + +| `mergeEnvSpecificResourceProfiles` | Resulting filename in `Profiles/` | +|------------------------------------|---------------------------------------------| +| `true` (default) | Name of the **template** override | +| `false` | Name of the **env-specific** override | + +In this tutorial the default merge mode is used, so the resulting file keeps the template override name: + +```text +environments/prod-cluster/prod-01/Profiles/prod-bss-override.yml +``` + +Open it to verify the merged result. Parameters that came from `prod-01-bss-loadtest` carry an inline `# from prod-01-bss-loadtest` comment, making it clear which env-specific file contributed each value: + +```yaml +applications: +- name: "Cloud-BSS" + services: + - name: "bss-processor" + parameters: + - name: "REPLICAS" + value: 6 # from prod-01-bss-loadtest + - name: "CPU_REQUEST" + value: "4000m" # from prod-01-bss-loadtest + - name: "CPU_LIMIT" + value: "8000m" # from prod-01-bss-loadtest + - name: "MEMORY_REQUEST" + value: "2Gi" + - name: "MEMORY_LIMIT" + value: "6Gi" # from prod-01-bss-loadtest +``` + +`MEMORY_REQUEST` has no comment - it was not present in `prod-01-bss-loadtest` and was kept unchanged from the template override. + +### 7.2 Effective Set + +Then inspect the per-service deployment parameters in the Effective Set: + +```text +environments/prod-cluster/prod-01/effective-set/deployment/bss/Cloud-BSS/values/per-service-parameters/bss-processor/deployment-parameters.yaml +``` + +Each parameter line carries an inline traceability comment showing its ultimate source after baseline + override resolution: + +```yaml +bss-processor: + REPLICAS: 6 #rp-override: prod-bss-override + CPU_REQUEST: 4000m #rp-override: prod-bss-override + CPU_LIMIT: 8000m #rp-override: prod-bss-override + MEMORY_REQUEST: 2Gi #rp-override: prod-bss-override + MEMORY_LIMIT: 6Gi #rp-override: prod-bss-override + HPA_ENABLED: false #rp-baseline: prod +``` + +The two comment formats: + +| Comment | Meaning | +|------------------------|--------------------------------------------------------------------| +| `#rp-baseline: ` | Value comes from the Baseline in the application SBOM | +| `#rp-override: ` | Value was set or overridden by the named Resource Profile Override | + +- **Effective Set comment** - shows which `Profiles/` file contributed the value. +- **`Profiles/` comment** (`# from ...`) - shows which env-specific file overrode that value inside the Profile Override. + +This makes troubleshooting straightforward: if a service is using unexpected resource values, open the Effective Set file, find the parameter, read the comment, and go directly to the source. + +## Summary + +In this tutorial you walked through the full resource profile management workflow: + +- **Baseline** - ships with the application artifact; defines starting performance parameters; read-only from EnvGene's perspective. +- **Template Resource Profile Override** - lives in `/templates/resource_profiles/`; referenced by `profile.name` in Cloud/Namespace templates; applies to all environments using the same template. +- **`template_override`** - replace two near-identical template files with a single base template; assign different profiles in `dev.yml` / `prod.yml` Template Descriptors via the `template_override.profile` block. +- **`overrides-parent`** - when consuming an external versioned parent template artifact, swap or augment its profile in the child Template Descriptor without editing the parent artifact. +- **Environment-Specific Override** - lives in the Instance Repository; referenced via `envSpecificResourceProfiles` in `env_definition.yml`; merged into or replaces the template override depending on `mergeEnvSpecificResourceProfiles`. +- **Override of override** - give the env-specific file a distinct name and reference it explicitly in `env_definition.yml`; use the environment-specific path (`/environments///Inventory/resource_profiles/`) to scope it to one environment, leaving all others unaffected. + +**Related documentation:** + +- [Resource Profile Feature Documentation](/docs/features/resource-profile.md) +- [How to Configure Resource Profiles](/docs/how-to/configure-resource-profiles.md) +- [Template Resource Profile Override Schema](/docs/envgene-objects.md#template-resource-profile-override) +- [Environment Specific Resource Profile Override Schema](/docs/envgene-objects.md#environment-specific-resource-profile-override) +- [Template Override Feature](/docs/features/template-override.md) +- [Template Inheritance Feature](/docs/features/template-inheritance.md) +- [Environment Inventory Reference](/docs/envgene-configs.md#env_definitionyml) From faca4ab7c42bc5c915553f22472fe46c327ab7b6 Mon Sep 17 00:00:00 2001 From: popoveugene <42543333+popoveugene@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:02:23 +0300 Subject: [PATCH 054/161] docs: traceability change (#1062) --- docs/features/calculator-cli.md | 70 +++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/docs/features/calculator-cli.md b/docs/features/calculator-cli.md index 57449c67f..e46c197a6 100644 --- a/docs/features/calculator-cli.md +++ b/docs/features/calculator-cli.md @@ -543,34 +543,34 @@ The CLI flag [`--enable-traceability`](#calculator-command-line-tool-execution-a ##### Parameter Source to Comment Mapping -| Parameter Source | Comment | Example | -|----------------------------------------------------|--------------------------------------------|----------------------------------------------------------------------------------------------| -| Custom Params (`--custom-params`) | `# custom params` | `OVERRIDE_KEY: "value" # custom params` | -| Environment Instance, Tenant | `# tenant` | `GITLAB_URL: "https://git.qibership.org" # tenant` | -| Environment Instance, Cloud | `# cloud` | `CLOUD_API_HOST: "https://api.example.com" # cloud` | -| Environment Instance, Namespace | `# namespace: ` | `NAMESPACE_NAME: "env-1-core" # namespace: env-1-core` | -| Environment Instance, Application | `# application: ` | `APP_FEATURE_FLAG: true # application: my-app` | -| Environment Instance, Resource Profile Override | `# resource-profile-override: ` | `CPU_LIMIT: "500m" # resource-profile-override: perf-small` | -| Environment Instance, Composite Structure | `# composite-structure` | `composite_structure: # composite-structure` | -| Environment Instance, BG Domain | `# bg-domain` | `bg_domain: # bg-domain` | -| Application SBOM | `# sbom` | `deploy_param: '' # sbom` | -| Application SBOM, Resource Profile Baseline | `# sbom, resource-profile-baseline: `| `PROFILE_BASELINE: "dev" # sbom, resource-profile-baseline: dev` | -| Calculated by calculator | `# envgene calculated` | `PUBLIC_GATEWAY_URL: "https://public-gateway-bss.qubership.org" # envgene calculated` | -| Calculator `--extra_params` | `# envgene pipeline parameter` | `DEPLOYMENT_SESSION_ID: "7e9f5f54-4be2-4fbd-a267-19e78d09810d" # envgene pipeline parameter` | -| Default value by calculator | `# envgene default` | `MANAGED_BY: "argocd" # envgene default` | +| Parameter Source | Comment | Example | +|-------------------------------------------------|-------------------------------|---------------------------------------------------------------------------------------------| +| Custom Params (`--custom-params`) | `#custom params` | `OVERRIDE_KEY: "value" #custom params` | +| Environment Instance, Tenant | `#tenant` | `GITLAB_URL: "https://git.qibership.org" #tenant` | +| Environment Instance, Cloud | `#cloud` | `CLOUD_API_HOST: "https://api.example.com" #cloud` | +| Environment Instance, Namespace | `#namespace` | `NAMESPACE_NAME: "env-1-core" #namespace` | +| Environment Instance, Application | `#application` | `APP_FEATURE_FLAG: true #application` | +| Environment Instance, Resource Profile Override | `#rp-override: ` | `CPU_LIMIT: "500m" #rp-override: perf-small` | +| Environment Instance, Composite Structure | `#composite-structure` | `composite_structure: {} #composite-structure` | +| Environment Instance, BG Domain | `#bg-domain` | `bg_domain: {} #bg-domain` | +| Application SBOM (component properties) | `#sbom` | `git_revision: "a71c5988" #sbom` | +| Application SBOM, Resource Profile Baseline | `#rp-baseline: ` | `CPU_LIMIT: "500m" #rp-baseline: dev` | +| Calculated by calculator | `#envgene calculated` | `PUBLIC_GATEWAY_URL: "https://public-gateway-bss.qubership.org" #envgene calculated` | +| Calculator `--extra_params` | `#envgene pipeline parameter` | `DEPLOYMENT_SESSION_ID: "7e9f5f54-4be2-4fbd-a267-19e78d09810d" #envgene pipeline parameter` | +| Default value by calculator | `#envgene default` | `MANAGED_BY: "argocd" #envgene default` | ##### Rules for Adding Comments -1. The comment is added after a single space following the parameter value on the same line for non-multiline values. +1. The comment is added immediately after the parameter value (no space between value and `#`) on the same line for non-multiline values. ```yaml - SECURITY_POLICY: strict # cloud + SECURITY_POLICY: strict #cloud ``` 2. The comment is added on the previous line above the parameter for multiline values (using `|` or `>`). ```yaml - # cloud + #cloud CS_CONTENT_SECURITY_POLICY: | {"CONTENT_SECURITY_POLICY":"default-src..."} ``` @@ -589,24 +589,42 @@ The CLI flag [`--enable-traceability`](#calculator-command-line-tool-execution-a ```yaml servers: - - "server1.example.com" # cloud - - "server2.example.com" # namespace: env-1 + - "server1.example.com" #cloud + - "server2.example.com" #namespace servers: - name: "server1" # cloud - host: "host1" # cloud - port: 8080 # namespace: env-1 + name: "server1" #cloud + host: "host1" #cloud + port: 8080 #namespace ``` 9. Comments are not added to YAML anchors/aliases (`&id001`, `*id001`, `<<: *id001`). But for regular keys/values filled in via anchors/aliases, still show their source as a comment: ```yaml global: &id001 - key1: value1 # cloud - key2: value2 # cloud + key1: value1 #cloud + key2: value2 #cloud service1: <<: *id001 - key3: value3 # namespace: env-1-core + key3: value3 #namespace + ``` + +10. **Exception for `deploy-descriptor.yaml`** — this file is almost entirely generated from the Application SBOM. As an exception to Rule 8, per-line comments are **not** added. Instead, a single file-level header comment is placed at the top of the file describing the source for all parameters. Parameters whose source differs from the header (e.g. predefined parameters with `#rp-baseline: ` or `#rp-override: `) are still marked inline. + + Example: + + ```yaml + #Source of parameters not marked inline: `#sbom` + global: + deployDescriptor: + my-service: + git_revision: a71c5988fc92de5f9698434bfe43d513245969aa + build_id_dtrust: 3d28937a-c7ce-4600-b454-6c08ae61f557 + service_name: my-service + version: release-2025.3-9.16.0 + type: cr + REPLICAS: 1 #rp-override: dev-override + CPU_LIMIT: 500m #rp-baseline: dev ``` #### [Version 2.0] Deployment Parameter Context From f3d242492841fdada3127665ddd0b97b92dc301d Mon Sep 17 00:00:00 2001 From: popoveugene <42543333+popoveugene@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:04:20 +0300 Subject: [PATCH 055/161] docs: sbom retention (#1057) * docs: add job artifact * docs: add sbom retention * docs: wip * docs: wip --- docs/README.md | 1 + docs/dev/job-artifacts.md | 51 ++++++++ docs/envgene-configs.md | 11 ++ docs/features/sbom-retention.md | 100 +++++++++++++++ docs/features/sbom.md | 7 ++ docs/use-cases/sbom-retention.md | 206 +++++++++++++++++++++++++++++++ schemas/config.schema.json | 32 ++++- 7 files changed, 407 insertions(+), 1 deletion(-) create mode 100644 docs/dev/job-artifacts.md create mode 100644 docs/features/sbom-retention.md create mode 100644 docs/use-cases/sbom-retention.md diff --git a/docs/README.md b/docs/README.md index 139dbcd69..03eb958c1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -64,6 +64,7 @@ - [**Blue-Green Deployment**](/docs/features/blue-green-deployment.md) - BG domains, state management, and `bg_manage` pipeline job - [**Resource Profiles**](/docs/features/resource-profile.md) - Baselines and overrides for performance parameters - [**SBOM**](/docs/features/sbom.md) - CycloneDX-based artifact and parameter exchange for EnvGene +- [**SBOM Retention**](/docs/features/sbom-retention.md) - Automatic cleanup of cached SBOM files to manage repository size ## Examples & Samples diff --git a/docs/dev/job-artifacts.md b/docs/dev/job-artifacts.md new file mode 100644 index 000000000..95bd89aa3 --- /dev/null +++ b/docs/dev/job-artifacts.md @@ -0,0 +1,51 @@ +# Job Artifacts + +## Overview + +GitLab pipeline jobs pass state between each other through artifacts without repeated Git checkouts. This document explains the mechanism and requirements. + +Artifact size limit: **1500 MB** + +## Git Checkout Strategy + +### First Job in Pipeline + +1. Performs `git checkout` +2. Gets fresh copy of repository +3. Modifies files (optional) +4. Saves required paths to job artifacts + +### Intermediate Jobs + +- Do NOT checkout repository +- Receive files from previous job's artifacts +- Modify files (optional) +- Save required paths to job artifacts + +### git_commit_job + +- Receives files from previous job's artifacts +- Does `git init` and `git pull` (retrieves current repository state from remote) +- Copies files from job's artifacts (overwrites pulled files with changes) +- Commits and pushes changes + +## Required Artifact Paths + +All jobs in the pipeline save **only** these paths to artifacts: + +- `/environments/` + +- `/configuration/` +- `/sboms/` +- `/templates/` + +These paths are: + +1. Modified by various jobs during pipeline execution +2. Needed by downstream jobs +3. Committed to Git by `git_commit_job` diff --git a/docs/envgene-configs.md b/docs/envgene-configs.md index a99032d26..27a39b303 100644 --- a/docs/envgene-configs.md +++ b/docs/envgene-configs.md @@ -225,6 +225,17 @@ artifact_definitions_discovery_mode: enum [`auto`, `true`, `false`] # `cmdb` - Application and Registry Definitions are discovered from a CMDB system (discovery procedure is not part of EnvGene Core). Discovery result is saved in repository # `auto` - Definitions are first searched in repository, if not found - discovered from CMDB. Discovery result is saved in repository app_reg_def_mode: enum [`auto`, `cmdb`, `local`] +# Optional +# SBOM retention configuration +# Triggers during Effective Set generation when repository reaches 1200 GB size threshold +sbom_retention: + # Optional. Default value - `false` + # Enable/disable SBOM retention cleanup + enabled: boolean + # Optional. Default value - `10` + # Number of latest versions to keep per application + # Used only when enabled is true + keep_versions_per_app: integer ``` ## `integration.yml` diff --git a/docs/features/sbom-retention.md b/docs/features/sbom-retention.md new file mode 100644 index 000000000..34fef2ea0 --- /dev/null +++ b/docs/features/sbom-retention.md @@ -0,0 +1,100 @@ +# SBOM Retention + +- [SBOM Retention](#sbom-retention) + - [Overview](#overview) + - [Problem Statement](#problem-statement) + - [Solution](#solution) + - [Retention Strategy](#retention-strategy) + - [Version-Based Strategy](#version-based-strategy) + - [When Cleanup is Triggered](#when-cleanup-is-triggered) + - [Configuration](#configuration) + - [Parameters](#parameters) + - [Examples](#examples) + - [No SBOM cleanup is performed](#no-sbom-cleanup-is-performed) + - [Keep only n most recent versions per application](#keep-only-n-most-recent-versions-per-application) + - [Use Cases](#use-cases) + +## Overview + +SBOM (Software Bill of Materials) files are cached in the Instance Repository to avoid expensive regeneration. This feature provides automatic cleanup of old SBOM files to manage repository size. + +## Problem Statement + +- SBOM generation is a computationally expensive operation +- SBOM files are cached in `/sboms/` directory for reuse +- [Job artifacts](/docs/dev/job-artifacts.md) size limit is 1500 GB +- Without cleanup, the cache grows indefinitely and may reach the size limit + +## Solution + +Automatic SBOM retention policy that: + +- Runs during effective set generation when [GENERATE_EFFECTIVE_SET: true](/docs/instance-pipeline-parameters.md#generate_effective_set) +- Monitors repository size +- Triggers cleanup when size threshold is reached (1200 GB) +- Keeps N most recent versions per application +- Prevents cache growth beyond acceptable limits + +## Retention Strategy + +### Version-Based Strategy + +The version-based strategy keeps the N most recent versions for each application: + +- Groups SBOM files by application name +- Sorts versions by file creation time (newest first) +- Keeps the latest N versions +- Deletes all older versions + +### When Cleanup is Triggered + +Cleanup runs **only** when: + +1. `GENERATE_EFFECTIVE_SET: true` +2. `sbom_retention.enabled: true` in configuration +3. Repository size reaches 1200 GB threshold + +## Configuration + +SBOM retention is configured in `/configuration/config.yml`. + +### Parameters + +```yaml +# Optional +# Triggers only when repository reaches 1200 GB +sbom_retention: + # Optional + # Default value: false + enabled: bool + # Optional + # Default value: 10 + keep_versions_per_app: int +``` + +### Examples + +#### No SBOM cleanup is performed + +```yaml +# No sbom_retention section +``` + +or + +```yaml +sbom_retention: + enabled: false +``` + +#### Keep only n most recent versions per application + +```yaml +sbom_retention: + enabled: true + keep_versions_per_app: n +``` + +## Use Cases + +For detailed step-by-step scenarios demonstrating different SBOM retention configurations and repository states, see [SBOM Retention Use Cases](/docs/use-cases/sbom-retention.md). diff --git a/docs/features/sbom.md b/docs/features/sbom.md index 059321388..d7847952e 100644 --- a/docs/features/sbom.md +++ b/docs/features/sbom.md @@ -8,6 +8,7 @@ - [SBOM types](#sbom-types) - [Application SBOM](#application-sbom) - [Environment Template SBOM](#environment-template-sbom) + - [SBOM Storage and Retention](#sbom-storage-and-retention) - [Use Cases](#use-cases) - [Affection map](#affection-map) - [Links](#links) @@ -53,6 +54,12 @@ A JSON file compliant with the CycloneDX specification, describing the following [Example](/examples/env-template.sbom.json) +## SBOM Storage and Retention + +Generated SBOM files are cached in the `/sboms/` directory of the Instance Repository to avoid expensive regeneration. + +To manage repository size and prevent reaching the 1500 GB limit, EnvGene provides automatic SBOM retention. See [SBOM Retention](/docs/features/sbom-retention.md) for configuration details. + ## Use Cases 1. TBD diff --git a/docs/use-cases/sbom-retention.md b/docs/use-cases/sbom-retention.md new file mode 100644 index 000000000..d79db0fb9 --- /dev/null +++ b/docs/use-cases/sbom-retention.md @@ -0,0 +1,206 @@ +# SBOM Retention Use Cases + +- [SBOM Retention Use Cases](#sbom-retention-use-cases) + - [Overview](#overview) + - [SBOM Cleanup Execution](#sbom-cleanup-execution) + - [UC-SBOM-1: SBOM Retention Disabled - No Cleanup](#uc-sbom-1-sbom-retention-disabled---no-cleanup) + - [UC-SBOM-2: Repository Below Threshold - No Cleanup](#uc-sbom-2-repository-below-threshold---no-cleanup) + - [UC-SBOM-3: Repository Above Threshold - Cleanup with Default Settings](#uc-sbom-3-repository-above-threshold---cleanup-with-default-settings) + - [UC-SBOM-4: Repository Above Threshold - Cleanup with Custom Version Count](#uc-sbom-4-repository-above-threshold---cleanup-with-custom-version-count) + +## Overview + +This document covers use cases for [SBOM Retention](/docs/features/sbom-retention.md) - automatic cleanup of cached SBOM files to manage Instance Repository size. + +## SBOM Cleanup Execution + +The cleanup logic is triggered during effective set generation and depends on configuration settings and repository size. These use cases demonstrate different scenarios based on configuration and repository state. + +### UC-SBOM-1: SBOM Retention Disabled - No Cleanup + +**Pre-requisites:** + +1. Instance Repository exists with `/sboms/` directory +2. SBOM files exist in `/sboms/` directory +3. SBOM retention is **disabled** in `/configuration/config.yml`: + + ```yaml + # Option 1: No sbom_retention section + crypt: false + ``` + + or + + ```yaml + # Option 2: Explicitly disabled + sbom_retention: + enabled: false + ``` + +4. Repository size is 1300 GB (above 1200 GB threshold) + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `GENERATE_EFFECTIVE_SET: true` + +**Steps:** + +1. The `generate_effective_set` job runs in the pipeline: + 1. Generates effective set for the environment + 2. Checks SBOM retention configuration + 3. Finds `sbom_retention.enabled: false` (or no configuration) + 4. Skips SBOM cleanup logic + 5. Completes effective set generation + +**Results:** + +1. Effective set is generated successfully +2. No SBOM files are deleted +3. Pipeline log shows: "SBOM retention is disabled, skipping cleanup" + +### UC-SBOM-2: Repository Below Threshold - No Cleanup + +**Pre-requisites:** + +1. Instance Repository exists with `/sboms/` directory +2. SBOM files exist in `/sboms/` directory with total size 800 GB +3. SBOM retention is **enabled** in `/configuration/config.yml`: + + ```yaml + sbom_retention: + enabled: true + keep_versions_per_app: 10 + ``` + +4. Repository size is 800 GB (below 1200 GB threshold) + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `GENERATE_EFFECTIVE_SET: true` + +**Steps:** + +1. The `generate_effective_set` job runs in the pipeline: + 1. Generates effective set for the environment + 2. Checks SBOM retention configuration + 3. Finds `sbom_retention.enabled: true` + 4. Checks repository size: 800 GB + 5. Compares with threshold: 800 GB < 1200 GB + 6. Skips SBOM cleanup (threshold not reached) + 7. Completes effective set generation + +**Results:** + +1. Effective set is generated successfully +2. No SBOM files are deleted +3. Pipeline log shows: "Repository size (800 GB) below threshold (1200 GB), skipping cleanup" + +### UC-SBOM-3: Repository Above Threshold - Cleanup with Default Settings + +**Pre-requisites:** + +1. Instance Repository exists with `/sboms/` directory +2. SBOM files exist for multiple applications: + - `app-a-1.0.15.sbom.json` through `app-a-1.0.1.sbom.json` (15 versions) + - `app-b-2.0.12.sbom.json` through `app-b-2.0.1.sbom.json` (12 versions) + - `app-c-3.5.8.sbom.json` through `app-c-3.5.1.sbom.json` (8 versions) +3. SBOM retention is **enabled** with default settings in `/configuration/config.yml`: + + ```yaml + sbom_retention: + enabled: true + keep_versions_per_app: 10 # default value + ``` + +4. Repository size is 1300 GB (above 1200 GB threshold) + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `GENERATE_EFFECTIVE_SET: true` + +**Steps:** + +1. The `generate_effective_set` job runs in the pipeline: + 1. Generates effective set for the environment + 2. Checks SBOM retention configuration: enabled with `keep_versions_per_app: 10` + 3. Checks repository size: 1300 GB > 1200 GB threshold + 4. Triggers SBOM cleanup process: + 1. Scans `/sboms/` directory + 2. Groups SBOM files by application name + 3. For each application: + - Sorts versions by file creation time (newest first) + - Keeps 10 most recent versions + - Deletes older versions + 5. Completes effective set generation + +**Results:** + +1. Effective set is generated successfully +2. SBOM files are cleaned up for each application: + - **app-a**: Keeps versions 1.0.15 through 1.0.6 (10 versions) + - **app-b**: Keeps versions 2.0.12 through 2.0.3 (10 versions) + - **app-c**: Keeps all 8 versions (no deletion needed) +3. Total files deleted: 7 SBOM files +4. Pipeline log shows: + - "Repository size (1300 GB) above threshold (1200 GB), starting cleanup" + - "Cleaned up 7 SBOM files" + - "Kept 10 versions per application" + +### UC-SBOM-4: Repository Above Threshold - Cleanup with Custom Version Count + +**Pre-requisites:** + +1. Instance Repository exists with `/sboms/` directory +2. SBOM files exist for application `postgres`: + - `postgres-pg16-2.10.10.sbom.json` through `postgres-pg16-2.10.1.sbom.json` (10 versions) +3. SBOM retention is **enabled** with custom settings in `/configuration/config.yml`: + + ```yaml + sbom_retention: + enabled: true + keep_versions_per_app: 3 # only keep 3 most recent versions + ``` + +4. Repository size is 1350 GB (above 1200 GB threshold) + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `GENERATE_EFFECTIVE_SET: true` + +**Steps:** + +1. The `generate_effective_set` job runs in the pipeline: + 1. Generates effective set for the environment + 2. Checks SBOM retention configuration: enabled with `keep_versions_per_app: 3` + 3. Checks repository size: 1350 GB > 1200 GB threshold + 4. Triggers SBOM cleanup process: + 1. Scans `/sboms/` directory + 2. Groups SBOM files by application name (finds `postgres`) + 3. Sorts postgres versions by file creation time (newest first) + 4. Keeps 3 most recent versions: 2.10.10, 2.10.9, 2.10.8 + 5. Deletes 7 older versions: 2.10.7 through 2.10.1 + 5. Completes effective set generation + +**Results:** + +1. Effective set is generated successfully +2. SBOM files for `postgres` are cleaned up: + - **Kept**: `postgres-pg16-2.10.10.sbom.json`, `postgres-pg16-2.10.9.sbom.json`, `postgres-pg16-2.10.8.sbom.json` + - **Deleted**: `postgres-pg16-2.10.7.sbom.json` through `postgres-pg16-2.10.1.sbom.json` (7 files) +3. Total files deleted: 7 SBOM files +4. Pipeline log shows: + - "Repository size (1350 GB) above threshold (1200 GB), starting cleanup" + - "Cleaned up 7 SBOM files for postgres" + - "Kept 3 versions per application" diff --git a/schemas/config.schema.json b/schemas/config.schema.json index c8f27ab93..371d097fa 100644 --- a/schemas/config.schema.json +++ b/schemas/config.schema.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "title": "Generic Configuration", - "description": "Configuration for the for cryptographic operations, artifact discovery, and cloud passport decryption", + "description": "Configuration for cryptographic operations, artifact discovery, SBOM retention, and cloud passport decryption", "additionalProperties": true, "properties": { "crypt": { @@ -57,6 +57,36 @@ "cmdb", "local" ] + }, + "sbom_retention": { + "type": "object", + "title": "SBOM Retention Configuration", + "description": "Configuration for automatic SBOM file cleanup to manage repository size. Triggers only when repository reaches 1200 GB threshold.", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable SBOM Retention", + "description": "Enable or disable SBOM retention cleanup", + "default": false, + "examples": [ + true, + false + ] + }, + "keep_versions_per_app": { + "type": "integer", + "title": "Versions to Keep", + "description": "Number of latest versions to keep per application. Used only when enabled is true.", + "minimum": 1, + "default": 10, + "examples": [ + 5, + 10, + 15 + ] + } + } } } } From 5963c98a2af1d73c3c86ce1b0009978babfebfcd Mon Sep 17 00:00:00 2001 From: Siva Reddy Kunduru <35566000+sivareddyit@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:37:27 +0530 Subject: [PATCH 056/161] feat: add custom parameters to cli (#1058) feat: add custom parameters to cli fix: added custom parameters to CLI 995 fix: added custom parameters to CLI 995 fix: added custom parameters to CLI 995 fix: added custom parameters to CLI 995 fix: added custom parameters to CLI 995 fix: added custom parameters to CLI 995 feat: add custom parameters to cli --- .../pojo/parameterset/CustomParameterDTO.java | 46 +++++++++++++ .../devops/commons/utils/ParameterUtils.java | 20 +++++- .../qubership/cloud/devops/cli/CmdbCli.java | 48 ++++++++++++-- .../devops/cli/parser/CliParameterParser.java | 66 +++++++++++++------ .../cli/pojo/dto/shared/SharedData.java | 8 +++ .../cloud/devops/cli/CmdbCliTest.java | 3 +- .../src/test/resources/config.json | 8 +++ .../environments/cluster-01/pl-01/cloud.yml | 6 +- .../monitoring-origin/credentials.yaml | 1 + .../effective-set/cleanup/pg/credentials.yaml | 1 + .../MONITORING/values/custom-params.yaml | 1 + .../pg/postgres/values/custom-params.yaml | 1 + .../MONITORING/credentials.yaml | 1 + .../MONITORING/parameters.yaml | 2 + .../runtime/pg/postgres/credentials.yaml | 1 + .../runtime/pg/postgres/parameters.yaml | 2 + .../processor/ParametersProcessor.java | 9 +-- .../processor/dto/ParameterBundle.java | 2 + .../processor/expression/binding/Binding.java | 4 +- .../ParametersCalculationServiceV1.java | 3 +- .../ParametersCalculationServiceV2.java | 33 ++++++++-- .../org/qubership/cloud/BindingBaseTest.java | 2 +- build_pipegene/scripts/effective_set_job.py | 25 ++++--- build_pipegene/scripts/gitlab_ci.py | 5 ++ scripts/utils/pipeline_parameters.py | 1 + 25 files changed, 247 insertions(+), 52 deletions(-) create mode 100644 build_effective_set_generator/commons/src/main/java/org/qubership/cloud/devops/commons/pojo/parameterset/CustomParameterDTO.java create mode 100644 build_effective_set_generator/effective-set-generator/src/test/resources/config.json create mode 100644 build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/monitoring-origin/MONITORING/values/custom-params.yaml create mode 100644 build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/pg/postgres/values/custom-params.yaml diff --git a/build_effective_set_generator/commons/src/main/java/org/qubership/cloud/devops/commons/pojo/parameterset/CustomParameterDTO.java b/build_effective_set_generator/commons/src/main/java/org/qubership/cloud/devops/commons/pojo/parameterset/CustomParameterDTO.java new file mode 100644 index 000000000..e8de43779 --- /dev/null +++ b/build_effective_set_generator/commons/src/main/java/org/qubership/cloud/devops/commons/pojo/parameterset/CustomParameterDTO.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024-2025 NetCracker Technology Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.qubership.cloud.devops.commons.pojo.parameterset; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.jackson.Jacksonized; +import org.qubership.cloud.devops.commons.utils.Parameter; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@Data +@Builder +@Jacksonized +@AllArgsConstructor +@NoArgsConstructor +public class CustomParameterDTO { + + private Map deployParams = Collections.emptyMap(); + private Map technicalParams = Collections.emptyMap(); + + public Map getAllParams() { + Map params = new HashMap<>(); + params.putAll(deployParams); + params.putAll(technicalParams); + return params; + } +} \ No newline at end of file diff --git a/build_effective_set_generator/commons/src/main/java/org/qubership/cloud/devops/commons/utils/ParameterUtils.java b/build_effective_set_generator/commons/src/main/java/org/qubership/cloud/devops/commons/utils/ParameterUtils.java index 84ecaae8f..399ab3ace 100644 --- a/build_effective_set_generator/commons/src/main/java/org/qubership/cloud/devops/commons/utils/ParameterUtils.java +++ b/build_effective_set_generator/commons/src/main/java/org/qubership/cloud/devops/commons/utils/ParameterUtils.java @@ -18,6 +18,7 @@ import lombok.experimental.UtilityClass; import org.apache.commons.collections4.MapUtils; +import org.qubership.cloud.devops.commons.pojo.parameterset.CustomParameterDTO; import java.util.*; @@ -117,6 +118,23 @@ public static void splitBgDomainParams(Map bgDomainMap, bgDomainParamsMap.put(CONTROLLER_NAMESPACE, controller); bgDomainSecureMap.put(CONTROLLER_NAMESPACE, Map.of(USERNAME, userName, PASSWORD, password)); } -} + public static void prepareCustomParams(CustomParameterDTO customParameterDTO, + Map deployParams, Map technicalParams) { + updateParameter(customParameterDTO.getAllParams(), deployParams); + updateParameter(customParameterDTO.getAllParams(), technicalParams); + } + + private static void updateParameter(Map customParams, Map params) { + for (Map.Entry entry : customParams.entrySet()) { + String key = entry.getKey(); + Parameter customParam = entry.getValue(); + if (params.containsKey(key)) { + Parameter deployParam = params.get(key); + customParam.setValue(deployParam.getValue()); + } + params.remove(key); + } + } +} diff --git a/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/CmdbCli.java b/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/CmdbCli.java index 7727a7fef..feb8f9907 100644 --- a/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/CmdbCli.java +++ b/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/CmdbCli.java @@ -16,6 +16,10 @@ package org.qubership.cloud.devops.cli; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.qubership.cloud.devops.cli.parser.CliParameterParser; @@ -26,12 +30,12 @@ import lombok.extern.slf4j.Slf4j; import picocli.CommandLine; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.Duration; import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.concurrent.Callable; import static org.qubership.cloud.devops.cli.exceptions.constants.ExceptionMessage.EFFECTIVE_SET_FAILED; @@ -49,6 +53,7 @@ public class CmdbCli implements Callable { @Inject FileDataRepositoryImpl fileDataRepository; + @Inject CliParameterParser parser; @@ -102,6 +107,7 @@ private void validateVersionDependentParams(EffectiveSetVersion version) { } } + @SneakyThrows private void setSharedData() { EffectiveSetVersion effectiveVersion = EffectiveSetVersion.fromString(envParams.version); sharedData.setEffectiveSetVersion(effectiveVersion); @@ -114,9 +120,40 @@ private void setSharedData() { sharedData.setOutputDir(envParams.outputDir); sharedData.setPcsspPaths(envParams.pcssp != null ? List.of(envParams.pcssp) : new ArrayList<>()); sharedData.setAppChartValidation(envParams.appChartValidation); + prepareCustomParameters(getCustomParams(envParams.customParams)); populateDeploymentSessionId(envParams.extraParams); } + @SneakyThrows + private String getCustomParams(String customParams) { + if (StringUtils.isNotEmpty(customParams) && + customParams.startsWith("@")) { + String fileName = customParams.substring(1); + Path projectRoot = Paths.get(System.getProperty("user.dir")); + Path path = projectRoot + .resolve("src/test/resources") + .resolve(fileName) + .normalize(); + customParams = Files.readString(path); + } + return customParams; + } + + private void prepareCustomParameters(String customParams) throws JsonProcessingException { + if (StringUtils.isEmpty(customParams)) { + return; + } + ObjectMapper mapper = new ObjectMapper(); + Map> map = mapper.readValue(customParams, new TypeReference<>() { + }); + if (map.containsKey("deployment")) { + sharedData.setCustomDeployParamMap(map.get("deployment")); + } + if (map.containsKey("runtime")) { + sharedData.setCustomRuntimeParamMap(map.get("runtime")); + } + } + private void populateDeploymentSessionId(String[] extraParams) { if (extraParams != null) { @@ -161,6 +198,9 @@ static class EnvCommandSpace { @CommandLine.Option(names = {"-acv", "--app_chart_validation"}, description = "App chart validation parameter on sbom", arity = "1") boolean appChartValidation = true; + @CommandLine.Option(names = {"-cp", "--custom-params"}, description = "Custom Parameters") + String customParams; + } } diff --git a/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/parser/CliParameterParser.java b/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/parser/CliParameterParser.java index d8da095b2..9e3e94873 100644 --- a/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/parser/CliParameterParser.java +++ b/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/parser/CliParameterParser.java @@ -41,10 +41,13 @@ import org.qubership.cloud.devops.commons.pojo.credentials.model.Credential; import org.qubership.cloud.devops.commons.pojo.credentials.model.UsernamePasswordCredentials; import org.qubership.cloud.devops.commons.pojo.namespaces.dto.NamespaceDTO; +import org.qubership.cloud.devops.commons.pojo.parameterset.CustomParameterDTO; import org.qubership.cloud.devops.commons.repository.interfaces.FileDataConverter; import org.qubership.cloud.devops.commons.utils.CredentialUtils; import org.qubership.cloud.devops.commons.utils.HelmNameNormalizer; +import org.qubership.cloud.devops.commons.utils.Parameter; import org.qubership.cloud.devops.commons.utils.ParameterUtils; +import org.qubership.cloud.devops.commons.utils.constant.ParametersConstants; import org.qubership.cloud.parameters.processor.dto.DeployerInputs; import org.qubership.cloud.parameters.processor.dto.ParameterBundle; import org.qubership.cloud.parameters.processor.service.ParametersCalculationServiceV1; @@ -96,7 +99,7 @@ public void generateEffectiveSet() throws IOException, IllegalArgumentException, processAndSaveParameters(inputData.getSolutionBomDTO(), tenantName, cloudName, namespaceDTOMap); } - private void processAndSaveParameters(Optional solutionDescriptor, String tenantName, String cloudName, Map namespaceDTOMap) throws IOException { + private void processAndSaveParameters(Optional solutionDescriptor, String tenantName, String cloudName, Map namespaceDTOMap) throws IOException { Map deployMappingFileData = new ConcurrentHashMap<>(); Map runtimeMappingFileData = new ConcurrentHashMap<>(); Map cleanupMappingFileData = new ConcurrentHashMap<>(); @@ -148,7 +151,7 @@ private void processAndSaveParameters(Optional solutionDescripto }); if (EffectiveSetVersion.V2_0 == sharedData.getEffectiveSetVersion()) { generateE2EOutput(tenantName, cloudName, k8TokenMap); - if (solutionDescriptor.isPresent()) { + if (solutionDescriptor.isPresent()) { fileDataConverter.writeToFile(new TreeMap<>(deployMappingFileData), sharedData.getOutputDir(), "deployment", "mapping.yaml"); fileDataConverter.writeToFile(new TreeMap<>(runtimeMappingFileData), sharedData.getOutputDir(), "runtime", "mapping.yaml"); fileDataConverter.writeToFile(new TreeMap<>(cleanupMappingFileData), sharedData.getOutputDir(), "cleanup", "mapping.yaml"); @@ -180,17 +183,6 @@ private void generateE2EOutput(String tenantName, String cloudName, Map k8TokenMap) throws IOException { - ParameterBundle parameterBundle = parametersServiceV2.getCleanupParameterBundle(tenantName, cloudName, namespace, null, originalNamespace, k8TokenMap); - if (parameterBundle.getCleanupParameters() == null) { - parameterBundle.setCleanupParameters(new HashMap<>()); - } - if (parameterBundle.getCleanupSecureParameters() == null) { - parameterBundle.setCleanupSecureParameters(new HashMap<>()); - } - createCleanupFiles(parameterBundle, namespace); - } - private void processBgDomainParameters() { BgDomainEntityDTO bgDomainEntityDTO = inputData.getBgDomainEntityDTO(); if (bgDomainEntityDTO != null && bgDomainEntityDTO.getControllerNamespace().getCredentials() != null) { @@ -204,12 +196,6 @@ private void processBgDomainParameters() { } } - private void createCleanupFiles(ParameterBundle parameterBundle, String namespace) throws IOException { - String cleanupDir = String.format("%s/%s/%s", sharedData.getOutputDir(), "cleanup", namespace); - fileDataConverter.writeToFile(parameterBundle.getCleanupParameters(), cleanupDir, "parameters.yaml"); - fileDataConverter.writeToFile(parameterBundle.getCleanupSecureParameters(), cleanupDir, "credentials.yaml"); - } - private void createTopologyFiles(Map k8TokenMap) throws IOException { Map topologyParams = new TreeMap<>(); Map topologySecuredParams = new TreeMap<>(); @@ -297,14 +283,17 @@ public void generateOutput(String tenantName, String cloudName, String namespace } ParameterBundle parameterBundle = null; if (EffectiveSetVersion.V2_0 == sharedData.getEffectiveSetVersion()) { + CustomParameterDTO customParams = getCustomParameters(); parameterBundle = parametersServiceV2.getCliParameter(tenantName, cloudName, namespaceName, appName, deployerInputs, originalNamespace, - k8TokenMap); - generateCleanupOutput(tenantName, cloudName, namespaceName, originalNamespace, k8TokenMap); + k8TokenMap, + customParams); + ParameterBundle cleanupParameterBundle = parametersServiceV2.getCleanupParameterBundle(tenantName, cloudName, namespaceName, null, originalNamespace, k8TokenMap); + createCleanupParams(parameterBundle, cleanupParameterBundle); } else { parameterBundle = parametersServiceV1.getCliParameter(tenantName, cloudName, @@ -318,6 +307,36 @@ public void generateOutput(String tenantName, String cloudName, String namespace createFiles(namespaceName, appName, parameterBundle, originalNamespace); } + private CustomParameterDTO getCustomParameters() { + CustomParameterDTO parameterDTO = CustomParameterDTO.builder().build(); + Map deployParams = new HashMap<>(); + Map techParams = new HashMap<>(); + sharedData.getCustomDeployParamMap().forEach((key, value) -> { + deployParams.put(key, new Parameter(value, ParametersConstants.CUSTOM_PARAMS_ORIGIN, false)); + }); + sharedData.getCustomRuntimeParamMap().forEach((key, value) -> { + techParams.put(key, new Parameter(value, ParametersConstants.CUSTOM_PARAMS_ORIGIN, false)); + }); + parameterDTO.setDeployParams(deployParams); + parameterDTO.setTechnicalParams(techParams); + return parameterDTO; + } + + private void createCleanupParams(ParameterBundle parameterBundle, ParameterBundle cleanupParameterBundle) { + if (cleanupParameterBundle.getCleanupParameters() == null) { + cleanupParameterBundle.setCleanupParameters(new HashMap<>()); + } + if (cleanupParameterBundle.getCleanupSecureParameters() == null) { + cleanupParameterBundle.setCleanupSecureParameters(new HashMap<>()); + } + if (MapUtils.isNotEmpty(cleanupParameterBundle.getCleanupSecureParameters()) && + MapUtils.isNotEmpty(parameterBundle.getCustomTechParameters())) { + cleanupParameterBundle.getCleanupSecureParameters().putAll(parameterBundle.getCustomTechParameters()); + } + parameterBundle.setCleanupParameters(cleanupParameterBundle.getCleanupParameters()); + parameterBundle.setCleanupSecureParameters(cleanupParameterBundle.getCleanupSecureParameters()); + } + private String findDefaultCredentialsId(String namespace) { return !StringUtils.isEmpty(inputData.getNamespaceDTOMap().get(namespace).getCredentialsId()) ? inputData.getNamespaceDTOMap().get(namespace).getCredentialsId() : inputData.getCloudDTO().getDefaultCredentialsId(); @@ -335,6 +354,10 @@ private void createFiles(String namespaceName, String appName, ParameterBundle p String deploymentDir = String.format("%s/%s/%s/%s/%s", sharedData.getOutputDir(), "deployment", namespaceName, appName, "values"); String runtimeDir = String.format("%s/%s/%s/%s", sharedData.getOutputDir(), "runtime", namespaceName, appName); + String cleanupDir = String.format("%s/%s/%s", sharedData.getOutputDir(), "cleanup", namespaceName); + fileDataConverter.writeToFile(parameterBundle.getCleanupParameters(), cleanupDir, "parameters.yaml"); + fileDataConverter.writeToFile(parameterBundle.getCleanupSecureParameters(), cleanupDir, "credentials.yaml"); + //deployment fileDataConverter.writeToFile(parameterBundle.getDeployParams(), deploymentDir, "deployment-parameters.yaml"); if (StringUtils.isNotBlank(parameterBundle.getAppChartName())) { @@ -359,6 +382,7 @@ private void createFiles(String namespaceName, String appName, ParameterBundle p //runtime parameters fileDataConverter.writeToFile(parameterBundle.getConfigServerParams(), runtimeDir, "parameters.yaml"); fileDataConverter.writeToFile(parameterBundle.getSecuredConfigParams(), runtimeDir, "credentials.yaml"); + fileDataConverter.writeToFile(parameterBundle.getCustomDeployParameters(), deploymentDir, "custom-params.yaml"); } else { String appDirectory = String.format("%s/%s/%s", sharedData.getOutputDir(), namespaceName, appName); fileDataConverter.writeToFile(parameterBundle.getDeployParams(), appDirectory, "deployment-parameters.yaml"); diff --git a/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/pojo/dto/shared/SharedData.java b/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/pojo/dto/shared/SharedData.java index 72303bdae..46f22f750 100644 --- a/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/pojo/dto/shared/SharedData.java +++ b/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/pojo/dto/shared/SharedData.java @@ -18,10 +18,13 @@ import jakarta.enterprise.context.ApplicationScoped; +import lombok.Builder; import lombok.Getter; import lombok.Setter; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; @Getter @@ -49,4 +52,9 @@ public class SharedData { private boolean appChartValidation; + @Builder.Default + private Map customDeployParamMap = Collections.emptyMap(); + @Builder.Default + private Map customRuntimeParamMap = Collections.emptyMap(); + } diff --git a/build_effective_set_generator/effective-set-generator/src/test/java/org/qubership/cloud/devops/cli/CmdbCliTest.java b/build_effective_set_generator/effective-set-generator/src/test/java/org/qubership/cloud/devops/cli/CmdbCliTest.java index 46b3aba43..56fe74f12 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/java/org/qubership/cloud/devops/cli/CmdbCliTest.java +++ b/build_effective_set_generator/effective-set-generator/src/test/java/org/qubership/cloud/devops/cli/CmdbCliTest.java @@ -40,7 +40,8 @@ void testGenerateEffectiveSet(@TempDir Path tempDir) throws Exception { "--output", outputPath.toString(), "--effective-set-version", "v2.0", "--extra_params", "DEPLOYMENT_SESSION_ID=6d5a6ce9-0b55-429d-8877-f7a88dae3d9c", - "--app_chart_validation", "false" + "--app_chart_validation", "false", + "--custom-params", "@config.json" ); assertEquals(0, exitCode); diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/config.json b/build_effective_set_generator/effective-set-generator/src/test/resources/config.json new file mode 100644 index 000000000..1598906d6 --- /dev/null +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/config.json @@ -0,0 +1,8 @@ +{ + "deployment": { + "username": "${creds.get(\"minio-cred\").username}" + }, + "runtime": { + "password": "${STORAGE_PASSWORD}" + } +} diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/cloud.yml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/cloud.yml index 0f978c1c8..e151438dd 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/cloud.yml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/cloud.yml @@ -72,8 +72,10 @@ deployParameters: ZOOKEEPER_URL: "${ZOOKEEPER_ADDRESS}" # cloud passport: cluster-01 version: 1.5 e2eParameters: CLOUD_LEVEL_PARAM_1: "cloud-level-value-1" # paramset: cloud-level-params version: 25.1 source: instance -technicalConfigurationParameters: {} +technicalConfigurationParameters: + integrations.ndo-api-gw.url: "http://api-test:8080" + artifact: "org.snakeyaml" deployParameterSets: [] e2eParameterSets: [] technicalConfigurationParameterSets: [] -productionMode: false # cloud passport: cluster-01 version: 1.5 \ No newline at end of file +productionMode: false # cloud passport: cluster-01 version: 1.5 diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/monitoring-origin/credentials.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/monitoring-origin/credentials.yaml index 7b4799025..f46d1cf1f 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/monitoring-origin/credentials.yaml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/monitoring-origin/credentials.yaml @@ -25,3 +25,4 @@ graphite-remote-adapter: user-placeholder-123 kafka: password: pass-placeholder-123 username: user-placeholder-123 +password: pass-placeholder-123 diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/pg/credentials.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/pg/credentials.yaml index f2b1a5208..0adb8aac0 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/pg/credentials.yaml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/pg/credentials.yaml @@ -5,3 +5,4 @@ DOC_STORAGE_USERNAME: user-placeholder-123 K8S_TOKEN: token-placeholder-123 STORAGE_PASSWORD: pass-placeholder-123 STORAGE_USERNAME: user-placeholder-123 +password: pass-placeholder-123 diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/monitoring-origin/MONITORING/values/custom-params.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/monitoring-origin/MONITORING/values/custom-params.yaml new file mode 100644 index 000000000..f2f7061e8 --- /dev/null +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/monitoring-origin/MONITORING/values/custom-params.yaml @@ -0,0 +1 @@ +username: user-placeholder-123 diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/pg/postgres/values/custom-params.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/pg/postgres/values/custom-params.yaml new file mode 100644 index 000000000..f2f7061e8 --- /dev/null +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/pg/postgres/values/custom-params.yaml @@ -0,0 +1 @@ +username: user-placeholder-123 diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/runtime/monitoring-origin/MONITORING/credentials.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/runtime/monitoring-origin/MONITORING/credentials.yaml index e69de29bb..0def499f3 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/runtime/monitoring-origin/MONITORING/credentials.yaml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/runtime/monitoring-origin/MONITORING/credentials.yaml @@ -0,0 +1 @@ +password: pass-placeholder-123 diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/runtime/monitoring-origin/MONITORING/parameters.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/runtime/monitoring-origin/MONITORING/parameters.yaml index 790b85de6..0e3a1acf8 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/runtime/monitoring-origin/MONITORING/parameters.yaml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/runtime/monitoring-origin/MONITORING/parameters.yaml @@ -1,3 +1,5 @@ PARAM_2: value-2 PARAM_6: value-6 TECHNICAL_PARAM_1: VALUE_TP_1 +integrations.ndo-api-gw.url: "http://api-test:8080" +artifact: "org.snakeyaml" diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/runtime/pg/postgres/credentials.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/runtime/pg/postgres/credentials.yaml index e69de29bb..0def499f3 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/runtime/pg/postgres/credentials.yaml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/runtime/pg/postgres/credentials.yaml @@ -0,0 +1 @@ +password: pass-placeholder-123 diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/runtime/pg/postgres/parameters.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/runtime/pg/postgres/parameters.yaml index e69de29bb..87987d319 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/runtime/pg/postgres/parameters.yaml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/runtime/pg/postgres/parameters.yaml @@ -0,0 +1,2 @@ +integrations.ndo-api-gw.url: "http://api-test:8080" +artifact: "org.snakeyaml" diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/ParametersProcessor.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/ParametersProcessor.java index 8df70d716..34725582a 100644 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/ParametersProcessor.java +++ b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/ParametersProcessor.java @@ -42,9 +42,10 @@ public ParametersProcessor(OpenTelemetryProvider openTelemetryProvider) { this.openTelemetryProvider = openTelemetryProvider; } - public Params processAllParameters(String tenant, String cloud, String namespace, String application, DeployerInputs deployerInputs, String originalNamespace) { + public Params processAllParameters(String tenant, String cloud, String namespace, String application, + DeployerInputs deployerInputs, String originalNamespace, Map customParams) { return openTelemetryProvider.withSpan("process", () -> { - Binding binding = new Binding(deployerInputs).init(tenant, cloud, namespace, application, originalNamespace); + Binding binding = new Binding(deployerInputs).init(tenant, cloud, namespace, application, originalNamespace, customParams); Language lang; lang = new ExpressionLanguage(binding); Map deploy = lang.processDeployment(); @@ -56,7 +57,7 @@ public Params processAllParameters(String tenant, String cloud, String namespace public Params processE2EParameters(String tenant, String cloud, String namespace, String application, DeployerInputs deployerInputs, String originalNamespace) { return openTelemetryProvider.withSpan("process", () -> { - Binding binding = new Binding(deployerInputs).init(tenant, cloud, namespace, application, originalNamespace); + Binding binding = new Binding(deployerInputs).init(tenant, cloud, namespace, application, originalNamespace, new HashMap<>()); Language lang; lang = new ExpressionLanguage(binding); Map e2e = lang.processCloudE2E(); @@ -66,7 +67,7 @@ public Params processE2EParameters(String tenant, String cloud, String namespace public Params processNamespaceParameters(String tenant, String cloud, String namespace, DeployerInputs deployerInputs, String originalNamespace) { return openTelemetryProvider.withSpan("process", () -> { - Binding binding = new Binding(deployerInputs).init(tenant, cloud, namespace, null, originalNamespace); + Binding binding = new Binding(deployerInputs).init(tenant, cloud, namespace, null, originalNamespace, new HashMap<>()); Language lang; lang = new ExpressionLanguage(binding); Map namespaceParams = lang.processNamespace(); diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/dto/ParameterBundle.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/dto/ParameterBundle.java index 86cd3efd5..0041ea994 100644 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/dto/ParameterBundle.java +++ b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/dto/ParameterBundle.java @@ -36,6 +36,8 @@ public class ParameterBundle { Map collisionSecureParameters; Map cleanupParameters; Map cleanupSecureParameters; + Map customDeployParameters; + Map customTechParameters; boolean processPerServiceParams; String appChartName; } diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/Binding.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/Binding.java index 4c79d92fb..64b9376d2 100644 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/Binding.java +++ b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/Binding.java @@ -76,7 +76,8 @@ private void processSet(String tenant, String setName, String application, Escap } } - public Binding init(String tenant, String cloud, String namespace, String application, String originalNamespace) { + public Binding init(String tenant, String cloud, String namespace, String application, + String originalNamespace, Map customParams) { this.tenant = tenant; super.put("tenant", new Parameter(new TenantMap(tenant, cloud, namespace, application, this, originalNamespace).init())); super.put("application", new Parameter(new ApplicationMap(application, this, namespace).init())); @@ -85,6 +86,7 @@ public Binding init(String tenant, String cloud, String namespace, String applic Map processed = calculateCredentialsAndPrepareStructuredParams(this); this.putAll(processed); + this.putAll(customParams); return this; } diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV1.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV1.java index e7aa6e4a1..e126151f7 100644 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV1.java +++ b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV1.java @@ -58,7 +58,8 @@ private ParameterBundle getParameterBundle(String tenantName, String cloudName, namespaceName, applicationName, deployerInputs, - originalNamespace); + originalNamespace, + new HashMap<>()); ParameterBundle parameterBundle = ParameterBundle.builder().build(); diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV2.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV2.java index cc33ad696..b7b8c9149 100644 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV2.java +++ b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV2.java @@ -21,6 +21,7 @@ import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.ObjectUtils; +import org.qubership.cloud.devops.commons.pojo.parameterset.CustomParameterDTO; import org.qubership.cloud.devops.commons.utils.Parameter; import org.qubership.cloud.devops.commons.utils.ParameterUtils; import org.qubership.cloud.parameters.processor.ParametersProcessor; @@ -34,6 +35,7 @@ import java.nio.charset.StandardCharsets; import java.util.*; +import static org.qubership.cloud.devops.commons.utils.ParameterUtils.prepareCustomParams; import static org.qubership.cloud.devops.commons.utils.constant.ApplicationConstants.*; import static org.qubership.cloud.devops.commons.utils.constant.NamespaceConstants.SSL_SECRET; @@ -49,8 +51,11 @@ public ParametersCalculationServiceV2(ParametersProcessor parametersProcessor) { } public ParameterBundle getCliParameter(String tenantName, String cloudName, String namespaceName, String applicationName, - DeployerInputs deployerInputs, String originalNamespace, Map k8TokenMap) { - return getParameterBundle(tenantName, cloudName, namespaceName, applicationName, deployerInputs, originalNamespace, k8TokenMap); + DeployerInputs deployerInputs, String originalNamespace, + Map k8TokenMap, CustomParameterDTO customParams) { + return getParameterBundle(tenantName, cloudName, namespaceName, + applicationName, deployerInputs, originalNamespace, + k8TokenMap, customParams); } public ParameterBundle getCliE2EParameter(String tenantName, String cloudName) { @@ -73,13 +78,15 @@ public ParameterBundle getCleanupParameterBundle(String tenantName, String cloud } private ParameterBundle getParameterBundle(String tenantName, String cloudName, String namespaceName, String applicationName, - DeployerInputs deployerInputs, String originalNamespace, Map k8TokenMap) { + DeployerInputs deployerInputs, String originalNamespace, + Map k8TokenMap, CustomParameterDTO customParams) { Params parameters = parametersProcessor.processAllParameters(tenantName, cloudName, namespaceName, applicationName, deployerInputs, - originalNamespace); + originalNamespace, + customParams.getAllParams()); ParameterBundle parameterBundle = ParameterBundle.builder().build(); @@ -89,6 +96,11 @@ private ParameterBundle getParameterBundle(String tenantName, String cloudName, if (MapUtils.isNotEmpty(parameters.getDeployParams()) && parameters.getDeployParams().containsKey(DEPLOY_DESC)) { processDeploymentDescriptorParams(parameters, parameterBundle); } + if (MapUtils.isNotEmpty(customParams.getAllParams())) { + prepareCustomParams(customParams, parameters.getDeployParams(), parameters.getTechParams()); + parameterBundle.setCustomDeployParameters(ParametersProcessor.convertParameterMapToObject(customParams.getDeployParams())); + parameterBundle.setCustomTechParameters(ParametersProcessor.convertParameterMapToObject(customParams.getTechnicalParams())); + } prepareSecureInsecureParams(parameters.getDeployParams(), parameterBundle, ParameterType.DEPLOY, k8TokenMap, originalNamespace); prepareSecureInsecureParams(parameters.getTechParams(), parameterBundle, ParameterType.TECHNICAL, k8TokenMap, originalNamespace); return parameterBundle; @@ -166,7 +178,7 @@ public void prepareSecureInsecureParams(Map parameters, Param , ParameterType parameterType, Map k8TokenMap, String originalNamespace) { Map securedParams = new TreeMap<>(); Map inSecuredParams = new TreeMap<>(); - if (parameters == null || parameters.isEmpty()) { + if (MapUtils.isEmpty(parameters) && MapUtils.isEmpty(parameterBundle.getCustomTechParameters())) { LOGGER.debug("No Parameters found. Check if the input values are correct"); return; } @@ -180,7 +192,7 @@ public void prepareSecureInsecureParams(Map parameters, Param } else if (parameterType == ParameterType.DEPLOY) { handleDeployParameters(parameterBundle, k8TokenMap, originalNamespace, finalSecuredParams, inSecuredParamsAsObject); } else if (parameterType == ParameterType.TECHNICAL) { - parameterBundle.setSecuredConfigParams(finalSecuredParams); + prepareCustomTechSecureParams(parameterBundle, finalSecuredParams); parameterBundle.setConfigServerParams(inSecuredParamsAsObject); } else if (parameterType == ParameterType.CLEANUP) { finalSecuredParams.put(K8S_TOKEN, k8TokenMap.get(originalNamespace)); @@ -189,6 +201,15 @@ public void prepareSecureInsecureParams(Map parameters, Param } } + private static void prepareCustomTechSecureParams(ParameterBundle parameterBundle, Map finalSecuredParams) { + Map customTechParams = ParametersProcessor.convertParameterMapToObject(parameterBundle.getCustomTechParameters()); + if (MapUtils.isEmpty(finalSecuredParams)) { + parameterBundle.setSecuredConfigParams(new TreeMap<>(customTechParams)); + } else { + parameterBundle.getSecuredConfigParams().putAll(customTechParams); + } + } + private void handleDeployParameters(ParameterBundle parameterBundle, Map k8TokenMap, String originalNamespace, Map finalSecuredParams, Map inSecuredParamsAsObject) { Object appChartName = inSecuredParamsAsObject.get(APPR_CHART_NAME); parameterBundle.setAppChartName(appChartName != null ? appChartName.toString() : ""); diff --git a/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/BindingBaseTest.java b/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/BindingBaseTest.java index 422a47a3e..c67b4c724 100644 --- a/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/BindingBaseTest.java +++ b/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/BindingBaseTest.java @@ -127,7 +127,7 @@ public T withSpan(String spanName, ThrowingSupplier constructor = Binding.class.getDeclaredConstructor(); constructor.setAccessible(true); Binding binding = constructor.newInstance() - .init("tenant", "cloud", "namespace", "application", "namespace"); + .init("tenant", "cloud", "namespace", "application", "namespace", new HashMap<>()); return binding; } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | SecurityException e) { diff --git a/build_pipegene/scripts/effective_set_job.py b/build_pipegene/scripts/effective_set_job.py index 3941531b7..82ba3b383 100644 --- a/build_pipegene/scripts/effective_set_job.py +++ b/build_pipegene/scripts/effective_set_job.py @@ -12,16 +12,18 @@ def prepare_generate_effective_set_job(pipeline, full_env_name, env_name, cluster_name, params): logger.info(f'Prepare generate-effective-set job for {full_env_name}') logger.info(f'Cleanup_targets: {cleanup_targets}') - + app_reg_defs_job = params["APP_REG_DEFS_JOB"] - artifact_app_defs_path = params["APP_DEFS_PATH"] + artifact_app_defs_path = params["APP_DEFS_PATH"] artifact_reg_defs_path = params["REG_DEFS_PATH"] sd_version = params["SD_VERSION"] sd_data = params["SD_DATA"] deployment_id = params["DEPLOYMENT_SESSION_ID"] effective_set_config = params["EFFECTIVE_SET_CONFIG"] tags = params['GITLAB_RUNNER_TAG_NAME'] - + if "CUSTOM_PARAMS" in params: + custom_params = params["CUSTOM_PARAMS"] + is_local_app_def = artifact_app_defs_path and artifact_reg_defs_path and app_reg_defs_job base_dir = getenv('CI_PROJECT_DIR') @@ -48,7 +50,7 @@ def prepare_generate_effective_set_job(pipeline, full_env_name, env_name, cluste "--envs-path=$CI_PROJECT_DIR/environments", f"--output=$CI_PROJECT_DIR/environments/{full_env_name}/effective-set" ] - + effective_set_config_dict = {} if effective_set_config: effective_set_config_dict = json.loads(effective_set_config) @@ -56,10 +58,10 @@ def prepare_generate_effective_set_job(pipeline, full_env_name, env_name, cluste effective_set_version = effective_set_config_dict.get("version") or "v2.0" full_sd_exists = sd_path.is_file() sd_data = bool(sd_data) or bool(sd_version) - + if not (full_sd_exists and sd_data) and effective_set_version.lower() == "v1.0": raise ValueError("Feature generation effective set for pipeline and topology context is not supported for v1.0") - + if full_sd_exists or sd_data: cmdb_cli_cmd_call.extend([ "--registries=${CI_PROJECT_DIR}/configuration/registry.yml", @@ -78,9 +80,12 @@ def prepare_generate_effective_set_job(pipeline, full_env_name, env_name, cluste if deployment_id: cmdb_cli_cmd_call.extend([f"--extra_params=DEPLOYMENT_SESSION_ID={deployment_id}"]) + if custom_params: + logger.info(f"custom_params : {custom_params}") + cmdb_cli_cmd_call.extend([f"--custom-params='{custom_params}'"]) script.append(" ".join(cmdb_cli_cmd_call)) script.append('python3 /module/scripts/main.py encrypt_cred_files') - + generate_effective_set_params = { "name": f'generate_effective_set.{full_env_name}', "image": '${effective_set_generator_image}', @@ -100,7 +105,7 @@ def prepare_generate_effective_set_job(pipeline, full_env_name, env_name, cluste "GITLAB_RUNNER_TAG_NAME": tags, "EXCLUDE_CLEANUP_TARGETS": " ".join(cleanup_targets) } - + needs = [] if is_local_app_def: # gcip library doesn't allow to create a Need object that has the same pipeline as one it runs within. @@ -119,8 +124,8 @@ def prepare_generate_effective_set_job(pipeline, full_env_name, env_name, cluste effective_set_expiry = effective_set_config_dict.get("effective_set_expiry") or "1 hour" logger.info(f"effective set expiry value '{effective_set_expiry}'") generate_effective_set_job.artifacts.expire_in = effective_set_expiry - + generate_effective_set_job.artifacts.when = WhenStatement.ALWAYS pipeline.add_children(generate_effective_set_job) - + return generate_effective_set_job diff --git a/build_pipegene/scripts/gitlab_ci.py b/build_pipegene/scripts/gitlab_ci.py index db6f0d02d..7dd835914 100644 --- a/build_pipegene/scripts/gitlab_ci.py +++ b/build_pipegene/scripts/gitlab_ci.py @@ -155,6 +155,11 @@ def build_pipeline(params: dict) -> None: params) else: logger.info(f'Preparing of generate_effective_set job for {full_env_name} is skipped.') + if "CUSTOM_PARAMS" in params and params["CUSTOM_PARAMS"]: + logger.warning( + "'CUSTOM_PARAMS' is only applied when ['GENERATE_EFFECTIVE_SET'](#generate_effective_set) " + "is 'true'. If 'GENERATE_EFFECTIVE_SET' is 'false', the 'generate_effective_set' job does not run " + "and 'CUSTOM_PARAMS' has no effect.") jobs_requiring_git_commit = ["appregdef_render_job", "process_sd_job", "env_build_job", "generate_effective_set_job", "env_inventory_generation_job", diff --git a/scripts/utils/pipeline_parameters.py b/scripts/utils/pipeline_parameters.py index c5092c524..8a125ffc2 100644 --- a/scripts/utils/pipeline_parameters.py +++ b/scripts/utils/pipeline_parameters.py @@ -41,6 +41,7 @@ def get_pipeline_parameters() -> dict: "APP_REG_DEFS_JOB": getenv("APP_REG_DEFS_JOB"), "EFFECTIVE_SET_CONFIG" : getenv("EFFECTIVE_SET_CONFIG"), "ENV_INVENTORY_CONTENT": getenv("ENV_INVENTORY_CONTENT"), + "CUSTOM_PARAMS" : getenv("CUSTOM_PARAMS"), "ENV_TEMPLATE_VERSION_UPDATE_MODE": getenv( "ENV_TEMPLATE_VERSION_UPDATE_MODE") or TemplateVersionUpdateMode.PERSISTENT.value, } From 6ae97fb6814f99c469dd7b456b06b19feb74e554 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Thu, 5 Mar 2026 10:10:07 +0000 Subject: [PATCH 057/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 6baa4f5cf..53c027ddd 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.29.0" - DOCKER_IMAGE_TAG_ENVGENE: "1.29.0" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.29.0" + DOCKER_IMAGE_TAG_PIPEGENE: "1.30.0" + DOCKER_IMAGE_TAG_ENVGENE: "1.30.0" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.30.0" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 2c2de7140..78cf8f728 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.29.0 +version: 1.30.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index d34707900..ec456c004 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.29.0 +version: 1.30.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 71dffe2c2..bdb36fbcd 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.29.0", + "envgene_version": "1.30.0", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 1d911e85a3186039bc459485fff5debbea24e51f Mon Sep 17 00:00:00 2001 From: popoveugene <42543333+popoveugene@users.noreply.github.com> Date: Fri, 6 Mar 2026 01:40:47 +0300 Subject: [PATCH 058/161] docs: add ES docs (#1069) * docs: add ES docs * docs: wip * docs: wip --- README.md | 5 + docs/README.md | 5 + docs/how-to/generate-effective-set.md | 212 ++++++++++++ docs/tutorials/effective-set.md | 468 ++++++++++++++++++++++++++ 4 files changed, 690 insertions(+) create mode 100644 docs/how-to/generate-effective-set.md create mode 100644 docs/tutorials/effective-set.md diff --git a/README.md b/README.md index b18f42040..dea33aeb0 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ After the pipeline finishes, the Environment configuration will be generated and ### Tutorials +- [**Understanding the Effective Set**](/docs/tutorials/effective-set.md) - Trace how parameters from Tenant, Cloud, Namespace, Application, and SBOM sources are merged into the final Effective Set; learn to read traceability comments and debug wrong values - [**Managing Resource Profiles**](/docs/tutorials/resource-profiles.md) - End-to-end walkthrough: from Baseline to Template Override to Environment-Specific Override, including `template_override`, `overrides-parent`, and result verification ### Core Concepts @@ -141,6 +142,10 @@ After the pipeline finishes, the Environment configuration will be generated and - [**Override Template Parameters**](/docs/how-to/environment-specific-parameters.md) - Override template parameters for specific environments - [**Configure Resource Profiles**](/docs/how-to/configure-resource-profiles.md) - Configure performance parameters for different environment types +**Effective Set:** + +- [**Generate an Effective Set**](/docs/how-to/generate-effective-set.md) - Trigger Effective Set generation from a Solution Descriptor artifact and template version + **Advanced Configuration:** - [**Configure Namespace Names for Sites**](/docs/how-to/configure-ns-names-for-sites.md) - Site-specific namespace naming diff --git a/docs/README.md b/docs/README.md index 03eb958c1..7e2716447 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,6 +15,7 @@ ## Tutorials +- [**Understanding the Effective Set**](/docs/tutorials/effective-set.md) - Trace how parameters from Tenant, Cloud, Namespace, Application, and SBOM sources are merged into the final Effective Set; learn to read traceability comments and debug wrong values - [**Managing Resource Profiles**](/docs/tutorials/resource-profiles.md) - End-to-end walkthrough: from Baseline to Template Override to Environment-Specific Override, including `template_override`, `overrides-parent`, and result verification ## Core Concepts @@ -41,6 +42,10 @@ - [**Override Template Parameters**](/docs/how-to/environment-specific-parameters.md) - Override template parameters for specific environments - [**Configure Resource Profiles**](/docs/how-to/configure-resource-profiles.md) - Configure performance parameters for different environment types +**Effective Set:** + +- [**Generate an Effective Set**](/docs/how-to/generate-effective-set.md) - Trigger Effective Set generation from a Solution Descriptor artifact and template version + **Advanced Configuration:** - [**Configure Namespace Names for Sites**](/docs/how-to/configure-ns-names-for-sites.md) - Site-specific namespace naming diff --git a/docs/how-to/generate-effective-set.md b/docs/how-to/generate-effective-set.md new file mode 100644 index 000000000..8e642030c --- /dev/null +++ b/docs/how-to/generate-effective-set.md @@ -0,0 +1,212 @@ +# How to Generate an Effective Set + +- [How to Generate an Effective Set](#how-to-generate-an-effective-set) + - [Description](#description) + - [Prerequisites](#prerequisites) + - [Steps](#steps) + - [1. Verify the Inventory Is Configured](#1-verify-the-inventory-is-configured) + - [2. Prepare the Solution Descriptor](#2-prepare-the-solution-descriptor) + - [3. Trigger the Pipeline](#3-trigger-the-pipeline) + - [Results](#results) + - [Other Common Scenarios](#other-common-scenarios) + - [Generate Without a Solution Descriptor](#generate-without-a-solution-descriptor) + - [Inject High-Priority Parameters at Runtime](#inject-high-priority-parameters-at-runtime) + +## Description + +This guide explains how to trigger a full Effective Set generation for a specific environment in the Instance Repository. + +The **Effective Set** is the final resolved parameter set for an environment - the output consumed by ArgoCD and other deployment tools. EnvGene calculates it by merging parameters from multiple sources in strict priority order. + +The Effective Set is written to `environments///effective-set/`. + +> [!IMPORTANT] +> The pipeline run is **not atomic**. If any earlier job fails, the Effective Set is not updated. You do not need to prepare the Environment Instance manually - the pipeline builds it as part of the same run. + +## Prerequisites + +1. An Instance Repository exists with the target environment's Inventory configured under `environments///Inventory/` +2. A template artifact is published and accessible (e.g. `env-template:2.5.0`) +3. A Solution Descriptor artifact is published and accessible (e.g. `sd:1.2.3`) - or you intend to generate only the `topology` and `pipeline` contexts (see [Generate Without a Solution Descriptor](#generate-without-a-solution-descriptor)) + +--- + +## Steps + +### 1. Verify the Inventory Is Configured + +Confirm that the environment's Inventory is present in the repository: + +```text +environments/prod-cluster/prod-01/ +└── Inventory/ + └── env_definition.yml +``` + +If `env_definition.yml` is missing, create it before proceeding. See [Environment Inventory](/docs/envgene-configs.md#env_definitionyml). + +--- + +### 2. Prepare the Solution Descriptor + +Provide the SD via one of the following pipeline variables. The pipeline writes it to `environments/prod-cluster/prod-01/Inventory/solution-descriptor/sd.yaml` automatically. + +**Option A - Artifact reference (SD_SOURCE_TYPE + SD_VERSION):** + +The SD is fetched from an artifact registry at pipeline start and written to the repository path above: + +```text +SD_SOURCE_TYPE: artifact +SD_VERSION: sd:1.2.3 +``` + +**Option B - Inline content (SD_SOURCE_TYPE + SD_DATA):** + +Pass the SD content as a JSON string directly as a pipeline variable. Useful for testing or one-off runs: + +```text +SD_SOURCE_TYPE: json +SD_DATA: '{"applications":[{"version":"Cloud-BSS:1.2.3","deployPostfix":"bss"},{"version":"cloud-oss:2.0.1","deployPostfix":"oss"}]}' +``` + +If neither is provided, the existing `sd.yaml` file already committed in the repository is used. + +--- + +### 3. Trigger the Pipeline + +Trigger the Instance pipeline with the following variables: + +| Variable | Value | Description | +|--------------------------|------------------------|----------------------------------------------------| +| `ENV_NAMES` | `prod-cluster/prod-01` | Target environment; comma-separated for multiple | +| `ENV_BUILDER` | `true` | Rebuild the Environment Instance from the template | +| `ENV_TEMPLATE_VERSION` | `env-template:2.5.0` | Template version to use for the Instance build | +| `GENERATE_EFFECTIVE_SET` | `true` | Enable the effective set generation job | +| `SD_SOURCE_TYPE` | `artifact` | How the SD is provided (`artifact` or `json`) | +| `SD_VERSION` | `sd:1.2.3` | SD artifact name and version | + +The pipeline executes the following job sequence: + +```text +appregdef_render → process_sd → env_build → generate_effective_set → git_commit +``` + +If only the Effective Set needs to be regenerated without rebuilding the Environment Instance, set `ENV_BUILDER: false`. The `generate_effective_set` job will use the existing Instance files from the previous run. + +> [!IMPORTANT] +> The `generate_effective_set` job always depends on `env_build`. If `ENV_BUILDER: false` is set but the Environment Instance files are already present from a previous run, generation proceeds normally. + +--- + +## Results + +A successful run produces the following structure under `environments/prod-cluster/prod-01/effective-set/`: + +```text +effective-set/ +├── topology/ +│ ├── parameters.yaml +│ └── credentials.yaml +├── pipeline/ +│ ├── parameters.yaml +│ └── credentials.yaml +├── deployment/ +│ ├── mapping.yml +│ ├── bss/ +│ │ └── Cloud-BSS/ +│ │ └── values/ +│ │ ├── deployment-parameters.yaml +│ │ ├── collision-deployment-parameters.yaml +│ │ ├── credentials.yaml +│ │ ├── collision-credentials.yaml +│ │ ├── deploy-descriptor.yaml +│ │ ├── custom-params.yaml +│ │ └── per-service-parameters/ +│ │ └── cloud-bss-7b/ +│ │ └── deployment-parameters.yaml +│ └── oss/ +│ └── cloud-oss/ +│ └── values/ +│ ├── deployment-parameters.yaml +│ ├── collision-deployment-parameters.yaml +│ ├── credentials.yaml +│ ├── collision-credentials.yaml +│ ├── deploy-descriptor.yaml +│ ├── custom-params.yaml +│ └── per-service-parameters/ +│ └── cloud-oss/ +│ └── deployment-parameters.yaml +├── runtime/ +│ ├── mapping.yml +│ ├── bss/ +│ │ └── Cloud-BSS/ +│ │ ├── parameters.yaml +│ │ └── credentials.yaml +│ └── oss/ +│ └── cloud-oss/ +│ ├── parameters.yaml +│ └── credentials.yaml +└── cleanup/ + ├── mapping.yml + ├── bss/ + │ ├── parameters.yaml + │ └── credentials.yaml + └── oss/ + ├── parameters.yaml + └── credentials.yaml +``` + +The `git_commit` job commits these files to the Instance Repository automatically. The pipeline run is complete when `git_commit` succeeds and the files appear under `environments/prod-cluster/prod-01/effective-set/`. + +--- + +## Other Common Scenarios + +### Generate Without a Solution Descriptor + +When no Solution Descriptor is provided - for example when setting up infrastructure-only namespaces or preparing an environment before any applications are defined - the Effective Set is generated in a partial mode. + +Without an SD, EnvGene does not know which applications belong to which namespaces, so the application-specific contexts cannot be produced. The following contexts are generated normally: + +- `topology` - cluster structure, namespace mapping, composite structure, BG domain +- `pipeline` - orchestration pipeline parameters + +The following contexts are **not generated**: + +- `deployment` - requires application definitions from the SD +- `runtime` - requires application definitions from the SD +- `cleanup` - requires application definitions from the SD + +To use this mode, simply omit `SD_VERSION` and `SD_SOURCE_TYPE` from the pipeline variables. If no SD artifact is passed and no `sd.yaml` exists in the repository, EnvGene skips all application-level processing automatically. + +--- + +### Inject High-Priority Parameters at Runtime + +Use `CUSTOM_PARAMS` to inject parameters at the highest priority level, overriding everything else. This is useful for temporary overrides, incident response, or injecting session-specific values. + +`CUSTOM_PARAMS` accepts a JSON-in-string value: + +```json +{ + "deployment": { + "FEATURE_FLAG_NEW_BILLING": "true", + "MAX_RETRIES": "5" + }, + "runtime": { + "LOG_LEVEL": "DEBUG" + } +} +``` + +Set this as a pipeline variable: + +```text +CUSTOM_PARAMS: '{"deployment":{"FEATURE_FLAG_NEW_BILLING":"true","MAX_RETRIES":"5"}}' +``` + +The injected parameters are written to `custom-params.yaml` inside each application's `values/` folder, applied at the highest priority level after all other values files. + +> [!WARNING] +> Custom Params override all other parameter sources. Use them only for temporary, session-specific values. Do not use Custom Params to replace permanent configuration - use Environment Specific ParameterSets instead. diff --git a/docs/tutorials/effective-set.md b/docs/tutorials/effective-set.md new file mode 100644 index 000000000..78513740b --- /dev/null +++ b/docs/tutorials/effective-set.md @@ -0,0 +1,468 @@ +# Tutorial: Understanding the Effective Set + +- [Tutorial: Understanding the Effective Set](#tutorial-understanding-the-effective-set) + - [What You Will Learn](#what-you-will-learn) + - [Prerequisites](#prerequisites) + - [Scenario](#scenario) + - [Step 1: Understand What the Effective Set Is](#step-1-understand-what-the-effective-set-is) + - [Step 2: Generate the Effective Set](#step-2-generate-the-effective-set) + - [Step 3: Read the Deployment Context](#step-3-read-the-deployment-context) + - [Step 4: Trace How Parameters Flow Into the Effective Set](#step-4-trace-how-parameters-flow-into-the-effective-set) + - [4.1 Resource sizing in per-service-parameters](#41-resource-sizing-in-per-service-parameters) + - [Step 5: Read the Topology Context](#step-5-read-the-topology-context) + - [Step 6: Read the Pipeline Context](#step-6-read-the-pipeline-context) + - [Step 7: Understand Parameter Priority](#step-7-understand-parameter-priority) + - [7.1 Verify priority with traceability comments](#71-verify-priority-with-traceability-comments) + - [Step 8: Understand What Triggers a Regeneration](#step-8-understand-what-triggers-a-regeneration) + - [Summary](#summary) + +## What You Will Learn + +By the end of this tutorial you will know how to: + +- Explain what the Effective Set is, why it exists, and what its contexts are +- Generate the Effective Set with traceability enabled +- Read the deployment context: understand the values files, their priority order, and traceability comments +- Trace how parameters from Tenant, Cloud, Namespace, Application, SBOM, and Resource Profile sources end up in the Effective Set +- Read the topology and pipeline contexts and understand what each one is for +- Use the parameter priority order and traceability comments to debug a wrong value +- Identify what changes require a regeneration + +## Prerequisites + +- A working Instance Repository with at least one environment that has been through `env_build` +- A Solution Descriptor present at `environments///Inventory/solution-descriptor/sd.yaml` +- At least one Application SBOM available in `sboms/` +- Basic familiarity with EnvGene Environment Instance structure (Tenant, Cloud, Namespace objects) + +## Scenario + +You manage a solution called BSS deployed to `prod-cluster/prod-01`. The solution has two namespaces: + +- `bss` - hosts the `Cloud-BSS` application with two services: `bss-processor` and `bss-api` +- `oss` - hosts the `cloud-oss` application with one service: `oss-api` + +You want to understand how the Effective Set is calculated for `prod-cluster/prod-01`, where each value comes from, and how to read the output files to debug a deployment issue. + +## Step 1: Understand What the Effective Set Is + +The Effective Set is the **final, merged parameter file tree** that EnvGene calculates for one environment. It is not something you write manually - EnvGene generates it by merging many input sources in a defined priority order. + +To understand why it exists, consider the problem it solves. Configuration for one environment is spread across many sources: + +- **Template Repository** - Namespace templates, Cloud templates, Template ParameterSets, Resource Profile Overrides. + +- **Instance Repository** - Cloud and Namespace objects with environment-specific parameters, Environment Specific ParameterSets, Resource Profile Overrides, Credentials, Cloud Passport. The Instance stores configuration for the environment itself but does not specify which applications are deployed to which namespaces or at which versions - that is the job of the Solution Descriptor. + +- **Solution Descriptor (SD)** - maps applications to namespaces and defines which version of each application to deploy. Each entry pairs an application version with a `deployPostfix` that identifies the target namespace. For `prod-01` this looks like: + + ```yaml + applications: + - version: "Cloud-BSS:1.2.3" + deployPostfix: "bss" + - version: "cloud-oss:2.0.1" + deployPostfix: "oss" + ``` + + This tells EnvGene that `Cloud-BSS v1.2.3` deploys to the `bss` namespace and `cloud-oss v2.0.1` to the `oss` namespace. Without an SD, EnvGene does not know what applications exist and cannot generate the `deployment`, `cleanup`, or `runtime` contexts. + +- **Application SBOMs** - for each application version listed in the SD, EnvGene reads the corresponding SBOM from `sboms/`. The SBOM is an EnvGene-internal artifact generated automatically per application version. Examples of what it includes: + - The list of microservices the application consists of (determines the structure of `per-service-parameters/`) + - Docker image coordinates for each microservice (written to `deploy-descriptor.yaml`) + - Resource Profile Baselines - named sets of CPU/memory/replica values that serve as the starting point before any overrides are applied. + +No single downstream tool can read all of these sources, understand their merge rules, and resolve Jinja expressions by itself. Instead, EnvGene acts as a pre-processor: it reads all sources, applies all merge rules, and writes a single output - the Effective Set - that contains only final, resolved values with no templates, no references, no indirection. + +The Effective Set for `prod-cluster/prod-01` lives at: + +```text +environments/prod-cluster/prod-01/effective-set/ +``` + +It is divided into five **contexts**. Each context has a format specific to its consumer - a tool reads only its own context and must not access other contexts. This means a change to any input that has **not been followed by a regeneration** is invisible to all consumers. + +> [!NOTE] +> The SD is optional inputs. Without an SD, the `deployment`, `cleanup`, and `runtime` contexts are not generated - only `topology` and `pipeline` are produced. + +| Context | Path | Source in Instance objects | Consumer | +|--------------|-----------------------------|--------------------------------------|-------------------------------| +| `deployment` | `effective-set/deployment/` | `deployParameters`, Application SBOM | ArgoCD, Helm | +| `pipeline` | `effective-set/pipeline/` | `e2eParameters` | Orchestration pipelines | +| `runtime` | `effective-set/runtime/` | `technicalConfigurationParameters` | Runtime configuration tooling | +| `cleanup` | `effective-set/cleanup/` | `deployParameters` | Uninstall tooling | +| `topology` | `effective-set/topology/` | Aggregated environment data | All consumers | + +**deployment** - Helm values applied at deploy time: infrastructure service URLs (Consul, DBaaS, MaaS), platform flags, monitoring settings, application-specific configuration, image tags, resource profile baselines (CPU, memory, replicas). + +**pipeline** - Parameters that configure how the orchestration pipeline operates for this environment: deployment tool URLs, per-namespace deployment policies, notification channels, test system settings, pipeline job references. + +**runtime** - Parameters that configure application behavior while it is already running. Applied via Consul without redeployment, allowing live configuration changes without a new Helm release. + +**cleanup** - Parameters needed during uninstall and teardown operations. + +**topology** - A structural description of the environment: which namespaces exist, how they relate (composite structure, BG domain), and cluster connection details. Unlike the other contexts, `topology` is not tied to a single consumer - any tool that needs to understand the environment layout may read it. + +Each context that contains sensitive parameters includes a `credentials.yaml` alongside the main values file. `credentials.yaml` holds parameters whose values are defined using credential macros in the Environment Instance - those values are resolved and SOPS-encrypted in the output. The unencrypted values are never written to disk. + +In this tutorial you will focus on the `deployment`, `topology`, and `pipeline` contexts, which are the most commonly used. + +## Step 2: Generate the Effective Set + +Trigger the Instance pipeline with the following variables: + +```text +ENV_NAMES: prod-cluster/prod-01 +GENERATE_EFFECTIVE_SET: true +EFFECTIVE_SET_CONFIG: {"enable_traceability":true} +``` + +`EFFECTIVE_SET_CONFIG` is a JSON object. In the pipeline UI you enter it as-is: `{"enable_traceability":true}`. In a YAML file (e.g. `.gitlab-ci.yml`) the value must be quoted and the inner quotes escaped: `"{\"enable_traceability\":true}"`. All fields are optional: + +| Field | Default | Description | +|---------------------------------|----------|-----------------------------------------------------------------| +| `version` | `v2.0` | Effective Set version. `v1.0` is legacy | +| `enable_traceability` | `false` | Add source comments showing each parameter's origin object type | +| `app_chart_validation` | `true` | Fail if any application is not an app chart application | +| `effective_set_expiry` | `1 hour` | CI artifact retention time | +| `contexts.pipeline.consumers[]` | none | Consumer-specific pipeline context schemas | + +`contexts.pipeline.consumers` adds consumer-specific pipeline sub context files alongside the generic `pipeline/parameters.yaml`. For example, adding `{"name":"dcl","version":"v1.0"}` produces `pipeline/dcl-parameters.yaml` and `pipeline/dcl-credentials.yaml`. See [Instance Pipeline Parameters](/docs/instance-pipeline-parameters.md#effective_set_config) for the full schema. + +With `GENERATE_EFFECTIVE_SET: true` set, the pipeline runs: + +```text +generate_effective_set → git_commit +``` + +`generate_effective_set` reads the Environment Instance files that are already in the repository (produced by a previous `env_build` run) and writes the Effective Set on top of them. It does not rebuild the Environment Instance itself. + +After the pipeline completes, pull the latest changes from the repository: + +```bash +git pull origin main +``` + +The generated files are now committed at `environments/prod-cluster/prod-01/effective-set/`. The directory structure for our scenario looks like this: + +```text +effective-set/ +├── deployment/ +│ ├── mapping.yml +│ ├── bss/ +│ │ └── Cloud-BSS/ +│ │ └── values/ +│ │ ├── collision-credentials.yaml +│ │ ├── collision-deployment-parameters.yaml +│ │ ├── credentials.yaml +│ │ ├── custom-params.yaml +│ │ ├── deploy-descriptor.yaml +│ │ ├── deployment-parameters.yaml +│ │ └── per-service-parameters/ +│ │ └── cloud-bss-7b/ +│ │ └── deployment-parameters.yaml +│ └── oss/ +│ └── cloud-oss/ +│ └── values/ +│ ├── collision-credentials.yaml +│ ├── collision-deployment-parameters.yaml +│ ├── credentials.yaml +│ ├── custom-params.yaml +│ ├── deploy-descriptor.yaml +│ ├── deployment-parameters.yaml +│ └── per-service-parameters/ +│ └── cloud-oss/ +│ └── deployment-parameters.yaml +├── topology/ +│ ├── credentials.yaml +│ └── parameters.yaml +├── pipeline/ +│ ├── credentials.yaml +│ └── parameters.yaml +├── runtime/ +│ ├── mapping.yml +│ ├── bss/ +│ │ └── Cloud-BSS/ +│ │ ├── credentials.yaml +│ │ └── parameters.yaml +│ └── oss/ +│ └── cloud-oss/ +│ ├── credentials.yaml +│ └── parameters.yaml +└── cleanup/ + ├── mapping.yml + ├── bss/ + │ ├── credentials.yaml + │ └── parameters.yaml + └── oss/ + ├── credentials.yaml + └── parameters.yaml +``` + +`mapping.yml` appears in the `deployment/`, `runtime/`, and `cleanup/` contexts. It maps each namespace name to its folder path within the context, so consumers can locate the right directory without inferring naming conventions. For example, a consumer looking up parameters for the `prod-01-bss` namespace reads `mapping.yml` to find the folder `bss/`. + +The `per-service-parameters/` subfolder name is application name normalized to comply with Kubernetes naming rules. `Cloud-BSS` becomes `cloud-bss-7b`; `cloud-oss` is already lowercase and stays unchanged. [Section 4.1](#41-resource-sizing-in-per-service-parameters) explains this in detail. + +> [!NOTE] +> This tutorial simplifies two things compared to real usage: +> +> - The SD is already committed to the repository, so no SD parameters are needed. +> - Only `generate_effective_set` is triggered; `env_build` does not run. +> +> In practice, Effective Set generation is a pre-step before deployment. +> A deployment typically means a new application version or a new application being added - the SD carries exactly this information: which applications to deploy and at which versions. +> At the same time, `env_build` ensures the Environment Instance reflects the latest template version before the Effective Set is calculated. +> These steps therefore go together in a single run: +> +> ```text +> ENV_NAMES: prod-cluster/prod-01 +> ENV_BUILDER: true +> ENV_TEMPLATE_VERSION: env-template:2.5.0 +> GENERATE_EFFECTIVE_SET: true +> EFFECTIVE_SET_CONFIG: {"enable_traceability":true} +> SD_SOURCE_TYPE: artifact +> SD_VERSION: sd:1.2.3 +> ``` + +## Step 3: Read the Deployment Context + +The deployment context is organized by namespace and application - each folder name corresponds to the `deployPostfix` value from the SD: + +```text +effective-set/deployment/ +└── / ← namespace folder, e.g. bss + └── / ← application name, e.g. Cloud-BSS + └── values/ ← Helm values files applied in priority order +``` + +Navigate to the `Cloud-BSS` application folder: + +```text +environments/prod-cluster/prod-01/effective-set/deployment/bss/Cloud-BSS/values/ +``` + +For each application the deployer receives a set of values files applied in this priority order (last file wins): + +```text +deploy-descriptor.yaml ← SBOM-derived artifact metadata (image names, tags, chart coordinates) +credentials.yaml ← sensitive user defined params; SOPS-encrypted if configured +deployment-parameters.yaml ← user defined params +collision-deployment-parameters.yaml ← params whose key matches a service name (moved to avoid Helm structure conflicts) +collision-credentials.yaml ← sensitive collision params; SOPS-encrypted if configured +per-service-parameters//deployment-parameters.yaml ← per-microservice resource sizing (all services as keys) +custom-params.yaml ← CUSTOM_PARAMS pipeline variable; highest priority +``` + +`deploy-descriptor.yaml` is generated entirely from the Application SBOM. It contains artifact metadata - Docker image names and tags, Helm chart coordinates, Git revision. It is read-only; users cannot override its values. + +With `enable_traceability: true`, every parameter in `deployment-parameters.yaml` carries an inline comment identifying its source object type. [Step 4](#step-4-trace-how-parameters-flow-into-the-effective-set) traces these in detail. + +## Step 4: Trace How Parameters Flow Into the Effective Set + +With traceability enabled, every parameter in `deployment-parameters.yaml` carries a comment that identifies its source object type. This is the primary tool for debugging a wrong value: find the parameter, read the comment, go to that object. + +Open `environments/prod-cluster/prod-01/effective-set/deployment/bss/Cloud-BSS/values/deployment-parameters.yaml`: + +```yaml +ARGOCD_URL: "https://argocd.prod-cluster.example.com" #cloud +PAAS_PLATFORM: "KUBERNETES" #cloud +BSS_NAMESPACE: "prod-01-bss" #namespace +CUSTOM_HOST: "bss.prod-cluster.example.com" #namespace +SERVICE_TYPE: "ClusterIP" #application +HEALTH_CHECK_PATH: "/api/health" #application +MANAGED_BY: "argocd" #envgene default +``` + +Each comment tells you where to look: + +| Comment | Source object | Where to look in the Instance | +|-----------------------|-------------------------|-----------------------------------------------------| +| `#tenant` | Tenant object | `tenant.yml` or its ParameterSets | +| `#cloud` | Cloud object | `cloud.yml` or its ParameterSets | +| `#namespace` | Namespace object | `Namespaces/bss/namespace.yml` or its ParameterSets | +| `#application` | Application object | `Namespaces/bss/Applications/Cloud-BSS.yml` | +| `#sbom` | Application SBOM | SBOM in `sboms/` - not directly editable | +| `#envgene calculated` | Computed by EnvGene | Not editable - derived value | +| `#envgene default` | EnvGene built-in | Internal default - not configurable | +| `#custom params` | `CUSTOM_PARAMS` var | Pipeline variable - highest priority | + +For the complete list of comment types see the [Calculator CLI Reference](../features/calculator-cli.md#version-20-traceability-comments). + +### 4.1 Resource sizing in per-service-parameters + +The `per-service-parameters/cloud-bss-7b/deployment-parameters.yaml` file uses different comment types because its values come from the SBOM and Resource Profile Overrides: + +```yaml +bss-api: + CPU_REQUEST: 250m #rp-baseline: prod + CPU_LIMIT: 500m #rp-baseline: prod + REPLICAS: 2 #rp-baseline: prod +bss-processor: + CPU_REQUEST: 500m #rp-baseline: prod + CPU_LIMIT: 2000m #rp-baseline: prod + REPLICAS: 5 #rp-override: prod-bss-override +``` + +`#rp-baseline: prod` - value comes from the Resource Profile Baseline named `prod` in the application SBOM. To change it, add a Resource Profile Override in the Instance. + +`#rp-override: prod-bss-override` - the SBOM baseline was overridden by `Profiles/prod-bss-override.yml` in the Instance. + +> [!NOTE] +> If the application is not an app chart (its Helm chart is a flat chart, not an umbrella chart with nested sub-charts), the structure differs: each microservice gets its own subfolder with a flat (non-nested) `deployment-parameters.yaml`. See [Calculator CLI Reference](/docs/features/calculator-cli.md) for details. + +## Step 5: Read the Topology Context + +Open: + +```text +effective-set/topology/parameters.yaml +``` + +The topology context describes the environment's structural layout. It is not per-application - it covers the entire environment: + +```yaml +cluster: + api_url: api.prod-cluster.example.com + api_port: "6443" + protocol: HTTPS + public_url: prod-cluster.example.com + +environments: + prod-cluster/prod-01: + namespaces: + prod-01-bss: + deployPostfix: bss + prod-01-oss: + deployPostfix: oss + +composite_structure: {} + +bg_domain: {} +``` + +The topology context is consumed by orchestration tools that need to understand which namespaces belong to an environment, how they relate to each other, and cluster connection details. `composite_structure` and `bg_domain` are empty when not configured for this environment. + +The `topology/credentials.yaml` file contains per-namespace Kubernetes service account tokens (SOPS-encrypted): + +```yaml +k8s_tokens: + prod-01-bss: ENC[AES256_GCM,data:...] + prod-01-oss: ENC[AES256_GCM,data:...] +``` + +## Step 6: Read the Pipeline Context + +Open: + +```text +effective-set/pipeline/parameters.yaml +``` + +The pipeline context contains parameters consumed by orchestration pipelines - values that control how downstream pipeline jobs run rather than how the application itself is configured: + +```yaml +DCL_CONFIG_ARGOCD_URL: "https://argocd-server.prod-cluster.example.com" +DCL_CONFIG_ARGOCD_PROJECT: "infra" +DCL_CONFIG_DOCKER_REGISTRY: "registry.example.com:17014" +DCL_CONFIG_REGISTRY_URL: "https://registry.example.com" +DCL_CONFIG_SKIP_CREDENTIALS_ENCRYPTION: false +DCL_CONFIG_SOPS_DCL2ARGO_AGE_PUBLIC_KEY: "age1abc123..." +DCL_GIT_BRANCH: master +DCL_GIT_URL: "https://git.example.com/pipelines/gitlab-dcl" +``` + +These parameters come from the `e2eParameters` sections of the Cloud object in the Environment Instance. Sensitive values (ArgoCD credentials, SSO secrets) are split into `pipeline/credentials.yaml` which is SOPS-encrypted. + +> [!NOTE] +> The attribute is called `e2eParameters` for historical reasons. They are general-purpose pipeline orchestration parameters. + +If consumer-specific contexts were configured in `EFFECTIVE_SET_CONFIG`, their files appear alongside the generic ones: + +```text +pipeline/ +├── parameters.yaml +├── credentials.yaml +├── e2e-runner-parameters.yaml ← consumer-specific +└── e2e-runner-credentials.yaml ← consumer-specific +``` + +The consumer-specific files contain a validated subset of the pipeline parameters, filtered through the consumer's JSON schema. + +## Step 7: Understand Parameter Priority + +When the same parameter key is defined at multiple levels, EnvGene applies a strict priority order to decide which value wins. From highest to lowest: + +1. **Custom Params** (`CUSTOM_PARAMS` pipeline variable) - highest, always wins +2. **Resource Profile Override** - instance-level (in `Profiles/`) takes precedence over template-level +3. **Resource Profile Baseline** (from SBOM) +4. **Application object** (`deployParameters` in `Applications/.yml`) +5. **Namespace object** (`deployParameters` in `namespace.yml`) +6. **Cloud object** (`deployParameters` in `cloud.yml`) +7. **Tenant object** (`deployParameters` in `tenant.yml`) + +A practical example: if `Tenant` defines `LOG_LEVEL: INFO` and `Namespace` defines `LOG_LEVEL: DEBUG`, the value in `deployment-parameters.yaml` will be `DEBUG` because Namespace has higher priority than Tenant. + +### 7.1 Verify priority with traceability comments + +Suppose `deployment-parameters.yaml` for `Cloud-BSS` shows: + +```yaml +LOG_LEVEL: "DEBUG" #namespace +``` + +The comment `#namespace` tells you the value is defined at the namespace level - either directly in `namespace.yml` or in a ParameterSet referenced by it. To change it, check `namespace.yml`: + +```text +environments/prod-cluster/prod-01/Namespaces/bss/namespace.yml +``` + +or the ParameterSet files under `Inventory/parameters/` that are assigned to the `bss` namespace. After editing, regenerate the Effective Set. + +The same logic applies to other comments: + +| Comment | Where to look | +|--------------------------|-----------------------------------------------------------| +| `#tenant` | `tenant.yml` or ParameterSets assigned to Tenant | +| `#cloud` | `cloud.yml` or ParameterSets assigned to Cloud | +| `#namespace` | `namespace.yml` or ParameterSets assigned to Namespace | +| `#application` | Application object in `Applications/` | +| `#sbom` | Application SBOM in `sboms/` - not directly editable | +| `#rp-baseline: ` | SBOM baseline - override with a Resource Profile Override | +| `#rp-override: ` | `Profiles/` in the Instance or Template Repository | +| `#envgene calculated` | Derived by EnvGene - not directly editable | +| `#envgene default` | Internal default - not configurable | +| `#custom params` | `CUSTOM_PARAMS` pipeline variable - highest priority | + +## Step 8: Understand What Triggers a Regeneration + +The Effective Set becomes stale whenever any of its inputs change. You must regenerate it in these situations: + +| Change | Required action | +|------------------------------------------------------|--------------------------------------------| +| Environment Specific ParameterSet modified | Regenerate Effective Set | +| Template ParameterSet changed (new template version) | Regenerate both Instance and Effective Set | +| Resource Profile Override modified | Regenerate Effective Set | +| Solution Descriptor applications changed | Update SD, regenerate Effective Set | +| Cloud Passport data updated | Regenerate both Instance and Effective Set | +| Credentials rotated | Regenerate Effective Set | + +## Summary + +In this tutorial you traced the full Effective Set lifecycle for the BSS solution: + +- **What it is** - the final merged parameter tree for one environment, written to `effective-set/` and read by ArgoCD and other consumers. +- **Five contexts** - `deployment` (application parameters and artifacts), `pipeline` (CI/CD parameters), `topology` (cluster layout and structure), `runtime` (runtime management parameters), `cleanup` (teardown parameters). +- **Parameter flow** - Tenant → Cloud → Namespace → Application → SBOM → Resource Profile Override, with each level overriding the previous one. +- **deployment-parameters.yaml** - the merged application parameters; traceability comments show which object type (Cloud, Namespace, Application, etc.) contributed each value. +- **deploy-descriptor.yaml** - SBOM-derived artifact metadata consumed by the deployer to know which Docker images and charts to install. +- **per-service-parameters** - resource sizing per microservice; `#rp-baseline` and `#rp-override` comments identify the source. +- **topology/parameters.yaml** - cluster structure: environment list, namespace mapping, composite structure, BG domain. +- **Priority order** - Custom Params win over everything; Instance ParameterSets override Template ParameterSets; Namespace overrides Cloud overrides Tenant. +- **Regeneration triggers** - any change to inputs (ParameterSets, Resource Profiles, SBOMs, SD, credentials) requires a new Effective Set generation. + +**Related documentation:** + +- [Calculator CLI](/docs/features/calculator-cli.md) +- [How to Generate an Effective Set](/docs/how-to/generate-effective-set.md) +- [How to Override Template Parameters](/docs/how-to/environment-specific-parameters.md) +- [Tutorial: Managing Resource Profiles](/docs/tutorials/resource-profiles.md) +- [Instance Pipeline Parameters](/docs/instance-pipeline-parameters.md) From 9526054b0b27a76ac4ed9f364ded4b4658ddd849 Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:31:37 +0300 Subject: [PATCH 059/161] fix: Updated the release process for GSF packages (#1070) --- .../actions/build-gsf-discovery/action.yml | 38 +++++----------- .github/actions/build-gsf-instance/action.yml | 44 ++++++------------- .github/workflows/docker_publish_release.yml | 44 ++++--------------- .../git_hooks/config.schema.json | 32 +++++++++++++- 4 files changed, 64 insertions(+), 94 deletions(-) diff --git a/.github/actions/build-gsf-discovery/action.yml b/.github/actions/build-gsf-discovery/action.yml index 396a96557..c52688ad8 100644 --- a/.github/actions/build-gsf-discovery/action.yml +++ b/.github/actions/build-gsf-discovery/action.yml @@ -56,6 +56,17 @@ runs: token: ${{ inputs.github-token }} fetch-depth: 0 + - name: Update history.log + shell: bash + run: | + COOKIECUTTER_DIR="gsf_packages/envgene_discovery_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}" + HISTORY_FILE="gsf_packages/envgene_discovery_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/history.log" + + # Run Python script to update history.log + python3 ./.github/scripts/update_history.py \ + "$COOKIECUTTER_DIR" \ + "$HISTORY_FILE" + - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -73,33 +84,6 @@ runs: id: tags uses: ./.github/actions/generate-docker-tags - - name: Update history.log - shell: bash - run: | - COOKIECUTTER_DIR="gsf_packages/envgene_discovery_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}" - HISTORY_FILE="gsf_packages/envgene_discovery_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/history.log" - - # Run Python script to update history.log - python3 ./.github/scripts/update_history.py \ - "$COOKIECUTTER_DIR" \ - "$HISTORY_FILE" - - - name: Commit and push changes - shell: bash - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git remote set-url origin https://x-access-token:${{ inputs.github-token }}@github.com/${GITHUB_REPOSITORY}.git - - BRANCH_NAME=${GITHUB_REF#refs/heads/} - BRANCH_NAME=${BRANCH_NAME#refs/tags/} - - git add -A - if ! git diff --quiet --cached; then - git commit -m "chore: Update GSF Discovery instance package files [skip ci]" - git push origin HEAD:"${BRANCH_NAME}" || echo "Failed to push changes" - fi - - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v5 diff --git a/.github/actions/build-gsf-instance/action.yml b/.github/actions/build-gsf-instance/action.yml index 80529f428..9dac36ed9 100644 --- a/.github/actions/build-gsf-instance/action.yml +++ b/.github/actions/build-gsf-instance/action.yml @@ -56,23 +56,6 @@ runs: token: ${{ inputs.github-token }} fetch-depth: 0 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ inputs.github-token }} - - - name: Generate Docker tags - id: tags - uses: ./.github/actions/generate-docker-tags - - name: Copy config.schema.json to git_hooks shell: bash run: | @@ -91,21 +74,22 @@ runs: "$COOKIECUTTER_DIR" \ "$HISTORY_FILE" - - name: Commit and push changes - shell: bash - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git remote set-url origin https://x-access-token:${{ inputs.github-token }}@github.com/${GITHUB_REPOSITORY}.git + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - BRANCH_NAME=${GITHUB_REF#refs/heads/} - BRANCH_NAME=${BRANCH_NAME#refs/tags/} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ inputs.github-token }} - git add -A - if ! git diff --quiet --cached; then - git commit -m "chore: Update GSF EnvGene instance package files [skip ci]" - git push origin HEAD:"${BRANCH_NAME}" || echo "Failed to push changes" - fi + - name: Generate Docker tags + id: tags + uses: ./.github/actions/generate-docker-tags - name: Extract metadata (tags, labels) for Docker id: meta diff --git a/.github/workflows/docker_publish_release.yml b/.github/workflows/docker_publish_release.yml index 730a95bb5..28e0115c8 100644 --- a/.github/workflows/docker_publish_release.yml +++ b/.github/workflows/docker_publish_release.yml @@ -86,7 +86,7 @@ jobs: fetch-depth: 0 token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }} - - name: Update Docker Image Tags in Pipeline + - name: Add the Envgene version to pipeline and GSF run: | # Paths to files TARGET_FILE="github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml" @@ -110,46 +110,18 @@ jobs: # Update version in GSF Discovery package.yaml sed -i "/version:/c\\version: ${NEW_TAG}" "$GSF_DISCOVERY_PACKAGE_FILE" - CHANGES_MADE=false - if ! git diff --quiet "$TARGET_FILE"; then - echo "Updated Envgene.yml with docker tags from job outputs" - CHANGES_MADE=true - fi - - if ! git diff --quiet "$ENVGENE_COOKIECUTTER_JSON_FILE"; then - echo "Updated cookiecutter.json with envgene_version" - CHANGES_MADE=true - fi - - if ! git diff --quiet "$GSF_INSTANCE_PACKAGE_FILE"; then - echo "Updated package.yaml with version" - CHANGES_MADE=true - fi - - if ! git diff --quiet "$GSF_DISCOVERY_PACKAGE_FILE"; then - echo "Updated package.yaml with version" - CHANGES_MADE=true - fi - - if [ "$CHANGES_MADE" = false ]; then - echo "No changes to commit - no images were built or tags were already up to date" - fi + git diff - git diff "$TARGET_FILE" || true - git diff "$ENVGENE_COOKIECUTTER_JSON_FILE" || true - git diff "$GSF_INSTANCE_PACKAGE_FILE" || true - git diff "$GSF_DISCOVERY_PACKAGE_FILE" || true - - if [ "$CHANGES_MADE" = true ]; then - git config --global user.name "qubership-actions[bot]" - git config --global user.email "qubership-actions[bot]@users.noreply.github.com" - git add "$TARGET_FILE" "$ENVGENE_COOKIECUTTER_JSON_FILE" "$GSF_INSTANCE_PACKAGE_FILE" "$GSF_DISCOVERY_PACKAGE_FILE" + git config --global user.name "qubership-actions[bot]" + git config --global user.email "qubership-actions[bot]@users.noreply.github.com" + git add . + if ! git diff --quiet --cached; then git commit -m "chore: Update docker image tags and envgene_version for branch ${{ github.ref_name }} [skip ci]" - git push origin HEAD:${{ github.ref_name }} || echo "Failed to push changes" else - echo "No changes to commit - skipping git operations" + echo "No changes to commit - working tree clean" fi + tag: needs: [update_image_tags] uses: netcracker/qubership-workflow-hub/.github/workflows/tag-creator.yml@v1.0.3 diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/git_hooks/config.schema.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/git_hooks/config.schema.json index c8f27ab93..371d097fa 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/git_hooks/config.schema.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/git_hooks/config.schema.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "title": "Generic Configuration", - "description": "Configuration for the for cryptographic operations, artifact discovery, and cloud passport decryption", + "description": "Configuration for cryptographic operations, artifact discovery, SBOM retention, and cloud passport decryption", "additionalProperties": true, "properties": { "crypt": { @@ -57,6 +57,36 @@ "cmdb", "local" ] + }, + "sbom_retention": { + "type": "object", + "title": "SBOM Retention Configuration", + "description": "Configuration for automatic SBOM file cleanup to manage repository size. Triggers only when repository reaches 1200 GB threshold.", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable SBOM Retention", + "description": "Enable or disable SBOM retention cleanup", + "default": false, + "examples": [ + true, + false + ] + }, + "keep_versions_per_app": { + "type": "integer", + "title": "Versions to Keep", + "description": "Number of latest versions to keep per application. Used only when enabled is true.", + "minimum": 1, + "default": 10, + "examples": [ + 5, + 10, + 15 + ] + } + } } } } From 69fba60533b4f1dc484448aead0220d6e88192b8 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Fri, 6 Mar 2026 06:41:18 +0000 Subject: [PATCH 060/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 53c027ddd..0293c73a2 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.30.0" - DOCKER_IMAGE_TAG_ENVGENE: "1.30.0" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.30.0" + DOCKER_IMAGE_TAG_PIPEGENE: "1.30.1" + DOCKER_IMAGE_TAG_ENVGENE: "1.30.1" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.30.1" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 78cf8f728..415036930 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.30.0 +version: 1.30.1 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index ec456c004..f3b8bf236 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.30.0 +version: 1.30.1 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index bdb36fbcd..8aaf97c63 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.30.0", + "envgene_version": "1.30.1", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 02340d54eb7e4d165b3fc55184af24c1c2ba2b3f Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Fri, 6 Mar 2026 11:01:03 +0300 Subject: [PATCH 061/161] feat: Updated the workflows (#1072) --- .github/actions/build-effective-set/action.yml | 2 ++ .github/actions/build-envgene/action.yml | 2 ++ .github/actions/build-gsf-discovery/action.yml | 3 +-- .github/actions/build-gsf-instance/action.yml | 3 +-- .github/actions/build-pipegene/action.yml | 2 ++ .github/actions/build-pipeline/action.yml | 2 ++ .github/workflows/docker_publish_release.yml | 2 +- 7 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/actions/build-effective-set/action.yml b/.github/actions/build-effective-set/action.yml index 73e7aa3c3..322e319cb 100644 --- a/.github/actions/build-effective-set/action.yml +++ b/.github/actions/build-effective-set/action.yml @@ -52,6 +52,8 @@ runs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + ref: ${{ inputs.version != '' && format('v{0}', inputs.version) || github.ref }} - name: Download JAR artifact uses: actions/download-artifact@v4 diff --git a/.github/actions/build-envgene/action.yml b/.github/actions/build-envgene/action.yml index a7024255f..c826802c0 100644 --- a/.github/actions/build-envgene/action.yml +++ b/.github/actions/build-envgene/action.yml @@ -49,6 +49,8 @@ runs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + ref: ${{ inputs.version != '' && format('v{0}', inputs.version) || github.ref }} - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/.github/actions/build-gsf-discovery/action.yml b/.github/actions/build-gsf-discovery/action.yml index c52688ad8..71389c81f 100644 --- a/.github/actions/build-gsf-discovery/action.yml +++ b/.github/actions/build-gsf-discovery/action.yml @@ -53,8 +53,7 @@ runs: - name: Checkout repository uses: actions/checkout@v4 with: - token: ${{ inputs.github-token }} - fetch-depth: 0 + ref: ${{ inputs.version != '' && format('v{0}', inputs.version) || github.ref }} - name: Update history.log shell: bash diff --git a/.github/actions/build-gsf-instance/action.yml b/.github/actions/build-gsf-instance/action.yml index 9dac36ed9..dd99cfe87 100644 --- a/.github/actions/build-gsf-instance/action.yml +++ b/.github/actions/build-gsf-instance/action.yml @@ -53,8 +53,7 @@ runs: - name: Checkout repository uses: actions/checkout@v4 with: - token: ${{ inputs.github-token }} - fetch-depth: 0 + ref: ${{ inputs.version != '' && format('v{0}', inputs.version) || github.ref }} - name: Copy config.schema.json to git_hooks shell: bash diff --git a/.github/actions/build-pipegene/action.yml b/.github/actions/build-pipegene/action.yml index 6f8e7172d..aa9478ae2 100644 --- a/.github/actions/build-pipegene/action.yml +++ b/.github/actions/build-pipegene/action.yml @@ -52,6 +52,8 @@ runs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + ref: ${{ inputs.version != '' && format('v{0}', inputs.version) || github.ref }} - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/.github/actions/build-pipeline/action.yml b/.github/actions/build-pipeline/action.yml index 485a4117e..e8d728535 100644 --- a/.github/actions/build-pipeline/action.yml +++ b/.github/actions/build-pipeline/action.yml @@ -35,6 +35,8 @@ runs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + ref: ${{ inputs.version != '' && format('v{0}', inputs.version) || github.ref }} - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/.github/workflows/docker_publish_release.yml b/.github/workflows/docker_publish_release.yml index 28e0115c8..e44e752ae 100644 --- a/.github/workflows/docker_publish_release.yml +++ b/.github/workflows/docker_publish_release.yml @@ -186,7 +186,7 @@ jobs: java-version: 17 upload-artifact: true artifact-id: effective_set_jar - ref: ${{ github.ref }} + ref: v${{ github.event.inputs.version }} secrets: maven-token: ${{ secrets.GITHUB_TOKEN }} From b657e75fbdfafaa53b22b2e5ce317eb825b3206b Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Fri, 6 Mar 2026 08:12:25 +0000 Subject: [PATCH 062/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 0293c73a2..520e9acb8 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.30.1" - DOCKER_IMAGE_TAG_ENVGENE: "1.30.1" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.30.1" + DOCKER_IMAGE_TAG_PIPEGENE: "1.30.2" + DOCKER_IMAGE_TAG_ENVGENE: "1.30.2" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.30.2" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 415036930..b22a7e5c1 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.30.1 +version: 1.30.2 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index f3b8bf236..970f1aaea 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.30.1 +version: 1.30.2 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 8aaf97c63..014f238c7 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.30.1", + "envgene_version": "1.30.2", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 647c338cd2c88cb4ce002ec20c78f6d27f97dab0 Mon Sep 17 00:00:00 2001 From: ismglvd-hub Date: Fri, 6 Mar 2026 13:17:29 +0500 Subject: [PATCH 063/161] docs: Align ENV_INVENTORY_CONTENT.credentials documentation and use cases (#1067) * doc: fix ENV_INVENTORY_CONTENT credentials contract docs * docs: fix ENV_INVENTORY_CONTENT credentials contract docs * docs: fix ENV_INVENTORY_CONTENT credentials contract doc --- docs/features/env-inventory-generation.md | 4 +++- docs/use-cases/environment-inventory-generation.md | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/features/env-inventory-generation.md b/docs/features/env-inventory-generation.md index d37df4cd3..4f3e3a31c 100644 --- a/docs/features/env-inventory-generation.md +++ b/docs/features/env-inventory-generation.md @@ -92,6 +92,7 @@ The generated Environment Inventory must be reused by other jobs in the same pip | `credentials[].action` | enum [`create_or_replace`, `delete`] | yes | Operation mode for the Shared Credentials file. See [Actions](#actions) | `create_or_replace` | | `credentials[].place` | enum[`site`,`cluster`,`env`] | yes | Defines where the Shared Credentials file is stored. See [Paths by place](#paths-by-place) | `site` | | `credentials[].content` | hashmap | no | Shared Credential as file content. Must be valid according [schema](/schemas/credential.schema.json) | See [example below](#full-env_inventory_content-example) | +| `credentials[].name` | string | yes | Name of the shared credentials file. The file will be saved as `.yml` | | | `resourceProfiles` | array | no | List of Resource Profile Override operations | See [example below](#full-env_inventory_content-example) | | `resourceProfiles[].action` | enum [`create_or_replace`, `delete`] | yes | Operation mode for the Resource Profile Override file. See [Actions](#actions) | `create_or_replace` | | `resourceProfiles[].place` | enum[`site`,`cluster`,`env`] | yes | Defines where the Resource Profile Override file is stored. See [Paths by place](#paths-by-place) | `cluster` | @@ -225,6 +226,7 @@ This example shows how to generate a new Environment Inventory (`env_definition. { "action": "create_or_replace", "place": "site", + "name": "prod-integration-creds", "content": { "prod-integration-creds": { "type": "", @@ -300,7 +302,7 @@ This example shows how to generate a new Environment Inventory (`env_definition. ##### ENV_INVENTORY_CONTENT in JSON-in-string format ```json -"{\"envDefinition\":{\"action\":\"create_or_replace\",\"content\":{\"inventory\":{\"environmentName\":\"env-1\",\"tenantName\":\"Applications\",\"cloudName\":\"cluster-1\",\"description\":\"Fullsample\",\"owners\":\"Qubershipteam\",\"config\":{\"updateRPOverrideNameWithEnvName\":false,\"updateCredIdsWithEnvName\":true}},\"envTemplate\":{\"name\":\"composite-prod\",\"artifact\":\"project-env-template:master_20231024-080204\",\"additionalTemplateVariables\":{\"ci\":{\"CI_PARAM_1\":\"ci-param-val-1\",\"CI_PARAM_2\":\"ci-param-val-2\"},\"e2eParameters\":{\"E2E_PARAM_1\":\"e2e-param-val-1\",\"E2E_PARAM_2\":\"e2e-param-val-2\"}},\"sharedTemplateVariables\":[\"prod-template-variables\",\"sample-cloud-template-variables\"],\"envSpecificParamsets\":{\"bss\":[\"env-specific-bss\"]},\"envSpecificTechnicalParamsets\":{\"bss\":[\"env-specific-tech\"]},\"envSpecificE2EParamsets\":{\"cloud\":[\"cloud-level-params\"]},\"sharedMasterCredentialFiles\":[\"prod-integration-creds\"],\"envSpecificResourceProfiles\":{\"cloud\":[\"cloud-specific-profile\"]}}}},\"paramSets\":[{\"action\":\"create_or_replace\",\"place\":\"env\",\"content\":{\"version\":\"\",\"name\":\"env-specific-bss\",\"parameters\":{\"key\":\"value\"},\"applications\":[]}}],\"credentials\":[{\"action\":\"create_or_replace\",\"place\":\"site\",\"content\":{\"prod-integration-creds\":{\"type\":\"\",\"data\":{\"username\":\"\",\"password\":\"\"}}}}],\"resourceProfiles\":[{\"action\":\"create_or_replace\",\"place\":\"cluster\",\"content\":{\"name\":\"cloud-specific-profile\",\"baseline\":\"dev\",\"description\":\"\",\"applications\":[{\"name\":\"core\",\"version\":\"release-20241103.225817\",\"sd\":\"\",\"services\":[{\"name\":\"operator\",\"parameters\":[{\"name\":\"GATEWAY_MEMORY_LIMIT\",\"value\":\"96Mi\"},{\"name\":\"GATEWAY_CPU_REQUEST\",\"value\":\"50m\"}]}]}],\"version\":0}}],\"sharedTemplateVariables\":[{\"action\":\"create_or_replace\",\"place\":\"site\",\"name\":\"prod-template-variables\",\"content\":{\"TEMPLATE_VAR_1\":\"prod-value-1\",\"TEMPLATE_VAR_2\":\"prod-value-2\",\"nested\":{\"key1\":\"nested-prod-value-1\",\"key2\":\"nested-prod-value-2\"}}},{\"action\":\"create_or_replace\",\"place\":\"cluster\",\"name\":\"sample-cloud-template-variables\",\"content\":{\"CLOUD_VAR_1\":\"cloud-value-1\",\"CLOUD_VAR_2\":\"cloud-value-2\"}}]}" +"{\"envDefinition\":{\"action\":\"create_or_replace\",\"content\":{\"inventory\":{\"environmentName\":\"env-1\",\"tenantName\":\"Applications\",\"cloudName\":\"cluster-1\",\"description\":\"Fullsample\",\"owners\":\"Qubershipteam\",\"config\":{\"updateRPOverrideNameWithEnvName\":false,\"updateCredIdsWithEnvName\":true}},\"envTemplate\":{\"name\":\"composite-prod\",\"artifact\":\"project-env-template:master_20231024-080204\",\"additionalTemplateVariables\":{\"ci\":{\"CI_PARAM_1\":\"ci-param-val-1\",\"CI_PARAM_2\":\"ci-param-val-2\"},\"e2eParameters\":{\"E2E_PARAM_1\":\"e2e-param-val-1\",\"E2E_PARAM_2\":\"e2e-param-val-2\"}},\"sharedTemplateVariables\":[\"prod-template-variables\",\"sample-cloud-template-variables\"],\"envSpecificParamsets\":{\"bss\":[\"env-specific-bss\"]},\"envSpecificTechnicalParamsets\":{\"bss\":[\"env-specific-tech\"]},\"envSpecificE2EParamsets\":{\"cloud\":[\"cloud-level-params\"]},\"sharedMasterCredentialFiles\":[\"prod-integration-creds\"],\"envSpecificResourceProfiles\":{\"cloud\":[\"cloud-specific-profile\"]}}}},\"paramSets\":[{\"action\":\"create_or_replace\",\"place\":\"env\",\"content\":{\"version\":\"\",\"name\":\"env-specific-bss\",\"parameters\":{\"key\":\"value\"},\"applications\":[]}}],\"credentials\":[{\"action\":\"create_or_replace\",\"place\":\"site\",\"name\":\"prod-integration-creds\",\"content\":{\"prod-integration-creds\":{\"type\":\"\",\"data\":{\"username\":\"\",\"password\":\"\"}}}}],\"resourceProfiles\":[{\"action\":\"create_or_replace\",\"place\":\"cluster\",\"content\":{\"name\":\"cloud-specific-profile\",\"baseline\":\"dev\",\"description\":\"\",\"applications\":[{\"name\":\"core\",\"version\":\"release-20241103.225817\",\"sd\":\"\",\"services\":[{\"name\":\"operator\",\"parameters\":[{\"name\":\"GATEWAY_MEMORY_LIMIT\",\"value\":\"96Mi\"},{\"name\":\"GATEWAY_CPU_REQUEST\",\"value\":\"50m\"}]}]}],\"version\":0}}],\"sharedTemplateVariables\":[{\"action\":\"create_or_replace\",\"place\":\"site\",\"name\":\"prod-template-variables\",\"content\":{\"TEMPLATE_VAR_1\":\"prod-value-1\",\"TEMPLATE_VAR_2\":\"prod-value-2\",\"nested\":{\"key1\":\"nested-prod-value-1\",\"key2\":\"nested-prod-value-2\"}}},{\"action\":\"create_or_replace\",\"place\":\"cluster\",\"name\":\"sample-cloud-template-variables\",\"content\":{\"CLOUD_VAR_1\":\"cloud-value-1\",\"CLOUD_VAR_2\":\"cloud-value-2\"}}]}" ``` #### `ENV_SPECIFIC_PARAMS` diff --git a/docs/use-cases/environment-inventory-generation.md b/docs/use-cases/environment-inventory-generation.md index e3ac0ba57..37162b22b 100644 --- a/docs/use-cases/environment-inventory-generation.md +++ b/docs/use-cases/environment-inventory-generation.md @@ -336,6 +336,7 @@ Instance pipeline (GitLab or GitHub) is started with: - `action: create_or_replace` - `place: env | cluster | site` +- `name: ` - `content` is a credentials map (one or multiple credentials) **Steps:** @@ -346,6 +347,7 @@ Instance pipeline (GitLab or GitHub) is started with: - [`/docs/features/env-inventory-generation.md`](/docs/features/env-inventory-generation.md) - `action == create_or_replace` - `place ∈ { env, cluster, site }` + - `name` is present - `content` is present 3. Resolves target path by `place`: - `place=env` → `/environments///Inventory/credentials/inventory_generation_creds.yml` @@ -382,6 +384,7 @@ Instance pipeline (GitLab or GitHub) is started with: - `action: create_or_replace` - `place: env | cluster | site` +- `name: ` - `content` is a credentials map (one or multiple credentials) **Steps:** @@ -392,6 +395,7 @@ Instance pipeline (GitLab or GitHub) is started with: - [`/docs/features/env-inventory-generation.md`](/docs/features/env-inventory-generation.md) - `action == create_or_replace` - `place ∈ { env, cluster, site }` + - `name` is present - `content` is present 3. Resolves target path by `place`. 4. Replaces the credentials file using `content` (fully overwrites the file). @@ -424,6 +428,7 @@ Instance pipeline (GitLab or GitHub) is started with: - `action: delete` - `place: env | cluster | site` +- `name: ` - `content` is present **Steps:** @@ -433,6 +438,7 @@ Instance pipeline (GitLab or GitHub) is started with: 2. Validates the `credentials[]` item against the request schema: - `action == delete` - `place ∈ { env, cluster, site }` + - `name` is present - `content` is present 3. Resolves target credentials file path by `place`. 4. Deletes the target credentials file if it exists. From ca0e5b752d763fbacd72b91497ace4ec4b1e3ee4 Mon Sep 17 00:00:00 2001 From: miyamuraga <198181742+miyamuraga@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:40:50 +0500 Subject: [PATCH 064/161] feat: sboms retention policy and fix shares too many files and directories between jobs (#1066) --- .github/actions/run-tests/action.yml | 8 +++ .../FileDataRepositoryImpl.java | 2 +- ...livery-20241115.141230-4-RELEASE.sbom.json | 0 ...2.10.5-20241215.141230-3-RELEASE.sbom.json | 0 .../scripts/sboms_retention_policy.py | 34 +++++++++ .../scripts/test_sboms_retention_policy.py | 72 +++++++++++++++++++ build_envgene/scripts/git_commit.sh | 6 +- build_pipegene/scripts/effective_set_job.py | 7 +- build_pipegene/scripts/gitlab_ci.py | 10 ++- .../envgene/envgenehelper/business_helper.py | 4 ++ python/envgene/envgenehelper/constants.py | 2 + python/envgene/envgenehelper/file_helper.py | 41 +++++++++++ python/envgene/envgenehelper/models.py | 7 ++ .../test_helpers/test_helpers.py | 16 +++-- python/envgene/pyproject.toml | 16 +++++ 15 files changed, 213 insertions(+), 12 deletions(-) rename build_effective_set_generator/effective-set-generator/src/test/resources/sboms/{ => MONITORING}/MONITORING-0.91.0-delivery-20241115.141230-4-RELEASE.sbom.json (100%) rename build_effective_set_generator/effective-set-generator/src/test/resources/sboms/{ => postgres}/postgres-pg16-2.10.5-20241215.141230-3-RELEASE.sbom.json (100%) create mode 100644 build_effective_set_generator/scripts/sboms_retention_policy.py create mode 100644 build_effective_set_generator/scripts/test_sboms_retention_policy.py diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index bd90f0003..21b873e36 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -106,3 +106,11 @@ runs: with: name: test_artifact path: tmp/ + + - name: SBOMS RETENTION POLICY test + shell: bash + run: | + cd build_effective_set_generator/scripts + pytest --capture=no -W ignore::DeprecationWarning --junitxml=../../junit.xml + cd ../.. + mv junit.xml junit_build_env.xml diff --git a/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/repository/implementation/FileDataRepositoryImpl.java b/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/repository/implementation/FileDataRepositoryImpl.java index f741b9133..0d6b37314 100644 --- a/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/repository/implementation/FileDataRepositoryImpl.java +++ b/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/repository/implementation/FileDataRepositoryImpl.java @@ -412,7 +412,7 @@ private SBApplicationDTO getSbApplicationDTO(Map> nsWithApp String namespace = applicationDTO.getDeployPostfix(); String appName = applicationDTO.getVersion().split(":")[0]; String appVersion = applicationDTO.getVersion().replace(":", "-"); - String appFileRef = String.format("%s/%s", sharedData.getSbomsPath().get(), appVersion + ".sbom.json"); + String appFileRef = String.format("%s/%s/%s", sharedData.getSbomsPath().get(), appName, appVersion + ".sbom.json"); SBApplicationDTO dto = SBApplicationDTO.builder() .appName(appName) .appVersion(appVersion) diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/sboms/MONITORING-0.91.0-delivery-20241115.141230-4-RELEASE.sbom.json b/build_effective_set_generator/effective-set-generator/src/test/resources/sboms/MONITORING/MONITORING-0.91.0-delivery-20241115.141230-4-RELEASE.sbom.json similarity index 100% rename from build_effective_set_generator/effective-set-generator/src/test/resources/sboms/MONITORING-0.91.0-delivery-20241115.141230-4-RELEASE.sbom.json rename to build_effective_set_generator/effective-set-generator/src/test/resources/sboms/MONITORING/MONITORING-0.91.0-delivery-20241115.141230-4-RELEASE.sbom.json diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/sboms/postgres-pg16-2.10.5-20241215.141230-3-RELEASE.sbom.json b/build_effective_set_generator/effective-set-generator/src/test/resources/sboms/postgres/postgres-pg16-2.10.5-20241215.141230-3-RELEASE.sbom.json similarity index 100% rename from build_effective_set_generator/effective-set-generator/src/test/resources/sboms/postgres-pg16-2.10.5-20241215.141230-3-RELEASE.sbom.json rename to build_effective_set_generator/effective-set-generator/src/test/resources/sboms/postgres/postgres-pg16-2.10.5-20241215.141230-3-RELEASE.sbom.json diff --git a/build_effective_set_generator/scripts/sboms_retention_policy.py b/build_effective_set_generator/scripts/sboms_retention_policy.py new file mode 100644 index 000000000..2cb1a4447 --- /dev/null +++ b/build_effective_set_generator/scripts/sboms_retention_policy.py @@ -0,0 +1,34 @@ +from envgenehelper import getenv_with_error, get_envgene_config_yaml, logger, cleanup_dir_by_size, deleteFileIfExists, \ + cleanup_dir_by_age, get_sboms_dir +from envgenehelper.constants import CI_JOB_ARTIFACT_MAX_SIZE_MB +from envgenehelper.models import SbomRetentionConfig + + +def sboms_retention_policy(): + work_dir = getenv_with_error('CI_PROJECT_DIR') + sboms_dir = get_sboms_dir(work_dir) + config = get_envgene_config_yaml() + sbom_retention = SbomRetentionConfig.model_validate(config.get("sbom_retention", {})) + + if not sbom_retention.enabled: + logger.info("SBOMs retention policy is disabled") + return + + if not sboms_dir.exists(): + logger.warning(f"There is no such directory: {sboms_dir}") + return + + logger.info("SBOMs retention policy is enabled") + for sbom_path in sboms_dir.iterdir(): + if sbom_path.is_file(): + logger.info(f"Removing outdated format file: {sbom_path}") + deleteFileIfExists(sbom_path) + + for app_sbom_dir in sboms_dir.iterdir(): + cleanup_dir_by_age(app_sbom_dir, sbom_retention.keep_versions_per_app) + + cleanup_dir_by_size(sboms_dir, CI_JOB_ARTIFACT_MAX_SIZE_MB) + + +if __name__ == "__main__": + sboms_retention_policy() diff --git a/build_effective_set_generator/scripts/test_sboms_retention_policy.py b/build_effective_set_generator/scripts/test_sboms_retention_policy.py new file mode 100644 index 000000000..e2da85d8b --- /dev/null +++ b/build_effective_set_generator/scripts/test_sboms_retention_policy.py @@ -0,0 +1,72 @@ +import time + +import pytest +from envgenehelper import cleanup_dir_by_age, cleanup_dir_by_size +from envgenehelper.test_helpers import TestHelpers + + +class TestSBOMSRetentionPolicy: + + @pytest.mark.unit + def test_cleanup_dir_by_age_removes_old_files(self, tmp_path): + now = time.time() + files = [ + tmp_path / "old.json", + tmp_path / "mid.json", + tmp_path / "new.json", + ] + TestHelpers.create_file(files[0], mtime=now - 300) + TestHelpers.create_file(files[1], mtime=now - 200) + TestHelpers.create_file(files[2], mtime=now - 100) + + cleanup_dir_by_age(tmp_path, keep_last=2) + + remaining = {f.name for f in tmp_path.iterdir()} + assert remaining == {"mid.json", "new.json"} + + @pytest.mark.unit + def test_cleanup_dir_by_age_keep_all(self, tmp_path): + now = time.time() + files = [ + tmp_path / "file1.json", + tmp_path / "file2.json", + tmp_path / "file3.json", + ] + TestHelpers.create_file(files[0], mtime=now - 180) + TestHelpers.create_file(files[1], mtime=now - 120) + TestHelpers.create_file(files[2], mtime=now - 60) + + cleanup_dir_by_age(tmp_path, keep_last=3) + + remaining = {f.name for f in tmp_path.iterdir()} + assert remaining == {"file1.json", "file2.json", "file3.json"} + + @pytest.mark.unit + def test_cleanup_dir_by_size_within_limit(self, tmp_path): + files = [ + tmp_path / "file1.json", + tmp_path / "file2.json", + tmp_path / "file3.json", + ] + for f in files: + TestHelpers.create_file(f, size=1024) + + cleanup_dir_by_size(tmp_path, max_size_mb=10) + + remaining = {f.name for f in tmp_path.iterdir()} + assert remaining == {"file1.json", "file2.json", "file3.json"} + + @pytest.mark.unit + def test_cleanup_dir_by_size_exceeds(self, tmp_path): + files = [ + tmp_path / "file1.json", + tmp_path / "file2.json", + tmp_path / "file3.json", + ] + for f in files: + TestHelpers.create_file(f, size=1024 * 1024) + + cleanup_dir_by_size(tmp_path, max_size_mb=2) + + remaining = {f.name for f in tmp_path.iterdir()} + assert remaining == set() diff --git a/build_envgene/scripts/git_commit.sh b/build_envgene/scripts/git_commit.sh index 314a75330..3d5934a3a 100755 --- a/build_envgene/scripts/git_commit.sh +++ b/build_envgene/scripts/git_commit.sh @@ -225,8 +225,10 @@ if [ -e /tmp/configuration ]; then fi if [ -e /tmp/sboms ]; then - echo "Restoring config folder" - cp -r /tmp/sboms . + echo "Restoring sboms folder" + rm -rf sboms + mkdir -p sboms + cp -r /tmp/sboms/. sboms/ fi if [ -e /tmp/gitlab-ci ]; then diff --git a/build_pipegene/scripts/effective_set_job.py b/build_pipegene/scripts/effective_set_job.py index 82ba3b383..2da103d2a 100644 --- a/build_pipegene/scripts/effective_set_job.py +++ b/build_pipegene/scripts/effective_set_job.py @@ -4,7 +4,7 @@ from gcip import WhenStatement, Need -from envgenehelper import logger +from envgenehelper import logger, get_sboms_dir from envgenehelper import cleanup_targets from pipeline_helper import job_instance @@ -30,7 +30,7 @@ def prepare_generate_effective_set_job(pipeline, full_env_name, env_name, cluste base_env_path = f"{base_dir}/environments/{full_env_name}" app_defs_path = f"{base_env_path}/AppDefs" reg_defs_path = f"{base_env_path}/RegDefs" - sboms_path = f"{base_dir}/sboms" + sboms_path = get_sboms_dir(base_dir) sd_path = Path(f'{base_dir}/environments/{full_env_name}/Inventory/solution-descriptor/sd.yaml') # TODO it is necessary to remove unnecessary calls, leave only script calls in such jobs! bad for gsf delivery @@ -43,6 +43,7 @@ def prepare_generate_effective_set_job(pipeline, full_env_name, env_name, cluste f'[ -n "$APP_REG_DEFS_JOB" ] && [ -n "$APP_DEFS_PATH" ] && mkdir -p {app_defs_path} && cp -rf {artifact_app_defs_path}/* {app_defs_path}', f'[ -n "$APP_REG_DEFS_JOB" ] && [ -n "$REG_DEFS_PATH" ] && mkdir -p {reg_defs_path} && cp -fr {artifact_reg_defs_path}/* {reg_defs_path}', 'python3 /module/scripts/main.py validate_creds', + 'python3 /module/scripts/sboms_retention_policy.py' ] cmdb_cli_cmd_call = [ @@ -65,7 +66,7 @@ def prepare_generate_effective_set_job(pipeline, full_env_name, env_name, cluste if full_sd_exists or sd_data: cmdb_cli_cmd_call.extend([ "--registries=${CI_PROJECT_DIR}/configuration/registry.yml", - f"--sboms-path={sboms_path}", + f"--sboms-path={str(sboms_path)}", f"--sd-path={sd_path}", ]) diff --git a/build_pipegene/scripts/gitlab_ci.py b/build_pipegene/scripts/gitlab_ci.py index 7dd835914..2376cf58c 100644 --- a/build_pipegene/scripts/gitlab_ci.py +++ b/build_pipegene/scripts/gitlab_ci.py @@ -196,8 +196,14 @@ def build_pipeline(params: dict) -> None: # check out repo only once in the first job of the generated pipeline, later jobs get it through artifacts from each other # purpose: avoid later jobs restoring files that were removed by previous jobs, so git commit job can commit those deletions - for job in sorted_pipeline.find_jobs(JobFilter()): # gets all jobs in pipeline - job.artifacts.add_paths('./') + for job in sorted_pipeline.find_jobs(JobFilter()): + job.artifacts.add_paths( + 'environments/', + 'configuration/', + 'sboms/', + 'templates/' + ) + is_first_job = job.needs is None or len(job.needs) == 0 if not is_first_job: job.add_variables(GIT_CHECKOUT="false") diff --git a/python/envgene/envgenehelper/business_helper.py b/python/envgene/envgenehelper/business_helper.py index cd61f8c41..86879dd10 100644 --- a/python/envgene/envgenehelper/business_helper.py +++ b/python/envgene/envgenehelper/business_helper.py @@ -446,3 +446,7 @@ def get_template_dirs(base_dir: str | None = None) -> dict[NamespaceRole, str]: def is_from_template_dir(file_path: str) -> bool: return bool(TEMPLATE_DIR_PATTERN.search(file_path)) + + +def get_sboms_dir(work_dir) -> Path: + return Path(work_dir) / "sboms" diff --git a/python/envgene/envgenehelper/constants.py b/python/envgene/envgenehelper/constants.py index 136ab51f5..f7f2f3c10 100644 --- a/python/envgene/envgenehelper/constants.py +++ b/python/envgene/envgenehelper/constants.py @@ -9,3 +9,5 @@ "bg_domain.yml", "composite_structure.yml", ] + +CI_JOB_ARTIFACT_MAX_SIZE_MB = 1200 # 80% from limit 1.5 diff --git a/python/envgene/envgenehelper/file_helper.py b/python/envgene/envgenehelper/file_helper.py index 32f7174a8..4210ba9c9 100644 --- a/python/envgene/envgenehelper/file_helper.py +++ b/python/envgene/envgenehelper/file_helper.py @@ -3,6 +3,7 @@ import re import shutil import tarfile +import time import zipfile from typing import Callable from pathlib import Path @@ -258,3 +259,43 @@ def cleanup_dir(path: str): def is_dir_empty(dir_path): dir_path = Path(dir_path) return dir_path.exists() and dir_path.is_dir() and not any(dir_path.iterdir()) + + +def cleanup_dir_by_size(dir_path, max_size_mb): + dir_path = Path(dir_path) + if not dir_path.exists(): + logger.warning(f"Path does not exist: {dir_path}") + return + + mb = 1024 * 1024 + max_size = max_size_mb * mb + + files = [Path(f) for f in findAllFilesInDir(dir_path, "")] + total = sum(f.stat().st_size for f in files) + total_mb = total / mb + + if total <= max_size: + logger.info(f"Directory size {total_mb:.2f} mb within limit {max_size_mb} mb") + return + + logger.info(f"Directory size {total_mb:.2f} mb exceeds limit {max_size_mb} mb, deleting all files in {dir_path}") + for file in files: + logger.info(f"Removing file: {file}") + deleteFileIfExists(file) + + +def cleanup_dir_by_age(dir_path, keep_last: int): + dir_path = Path(dir_path) + + if not dir_path.exists(): + logger.warning(f"Path does not exist: {dir_path}") + return + + files = [f for f in dir_path.iterdir() if f.is_file()] + files.sort(key=lambda f: f.stat().st_mtime, reverse=True) + + if len(files) > keep_last: + logger.info(f"Only {keep_last} files will remain in {dir_path}") + for old_file in files[keep_last:]: + logger.info(f"Removing file: {old_file}") + deleteFileIfExists(old_file) diff --git a/python/envgene/envgenehelper/models.py b/python/envgene/envgenehelper/models.py index 49f60dc95..af7c83730 100644 --- a/python/envgene/envgenehelper/models.py +++ b/python/envgene/envgenehelper/models.py @@ -1,5 +1,7 @@ from enum import Enum +from pydantic import BaseModel, Field + class TemplateVersionUpdateMode(str, Enum): PERSISTENT = "PERSISTENT" @@ -13,3 +15,8 @@ def _missing_(cls, value): if m.value == value: return m return None + + +class SbomRetentionConfig(BaseModel): + enabled: bool = Field(default=False) + keep_versions_per_app: int = Field(default=10, ge=0) diff --git a/python/envgene/envgenehelper/test_helpers/test_helpers.py b/python/envgene/envgenehelper/test_helpers/test_helpers.py index cc71e5356..634ac42de 100644 --- a/python/envgene/envgenehelper/test_helpers/test_helpers.py +++ b/python/envgene/envgenehelper/test_helpers/test_helpers.py @@ -20,7 +20,7 @@ def clean_test_dir(path: str | Path) -> Path: return path @staticmethod - def compare_dirs_content(source_dir, target_dir) -> tuple[list[str], list[str], dict[str,str] | list, list]: + def compare_dirs_content(source_dir, target_dir) -> tuple[list[str], list[str], dict[str, str] | list, list]: source_map = {Path(f).name: f for f in get_all_files_in_dir(source_dir)} target_map = {Path(f).name: f for f in get_all_files_in_dir(target_dir)} @@ -40,10 +40,12 @@ def compare_dirs_content(source_dir, target_dir) -> tuple[list[str], list[str], file1 = os.path.join(source_dir, file) file2 = os.path.join(target_dir, file) with open(file1, 'r') as f1, open(file2, 'r') as f2: - verbose_diff_list = difflib.unified_diff(f1.readlines(), f2.readlines(), fromfile=file1, tofile=file2, lineterm='') + verbose_diff_list = difflib.unified_diff(f1.readlines(), f2.readlines(), fromfile=file1, + tofile=file2, lineterm='') diff_list = [] for line in verbose_diff_list: - if (line.startswith('+') and not line.startswith('+++')) or (line.startswith('-') and not line.startswith('---')): + if (line.startswith('+') and not line.startswith('+++')) or ( + line.startswith('-') and not line.startswith('---')): diff_list.append(line) mismach_dict[file] = ''.join(diff_list) logger.error(f"Diff for {file}:\n{''.join(verbose_diff_list)}") @@ -52,7 +54,8 @@ def compare_dirs_content(source_dir, target_dir) -> tuple[list[str], list[str], return extra_files, missing_files, mismatch, errors @staticmethod - def assert_dirs_content(source_dir, target_dir, check_for_missing_files=False, check_for_extra_files=False, expected_mismatch:dict[str, str] | None=None): + def assert_dirs_content(source_dir, target_dir, check_for_missing_files=False, check_for_extra_files=False, + expected_mismatch: dict[str, str] | None = None): extra_files, missing_files, mismatch, errors = TestHelpers.compare_dirs_content(source_dir, target_dir) if check_for_extra_files: @@ -77,3 +80,8 @@ def create_fake_zip(files: dict[str, str] = None) -> bytes: zf.writestr(filename, content) return zip_buffer.getvalue() + @staticmethod + def create_file(path: Path, size=1024, mtime=None): + path.write_bytes(b"x" * size) + if mtime is not None: + os.utime(path, (mtime, mtime)) diff --git a/python/envgene/pyproject.toml b/python/envgene/pyproject.toml index 3be997722..7d325f5f1 100644 --- a/python/envgene/pyproject.toml +++ b/python/envgene/pyproject.toml @@ -1,7 +1,23 @@ +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + + +[project] +name = "envgenehelper" +version = "0.0.1" +requires-python = "~=3.12" + +dependencies = [ + "pydantic~=2.10.6" +] + + [tool.black] line-length = 120 skip-string-normalization = true + [tool.mypy] warn_return_any = true warn_unused_configs = true From 7ecf56718b945d9fbe2724c1bcec0b159a0f9129 Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:52:48 +0300 Subject: [PATCH 065/161] fix: Updated GSF logic to keep pipeline vars and update its formatting (#1073) --- .../scripts/init.py | 91 ++++++++++++++++--- .../scripts/init.py | 91 ++++++++++++++++--- 2 files changed, 152 insertions(+), 30 deletions(-) diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/scripts/init.py b/gsf_packages/envgene_discovery_project/git-system-follower-package/scripts/init.py index f747891b4..ba68c8be3 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/scripts/init.py +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/scripts/init.py @@ -3,8 +3,49 @@ from git_system_follower.develop.api.templates import create_template, get_template_names # Protected files that should never be deleted -# pipeline_vars: preserve if exists in repo (user customizations) -PROTECTED_FILES = {'history.log', '.gitlab-ci.yml', '.gitignore', 'gitlab-ci/pipeline_vars.yaml', 'gitlab-ci/pipeline_vars.yml'} +PROTECTED_FILES = { + 'history.log', + '.gitlab-ci.yml', + '.gitignore', + 'gitlab-ci/pipeline_vars.yaml', + 'gitlab-ci/pipeline_vars.yml', +} + + +def _migrate_pipeline_vars_format(content: bytes) -> bytes: + """Migrate old .pipeline_vars: wrapper format to new format.""" + text = content.decode('utf-8') + lines = text.splitlines() + + if not lines: + return content + + # Find .pipeline_vars: - can be anywhere in file (after ---, comments, etc.) + start_idx = None + for i, line in enumerate(lines): + if line.strip() == '.pipeline_vars:': + start_idx = i + break + if start_idx is None: + return content # Not old format + + rest = lines[start_idx + 1:] + non_empty = [line for line in rest if line.strip()] + if not non_empty: + return b'---\n' + + min_indent = min(len(line) - len(line.lstrip()) for line in non_empty) + dedented = [] + for line in rest: + if line.strip() and len(line) - len(line.lstrip()) >= min_indent: + dedented.append(line[min_indent:]) + else: + dedented.append(line) + + result = '\n'.join(dedented) + if not result.strip().startswith('---'): + result = '---\n' + result + return result.encode('utf-8') def _delete_files_from_history(parameters: Parameters): @@ -15,15 +56,14 @@ def _delete_files_from_history(parameters: Parameters): history_log_path = cookiecutter_template_dir / 'history.log' if not history_log_path.exists(): - print(f'Warning: history.log not found at {history_log_path}') + print(f'\t\tWarning: history.log not found at {history_log_path}') return - - # Read files from history.log + try: with open(history_log_path, 'r', encoding='utf-8') as f: files_to_delete = {line.strip() for line in f if line.strip()} except Exception as e: - print(f'Warning: Could not read history.log: {e}') + print(f'\t\tWarning: Could not read history.log: {e}') return if not files_to_delete: @@ -48,9 +88,9 @@ def _delete_files_from_history(parameters: Parameters): directories_to_check.add(file_path.parent) file_full_path.unlink() deleted_count += 1 - print(f'Deleted file: {file_path_str}') + print(f'\t\tDeleted file: {file_path_str}') except Exception as e: - print(f'Warning: Could not delete file {file_path_str}: {e}') + print(f'\t\tWarning: Could not delete file {file_path_str}: {e}') # Delete empty directories for dir_path in sorted(directories_to_check, key=lambda p: len(p.parts), reverse=True): @@ -59,12 +99,12 @@ def _delete_files_from_history(parameters: Parameters): try: if not any(dir_full_path.iterdir()): dir_full_path.rmdir() - print(f'Deleted empty directory: {dir_path}') + print(f'\t\tDeleted empty directory: {dir_path}') except Exception: pass if deleted_count > 0: - print(f'Deleted {deleted_count} file(s) from repository') + print(f'\t\tDeleted {deleted_count} file(s) from repository') def main(parameters: Parameters): @@ -85,17 +125,38 @@ def main(parameters: Parameters): variables = parameters.extras.copy() variables.pop('TEMPLATE', None) - create_template(parameters, template, variables) - # Use current working directory as repository root + # Backup pipeline_vars before create_template - never overwrite user's file repo_root = Path.cwd() + pipeline_vars_paths = ['gitlab-ci/pipeline_vars.yaml', 'gitlab-ci/pipeline_vars.yml'] + pipeline_vars_backup = {} + for f in pipeline_vars_paths: + path = repo_root / f + if path.exists() and path.is_file(): + pipeline_vars_backup[f] = path.read_bytes() + print(f'\t\tFile {f} exists. Backed up for preserve') + + create_template(parameters, template, variables) + + # Restore pipeline_vars if it existed - never overwrite user's file + for f in pipeline_vars_paths: + if f in pipeline_vars_backup: + path = repo_root / f + content = pipeline_vars_backup[f] + migrated = _migrate_pipeline_vars_format(content) + if migrated != content: + print(f'\t\tFile {f} migrated to new format. Preserved') + else: + print(f'\t\tFile {f} preserved. Skip overwrite') + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(migrated) + internal_files_to_remove = ['history.log'] - for file_name in internal_files_to_remove: file_path = repo_root / file_name if file_path.exists(): try: file_path.unlink() - print(f'Removed internal package file: {file_name}') + print(f'\t\tRemoved internal file: {file_name}') except Exception as e: - print(f'Warning: Could not remove {file_name}: {e}') + print(f'\t\tWarning: Could not remove {file_name}: {e}') diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/init.py b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/init.py index f747891b4..24426bf3a 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/init.py +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/init.py @@ -3,8 +3,49 @@ from git_system_follower.develop.api.templates import create_template, get_template_names # Protected files that should never be deleted -# pipeline_vars: preserve if exists in repo (user customizations) -PROTECTED_FILES = {'history.log', '.gitlab-ci.yml', '.gitignore', 'gitlab-ci/pipeline_vars.yaml', 'gitlab-ci/pipeline_vars.yml'} +PROTECTED_FILES = { + 'history.log', + '.gitlab-ci.yml', + '.gitignore', + 'gitlab-ci/pipeline_vars.yaml', + 'gitlab-ci/pipeline_vars.yml', +} + + +def _migrate_pipeline_vars_format(content: bytes) -> bytes: + """Migrate old .pipeline_vars: wrapper format to new format.""" + text = content.decode('utf-8') + lines = text.splitlines() + + if not lines: + return content + + # Find .pipeline_vars: - can be anywhere in file (after ---, comments, etc.) + start_idx = None + for i, line in enumerate(lines): + if line.strip() == '.pipeline_vars:': + start_idx = i + break + if start_idx is None: + return content # Not old format + + rest = lines[start_idx + 1:] + non_empty = [line for line in rest if line.strip()] + if not non_empty: + return b'---\n' + + min_indent = min(len(line) - len(line.lstrip()) for line in non_empty) + dedented = [] + for line in rest: + if line.strip() and len(line) - len(line.lstrip()) >= min_indent: + dedented.append(line[min_indent:]) + else: + dedented.append(line) + + result = '\n'.join(dedented) + if not result.strip().startswith('---'): + result = '---\n' + result + return result.encode('utf-8') def _delete_files_from_history(parameters: Parameters): @@ -15,15 +56,14 @@ def _delete_files_from_history(parameters: Parameters): history_log_path = cookiecutter_template_dir / 'history.log' if not history_log_path.exists(): - print(f'Warning: history.log not found at {history_log_path}') + print(f'\t\tWarning: history.log not found at {history_log_path}') return - - # Read files from history.log + try: with open(history_log_path, 'r', encoding='utf-8') as f: files_to_delete = {line.strip() for line in f if line.strip()} except Exception as e: - print(f'Warning: Could not read history.log: {e}') + print(f'\t\tWarning: Could not read history.log: {e}') return if not files_to_delete: @@ -48,9 +88,9 @@ def _delete_files_from_history(parameters: Parameters): directories_to_check.add(file_path.parent) file_full_path.unlink() deleted_count += 1 - print(f'Deleted file: {file_path_str}') + print(f'\t\tDeleted file: {file_path_str}') except Exception as e: - print(f'Warning: Could not delete file {file_path_str}: {e}') + print(f'\t\tWarning: Could not delete file {file_path_str}: {e}') # Delete empty directories for dir_path in sorted(directories_to_check, key=lambda p: len(p.parts), reverse=True): @@ -59,12 +99,12 @@ def _delete_files_from_history(parameters: Parameters): try: if not any(dir_full_path.iterdir()): dir_full_path.rmdir() - print(f'Deleted empty directory: {dir_path}') + print(f'\t\tDeleted empty directory: {dir_path}') except Exception: pass if deleted_count > 0: - print(f'Deleted {deleted_count} file(s) from repository') + print(f'\t\tDeleted {deleted_count} file(s) from repository') def main(parameters: Parameters): @@ -85,17 +125,38 @@ def main(parameters: Parameters): variables = parameters.extras.copy() variables.pop('TEMPLATE', None) - create_template(parameters, template, variables) - # Use current working directory as repository root + # Backup pipeline_vars if exists - do not overwrite user's file repo_root = Path.cwd() + pipeline_vars_paths = ['gitlab-ci/pipeline_vars.yaml', 'gitlab-ci/pipeline_vars.yml'] + pipeline_vars_backup = {} + for f in pipeline_vars_paths: + path = repo_root / f + if path.exists() and path.is_file(): + pipeline_vars_backup[f] = path.read_bytes() + print(f'\t\tFile {f} exists. Backed up for preserve') + + create_template(parameters, template, variables) + + # Restore pipeline_vars if it existed - never overwrite user's file + for f in pipeline_vars_paths: + if f in pipeline_vars_backup: + path = repo_root / f + content = pipeline_vars_backup[f] + migrated = _migrate_pipeline_vars_format(content) + if migrated != content: + print(f'\t\tFile {f} migrated to new format. Preserved') + else: + print(f'\t\tFile {f} preserved. Skip overwrite') + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(migrated) + internal_files_to_remove = ['history.log'] - for file_name in internal_files_to_remove: file_path = repo_root / file_name if file_path.exists(): try: file_path.unlink() - print(f'Removed internal package file: {file_name}') + print(f'\t\tRemoved internal file: {file_name}') except Exception as e: - print(f'Warning: Could not remove {file_name}: {e}') + print(f'\t\tWarning: Could not remove {file_name}: {e}') From 8f1584b6a8600fdea82de65dd2b85bddb2b266a4 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Fri, 6 Mar 2026 13:04:15 +0000 Subject: [PATCH 066/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 520e9acb8..97fca2c20 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.30.2" - DOCKER_IMAGE_TAG_ENVGENE: "1.30.2" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.30.2" + DOCKER_IMAGE_TAG_PIPEGENE: "1.30.3" + DOCKER_IMAGE_TAG_ENVGENE: "1.30.3" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.30.3" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index b22a7e5c1..ee81898f1 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.30.2 +version: 1.30.3 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 970f1aaea..c1d78160e 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.30.2 +version: 1.30.3 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 014f238c7..da8b1bfec 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.30.2", + "envgene_version": "1.30.3", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From cb8156a41cb75856f74906a021a1e5ff661e855b Mon Sep 17 00:00:00 2001 From: popoveugene <42543333+popoveugene@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:10:47 +0300 Subject: [PATCH 067/161] docs: template composition rename (#1074) * docs: template composition rename * docs: wip --- README.md | 16 +++---- docs/README.md | 2 +- docs/analysis/documentation-gaps-analysis.md | 20 ++++---- docs/envgene-objects.md | 31 +++++++----- ...inheritance.md => template-composition.md} | 45 ++++++++++-------- ...tance-1.png => template-composition-1.png} | Bin ...tance-2.png => template-composition-2.png} | Bin ...tance-3.png => template-composition-3.png} | Bin docs/tutorials/resource-profiles.md | 6 +-- 9 files changed, 66 insertions(+), 54 deletions(-) rename docs/features/{template-inheritance.md => template-composition.md} (87%) rename docs/images/{template-inheritance-1.png => template-composition-1.png} (100%) rename docs/images/{template-inheritance-2.png => template-composition-2.png} (100%) rename docs/images/{template-inheritance-3.png => template-composition-3.png} (100%) diff --git a/README.md b/README.md index dea33aeb0..035af5eff 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,13 @@ - [System Requirements](#system-requirements) - [Basic Usage](#basic-usage) - [📚 Documentation](#-documentation) - - [Getting Started](#getting-started) - - [Tutorials](#tutorials) - - [Core Concepts](#core-concepts) - - [How-To Guides](#how-to-guides) - - [Advanced Features](#advanced-features) - - [Examples \& Samples](#examples--samples) - - [Development](#development) + - [Getting Started](#getting-started) + - [Tutorials](#tutorials) + - [Core Concepts](#core-concepts) + - [How-To Guides](#how-to-guides) + - [Advanced Features](#advanced-features) + - [Examples \& Samples](#examples--samples) + - [Development](#development) - [🤝 Contributing](#-contributing) - [📄 License](#-license) @@ -164,7 +164,7 @@ After the pipeline finishes, the Environment configuration will be generated and - [**System Certificate Configuration**](/docs/features/system-certificate.md) - Auto-config system certs for internal registries or TLS services - [**Template Override**](/docs/features/template-override.md) - Use a base Environment template and override parts as needed - [**Automatic Environment Name Derivation**](/docs/features/auto-env-name-derivation.md) - Auto-detect Environment name from folder structure -- [**Template Inheritance**](/docs/features/template-inheritance.md) - Advanced Environment template patterns +- [**Template Composition**](/docs/features/template-composition.md) - Advanced Environment template patterns - [**Blue-Green Deployment**](/docs/features/blue-green-deployment.md) - BG domains, state management, and `bg_manage` pipeline job - [**Resource Profiles**](/docs/features/resource-profile.md) - Baselines and overrides for performance parameters diff --git a/docs/README.md b/docs/README.md index 7e2716447..ec9549f57 100644 --- a/docs/README.md +++ b/docs/README.md @@ -65,7 +65,7 @@ - [**System Certificate Configuration**](/docs/features/system-certificate.md) - Auto-config system certs for internal registries or TLS services - [**Template Override**](/docs/features/template-override.md) - Use a base Environment template and override parts as needed - [**Automatic Environment Name Derivation**](/docs/features/auto-env-name-derivation.md) - Auto-detect Environment name from folder structure -- [**Template Inheritance**](/docs/features/template-inheritance.md) - Advanced Environment template patterns +- [**Template Composition**](/docs/features/template-composition.md) - Advanced Environment template patterns - [**Blue-Green Deployment**](/docs/features/blue-green-deployment.md) - BG domains, state management, and `bg_manage` pipeline job - [**Resource Profiles**](/docs/features/resource-profile.md) - Baselines and overrides for performance parameters - [**SBOM**](/docs/features/sbom.md) - CycloneDX-based artifact and parameter exchange for EnvGene diff --git a/docs/analysis/documentation-gaps-analysis.md b/docs/analysis/documentation-gaps-analysis.md index 9068bf6d3..115eeef81 100644 --- a/docs/analysis/documentation-gaps-analysis.md +++ b/docs/analysis/documentation-gaps-analysis.md @@ -253,10 +253,10 @@ How-to guides are **task-oriented** recipes that guide users through steps to so - Common use cases - Best practices -- ❌ **How to use Template Inheritance** +- ❌ **How to use Template Composition** - Create parent template - Create child template - - Configure inheritance + - Configure composition - Override parent elements --- @@ -293,7 +293,7 @@ How-to guides are **task-oriented** recipes that guide users through steps to so ##### Advanced Scenarios (0% coverage) - ❌ **How to use Template Override** - - When to use override vs inheritance + - When to use override vs composition - Configure override - Override specific sections - Test overrides @@ -581,11 +581,11 @@ why things are the way they are. - Data flow between repositories - Why this architecture -- ❌ **How Template Inheritance works and when to use it** - - Inheritance concept +- ❌ **How Template Composition works and when to use it** + - Composition concept - Use cases - Design patterns - - When NOT to use inheritance + - When NOT to use composition - ❌ **EnvGene Data Model: From Template to Effective Set** - Complete data flow @@ -610,7 +610,7 @@ why things are the way they are. - Consumers of Effective Set - ❌ **ParameterSets concept and parameter hierarchy** - - Parameter inheritance model + - Parameter composition model - Merge strategies - Precedence rules - Design rationale @@ -682,7 +682,7 @@ why things are the way they are. - Complementary use - Choosing between them -- ❌ **Template Inheritance vs Template Override** +- ❌ **Template Composition vs Template Override** - When to use each - Pros and cons - Can they be combined? @@ -975,7 +975,7 @@ Troubleshooting guides reduce support load. │ ├── comparisons/ # 🆕 NEW section │ │ ├── envgene-vs-helm.md │ │ ├── envgene-vs-kustomize.md -│ │ └── inheritance-vs-override.md +│ │ └── composition-vs-override.md │ └── advanced-topics/ # 🆕 NEW section │ ├── scaling-to-100-environments.md │ ├── security-best-practices.md @@ -1243,7 +1243,7 @@ Create documentation for each primary user journey: 3. 📕 `reference/templates/jinja-filters.md` - REFERENCE 4. 📕 `reference/objects/template-descriptor.md` - REFERENCE (exists as part of envgene-objects.md) 5. 📙 `explanation/patterns/template-repository-organization.md` - EXPLANATION -6. 📗 `how-to/advanced/template-inheritance.md` - HOW-TO +6. 📗 `how-to/advanced/template-composition.md` - HOW-TO **Current gaps:** Steps 1, 3, 5, 6 missing diff --git a/docs/envgene-objects.md b/docs/envgene-objects.md index 9f0654f97..0639e257d 100644 --- a/docs/envgene-objects.md +++ b/docs/envgene-objects.md @@ -71,8 +71,8 @@ It has the following structure: ```yaml # Optional -# Template Inheritance configuration -# See details in https://github.com/Netcracker/qubership-envgene/blob/main/docs/features/template-inheritance.md +# Template Composition configuration +# See details in https://github.com/Netcracker/qubership-envgene/blob/main/docs/features/template-composition.md parent-templates: # Optional # Value must be in `application:version` notation @@ -82,8 +82,8 @@ parent-templates: tenant: string # or tenant: - # Template Inheritance configuration - # See details in https://github.com/Netcracker/qubership-envgene/blob/main/docs/features/template-inheritance.md + # Template Composition configuration + # See details in https://github.com/Netcracker/qubership-envgene/blob/main/docs/features/template-composition.md parent: string # Mandatory # Can be specified either as direct template path (string) or as an object @@ -95,17 +95,19 @@ cloud: # Optional # Template Override configuration # See details in https://github.com/Netcracker/qubership-envgene/blob/main/docs/template-override.md - template_override: template_override: # Optional - # Template Inheritance configuration - # See details in https://github.com/Netcracker/qubership-envgene/blob/main/docs/features/template-inheritance.md + # Template Composition configuration + # See details in https://github.com/Netcracker/qubership-envgene/blob/main/docs/features/template-composition.md parent: string # Optional - # Template Inheritance configuration - # See details in https://github.com/Netcracker/qubership-envgene/blob/main/docs/features/template-inheritance.md + # Template Composition configuration + # See details in https://github.com/Netcracker/qubership-envgene/blob/main/docs/features/template-composition.md overrides-parent: + # Optional + # Override the name of the cloud in rendering result + name: string profile: override-profile-name: parent-profile-name: @@ -137,16 +139,19 @@ namespaces: # Optional # Name of Namespace in Parent Template - # See details in https://github.com/Netcracker/qubership-envgene/blob/main/docs/features/template-inheritance.md + # See details in https://github.com/Netcracker/qubership-envgene/blob/main/docs/features/template-composition.md name: string # Optional # Parent template name - # See details in https://github.com/Netcracker/qubership-envgene/blob/main/docs/features/template-inheritance.md + # See details in https://github.com/Netcracker/qubership-envgene/blob/main/docs/features/template-composition.md parent: string # Optional - # Template Inheritance configuration - # See details in https://github.com/Netcracker/qubership-envgene/blob/main/docs/features/template-inheritance.md + # Template Composition configuration + # See details in https://github.com/Netcracker/qubership-envgene/blob/main/docs/features/template-composition.md overrides-parent: + # Optional + # Override the name of the namespace in rendering result + name: string profile: override-profile-name: string parent-profile-name: string diff --git a/docs/features/template-inheritance.md b/docs/features/template-composition.md similarity index 87% rename from docs/features/template-inheritance.md rename to docs/features/template-composition.md index a126557b1..6d40d09fb 100644 --- a/docs/features/template-inheritance.md +++ b/docs/features/template-composition.md @@ -1,13 +1,13 @@ -# Template Inheritance +# Template Composition -- [Template Inheritance](#template-inheritance) +- [Template Composition](#template-composition) - [Problem Statement](#problem-statement) - [Proposed Approach](#proposed-approach) - [Key Capabilities](#key-capabilities) - [Use Cases](#use-cases) - [Case 1](#case-1) - [Case 2](#case-2) - - [Template Inheritance Configuration](#template-inheritance-configuration) + - [Template Composition Configuration](#template-composition-configuration) - [Template Descriptor](#template-descriptor) - [Examples](#examples) - [Child Template Descriptor for One Namespace](#child-template-descriptor-for-one-namespace) @@ -31,7 +31,7 @@ This creates inefficiencies in configuration management, increases error potenti ## Proposed Approach -Introduce **Template Inheritance** - a feature that enables the creation of child templates by inheriting some or all components from one or more parent templates. Supported inheritable components include: +Introduce **Template Composition** - a feature that enables the creation of child templates by composing some or all components from one or more parent templates. Supported components include: - Tenant template - Cloud template @@ -39,17 +39,17 @@ Introduce **Template Inheritance** - a feature that enables the creation of chil This diagram shows parent and child templates with their components. The color of component indicates its source: -![template-inheritance-1.png](/docs/images/template-inheritance-1.png) +![template-composition-1.png](/docs/images/template-composition-1.png) ### Key Capabilities -1. **Selective Inheritance**: - - Inherit some or all components from parents +1. **Selective Composition**: + - Compose some or all components from parents - Optionally override specific parameters of inherited components - Define new components in child template -2. **Component Inheritance Rules**: - - **Inheritable Components**: +2. **Component Composition Rules**: + - **Composable Components**: - Tenant template (cannot be overridden) - Cloud template (override allowed) - Namespace template (override allowed) @@ -62,7 +62,7 @@ This diagram shows parent and child templates with their components. The color o - `e2eParameterSets` - `technicalConfigurationParameterSets` -3. **Inheritance Processing**: +3. **Composition Processing**: - Occurs during child template build in template repository pipeline - Process flow: 1. Download parent template artifacts specified in `parent-templates` section @@ -75,7 +75,7 @@ This diagram shows parent and child templates with their components. The color o 4. **Key Characteristics**: - Built child templates are regular EnvGene artifacts requiring no special handling - Parent templates are regular EnvGene templates needing no special configuration - - Supports multi-level inheritance chains + - Supports multi-level composition chains ### Use Cases @@ -85,27 +85,27 @@ This feature can be used in scenarios where EnvGene manages configuration parame A solution comprises multiple applications, where application's teams develop and provide their respective templates. The team responsible for the overall solution collects these templates, combines them into a product-level template, and adds necessary customizations. -![template-inheritance-2.png](/docs/images/template-inheritance-2.png) +![template-composition-2.png](/docs/images/template-composition-2.png) #### Case 2 A solution consists of application groups (domains). Domain teams develop and provide their templates. The product team aggregates these into a product-level template, adding for example integration parameters. Then, a project team customizes this product template for specific project needs. Here, the product template acts as both a parent and child template. -![template-inheritance-3.png](/docs/images/template-inheritance-3.png) +![template-composition-3.png](/docs/images/template-composition-3.png) -### Template Inheritance Configuration +### Template Composition Configuration -Template inheritance is configured in the [Template Descriptor](/docs/envgene-objects.md#template-descriptor) in the child template repository. Below is a description of such a Template Descriptor +Template composition is configured in the [Template Descriptor](/docs/envgene-objects.md#template-descriptor) in the child template repository. Below is a description of such a Template Descriptor #### Template Descriptor ```yaml --- # Optional -# If not set than no Templates Inheritance is assumed +# If not set than no Template Composition is assumed parent-templates: # Key is a parent template name - # Value is a parent template artifact is a in app:ver notation. SNAPSHOT version is not supported + # Value is a parent template artifact in app:ver notation. SNAPSHOT version is not supported default-bss: bss-product-template:2.0.0 basic-template: basic-product-template:10.1.3 # Optional @@ -113,7 +113,7 @@ parent-templates: composite_structure: "{{ templates_dir }}/env_templates/composite/composite_structure.yml.j2" # Optional # If not set, the most recent Tenant found in the parent templates referenced by the `namespaces` attribute will be used -# It can be string or dict, if string is provide that means that no inheritance is needed and the exact template will be used +# It can be string or dict, if string is provide that means that no composition is needed and the exact template will be used # example of string value tenant: "{{ templates_dir }}/env_templates/default/tenant.yml.j2" # example of dict value @@ -121,7 +121,7 @@ tenant: parent: basic-template # Optional # If not set, the most recent Cloud found in the parent templates referenced by the `namespaces` attribute will be used -# It can be string or dict, if string is provide that means that no inheritance is needed and the exact template will be used +# It can be string or dict, if string is provide that means that no composition is needed and the exact template will be used # example of string value cloud: "{{ templates_dir }}/env_templates/default/cloud.yml.j2" # example of dict value @@ -130,6 +130,9 @@ cloud: # Optional # Section with parameters that should override parent overrides-parent: + # Optional + # Override the name of the cloud in rendering result + name: "override-cloud" # Optional # Section to override resource profile profile: @@ -144,6 +147,7 @@ cloud: baseline-profile-name: dev # Optional. Default value is `false` # Whether to merge parameters from override-profile-name to parent-profile-name + # This mode does not support merging of Jinja template based resource profiles. merge-with-parent: true # Optional # Parameters that extend/override the parent template's values @@ -178,6 +182,9 @@ namespaces: # Optional # Section with parameters that override the parent template's values overrides-parent: + # Optional + # Override the name of the namespace in rendering result + name: "override-ns" # Optional # Section to override resource profile profile: diff --git a/docs/images/template-inheritance-1.png b/docs/images/template-composition-1.png similarity index 100% rename from docs/images/template-inheritance-1.png rename to docs/images/template-composition-1.png diff --git a/docs/images/template-inheritance-2.png b/docs/images/template-composition-2.png similarity index 100% rename from docs/images/template-inheritance-2.png rename to docs/images/template-composition-2.png diff --git a/docs/images/template-inheritance-3.png b/docs/images/template-composition-3.png similarity index 100% rename from docs/images/template-inheritance-3.png rename to docs/images/template-composition-3.png diff --git a/docs/tutorials/resource-profiles.md b/docs/tutorials/resource-profiles.md index cd085c01c..6804d6721 100644 --- a/docs/tutorials/resource-profiles.md +++ b/docs/tutorials/resource-profiles.md @@ -9,7 +9,7 @@ - [2.1 Create the override file](#21-create-the-override-file) - [2.2 Reference the profile in a Namespace template](#22-reference-the-profile-in-a-namespace-template) - [Step 3: Use `template_override` to Differentiate Template Profiles](#step-3-use-template_override-to-differentiate-template-profiles) - - [Step 4: Use `overrides-parent` in an Inherited Template](#step-4-use-overrides-parent-in-an-inherited-template) + - [Step 4: Use `overrides-parent` in a Composed Template](#step-4-use-overrides-parent-in-a-composed-template) - [Step 5: Add an Environment-Specific Override in the Instance Repository](#step-5-add-an-environment-specific-override-in-the-instance-repository) - [5.1 Choose the right scope](#51-choose-the-right-scope) - [5.2 Create the env-specific override file](#52-create-the-env-specific-override-file) @@ -205,7 +205,7 @@ When EnvGene generates an Environment Instance it renders `bss.yml.j2`, then mer `template_override` supports Jinja expressions, so you can also compute profile names dynamically from environment variables. -## Step 4: Use `overrides-parent` in an Inherited Template +## Step 4: Use `overrides-parent` in a Composed Template A different situation arises when you consume a component from an **external artifact** - a parent template referenced via `parent-templates` that you cannot edit directly. `overrides-parent` is the right mechanism for this case: it lets a child Template Descriptor swap or augment the parent's profile without touching the parent artifact. @@ -459,5 +459,5 @@ In this tutorial you walked through the full resource profile management workflo - [Template Resource Profile Override Schema](/docs/envgene-objects.md#template-resource-profile-override) - [Environment Specific Resource Profile Override Schema](/docs/envgene-objects.md#environment-specific-resource-profile-override) - [Template Override Feature](/docs/features/template-override.md) -- [Template Inheritance Feature](/docs/features/template-inheritance.md) +- [Template Composition Feature](/docs/features/template-composition.md) - [Environment Inventory Reference](/docs/envgene-configs.md#env_definitionyml) From 9b4d4ae469350c1fa7575a89c4ec28f5f897f932 Mon Sep 17 00:00:00 2001 From: popoveugene <42543333+popoveugene@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:21:20 +0300 Subject: [PATCH 068/161] docs: fix gb mb (#1075) --- docs/envgene-configs.md | 2 +- docs/features/sbom-retention.md | 8 ++++---- docs/features/sbom.md | 2 +- docs/use-cases/sbom-retention.md | 24 ++++++++++++------------ 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/envgene-configs.md b/docs/envgene-configs.md index 27a39b303..61f972cfc 100644 --- a/docs/envgene-configs.md +++ b/docs/envgene-configs.md @@ -227,7 +227,7 @@ artifact_definitions_discovery_mode: enum [`auto`, `true`, `false`] app_reg_def_mode: enum [`auto`, `cmdb`, `local`] # Optional # SBOM retention configuration -# Triggers during Effective Set generation when repository reaches 1200 GB size threshold +# Triggers during Effective Set generation when repository reaches 1200 MB size threshold sbom_retention: # Optional. Default value - `false` # Enable/disable SBOM retention cleanup diff --git a/docs/features/sbom-retention.md b/docs/features/sbom-retention.md index 34fef2ea0..63f40c403 100644 --- a/docs/features/sbom-retention.md +++ b/docs/features/sbom-retention.md @@ -22,7 +22,7 @@ SBOM (Software Bill of Materials) files are cached in the Instance Repository to - SBOM generation is a computationally expensive operation - SBOM files are cached in `/sboms/` directory for reuse -- [Job artifacts](/docs/dev/job-artifacts.md) size limit is 1500 GB +- [Job artifacts](/docs/dev/job-artifacts.md) size limit is 1500 MB - Without cleanup, the cache grows indefinitely and may reach the size limit ## Solution @@ -31,7 +31,7 @@ Automatic SBOM retention policy that: - Runs during effective set generation when [GENERATE_EFFECTIVE_SET: true](/docs/instance-pipeline-parameters.md#generate_effective_set) - Monitors repository size -- Triggers cleanup when size threshold is reached (1200 GB) +- Triggers cleanup when size threshold is reached (1200 MB) - Keeps N most recent versions per application - Prevents cache growth beyond acceptable limits @@ -52,7 +52,7 @@ Cleanup runs **only** when: 1. `GENERATE_EFFECTIVE_SET: true` 2. `sbom_retention.enabled: true` in configuration -3. Repository size reaches 1200 GB threshold +3. Repository size reaches 1200 MB threshold ## Configuration @@ -62,7 +62,7 @@ SBOM retention is configured in `/configuration/config.yml`. ```yaml # Optional -# Triggers only when repository reaches 1200 GB +# Triggers only when repository reaches 1200 MB sbom_retention: # Optional # Default value: false diff --git a/docs/features/sbom.md b/docs/features/sbom.md index d7847952e..07922ceea 100644 --- a/docs/features/sbom.md +++ b/docs/features/sbom.md @@ -58,7 +58,7 @@ A JSON file compliant with the CycloneDX specification, describing the following Generated SBOM files are cached in the `/sboms/` directory of the Instance Repository to avoid expensive regeneration. -To manage repository size and prevent reaching the 1500 GB limit, EnvGene provides automatic SBOM retention. See [SBOM Retention](/docs/features/sbom-retention.md) for configuration details. +To manage repository size and prevent reaching the 1500 MB limit, EnvGene provides automatic SBOM retention. See [SBOM Retention](/docs/features/sbom-retention.md) for configuration details. ## Use Cases diff --git a/docs/use-cases/sbom-retention.md b/docs/use-cases/sbom-retention.md index d79db0fb9..79f112259 100644 --- a/docs/use-cases/sbom-retention.md +++ b/docs/use-cases/sbom-retention.md @@ -37,7 +37,7 @@ The cleanup logic is triggered during effective set generation and depends on co enabled: false ``` -4. Repository size is 1300 GB (above 1200 GB threshold) +4. Repository size is 1300 MB (above 1200 MB threshold) **Trigger:** @@ -66,7 +66,7 @@ Instance pipeline (GitLab or GitHub) is started with parameters: **Pre-requisites:** 1. Instance Repository exists with `/sboms/` directory -2. SBOM files exist in `/sboms/` directory with total size 800 GB +2. SBOM files exist in `/sboms/` directory with total size 800 MB 3. SBOM retention is **enabled** in `/configuration/config.yml`: ```yaml @@ -75,7 +75,7 @@ Instance pipeline (GitLab or GitHub) is started with parameters: keep_versions_per_app: 10 ``` -4. Repository size is 800 GB (below 1200 GB threshold) +4. Repository size is 800 MB (below 1200 MB threshold) **Trigger:** @@ -90,8 +90,8 @@ Instance pipeline (GitLab or GitHub) is started with parameters: 1. Generates effective set for the environment 2. Checks SBOM retention configuration 3. Finds `sbom_retention.enabled: true` - 4. Checks repository size: 800 GB - 5. Compares with threshold: 800 GB < 1200 GB + 4. Checks repository size: 800 MB + 5. Compares with threshold: 800 MB < 1200 MB 6. Skips SBOM cleanup (threshold not reached) 7. Completes effective set generation @@ -99,7 +99,7 @@ Instance pipeline (GitLab or GitHub) is started with parameters: 1. Effective set is generated successfully 2. No SBOM files are deleted -3. Pipeline log shows: "Repository size (800 GB) below threshold (1200 GB), skipping cleanup" +3. Pipeline log shows: "Repository size (800 MB) below threshold (1200 MB), skipping cleanup" ### UC-SBOM-3: Repository Above Threshold - Cleanup with Default Settings @@ -118,7 +118,7 @@ Instance pipeline (GitLab or GitHub) is started with parameters: keep_versions_per_app: 10 # default value ``` -4. Repository size is 1300 GB (above 1200 GB threshold) +4. Repository size is 1300 MB (above 1200 MB threshold) **Trigger:** @@ -132,7 +132,7 @@ Instance pipeline (GitLab or GitHub) is started with parameters: 1. The `generate_effective_set` job runs in the pipeline: 1. Generates effective set for the environment 2. Checks SBOM retention configuration: enabled with `keep_versions_per_app: 10` - 3. Checks repository size: 1300 GB > 1200 GB threshold + 3. Checks repository size: 1300 MB > 1200 MB threshold 4. Triggers SBOM cleanup process: 1. Scans `/sboms/` directory 2. Groups SBOM files by application name @@ -151,7 +151,7 @@ Instance pipeline (GitLab or GitHub) is started with parameters: - **app-c**: Keeps all 8 versions (no deletion needed) 3. Total files deleted: 7 SBOM files 4. Pipeline log shows: - - "Repository size (1300 GB) above threshold (1200 GB), starting cleanup" + - "Repository size (1300 MB) above threshold (1200 MB), starting cleanup" - "Cleaned up 7 SBOM files" - "Kept 10 versions per application" @@ -170,7 +170,7 @@ Instance pipeline (GitLab or GitHub) is started with parameters: keep_versions_per_app: 3 # only keep 3 most recent versions ``` -4. Repository size is 1350 GB (above 1200 GB threshold) +4. Repository size is 1350 MB (above 1200 MB threshold) **Trigger:** @@ -184,7 +184,7 @@ Instance pipeline (GitLab or GitHub) is started with parameters: 1. The `generate_effective_set` job runs in the pipeline: 1. Generates effective set for the environment 2. Checks SBOM retention configuration: enabled with `keep_versions_per_app: 3` - 3. Checks repository size: 1350 GB > 1200 GB threshold + 3. Checks repository size: 1350 MB > 1200 MB threshold 4. Triggers SBOM cleanup process: 1. Scans `/sboms/` directory 2. Groups SBOM files by application name (finds `postgres`) @@ -201,6 +201,6 @@ Instance pipeline (GitLab or GitHub) is started with parameters: - **Deleted**: `postgres-pg16-2.10.7.sbom.json` through `postgres-pg16-2.10.1.sbom.json` (7 files) 3. Total files deleted: 7 SBOM files 4. Pipeline log shows: - - "Repository size (1350 GB) above threshold (1200 GB), starting cleanup" + - "Repository size (1350 MB) above threshold (1200 MB), starting cleanup" - "Cleaned up 7 SBOM files for postgres" - "Kept 3 versions per application" From 4a231b5f1377a8d46b38cd07f413f3f734b3ab81 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Tue, 10 Mar 2026 06:45:06 +0000 Subject: [PATCH 069/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 97fca2c20..3f715b632 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.30.3" - DOCKER_IMAGE_TAG_ENVGENE: "1.30.3" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.30.3" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.0" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.0" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.0" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index ee81898f1..c3a62a683 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.30.3 +version: 1.31.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index c1d78160e..9c8659b54 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.30.3 +version: 1.31.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index da8b1bfec..4110dabcb 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.30.3", + "envgene_version": "1.31.0", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From fa5bbcb3e0b694d3d3c0627ecd9346e8214351ac Mon Sep 17 00:00:00 2001 From: Siva Reddy Kunduru <35566000+sivareddyit@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:04:37 +0530 Subject: [PATCH 070/161] fix: added check when no custom parameters are passed to cli (#1078) --- .../processor/service/ParametersCalculationServiceV2.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV2.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV2.java index b7b8c9149..34fd04841 100644 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV2.java +++ b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV2.java @@ -202,6 +202,9 @@ public void prepareSecureInsecureParams(Map parameters, Param } private static void prepareCustomTechSecureParams(ParameterBundle parameterBundle, Map finalSecuredParams) { + if (MapUtils.isEmpty(parameterBundle.getCustomTechParameters())) { + return; + } Map customTechParams = ParametersProcessor.convertParameterMapToObject(parameterBundle.getCustomTechParameters()); if (MapUtils.isEmpty(finalSecuredParams)) { parameterBundle.setSecuredConfigParams(new TreeMap<>(customTechParams)); From 038e237c85693963447ca07a6e109ead471ca8ba Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Tue, 10 Mar 2026 07:40:35 +0000 Subject: [PATCH 071/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 3f715b632..f6c94fa8b 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.0" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.0" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.0" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.1" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.1" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.1" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index c3a62a683..95356c965 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.0 +version: 1.31.1 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 9c8659b54..702609d95 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.0 +version: 1.31.1 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 4110dabcb..3b8689a2e 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.0", + "envgene_version": "1.31.1", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 958018197495f1df336fa660ef825c9cce52409c Mon Sep 17 00:00:00 2001 From: popoveugene <42543333+popoveugene@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:03:59 +0300 Subject: [PATCH 072/161] docs: add cmdb.creds.get decprecated macro (#1076) --- docs/template-macros.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/template-macros.md b/docs/template-macros.md index 3bf6382b3..76f2392d5 100644 --- a/docs/template-macros.md +++ b/docs/template-macros.md @@ -70,7 +70,8 @@ - [`cloud`](#cloud) - [`deployer`](#deployer) - [Deprecated Credential Macros](#deprecated-credential-macros) - - [`${envgene.creds.get('').username|password|secret}`](#envgenecredsgetcred-idusernamepasswordsecret) + - [`${envgen.creds.get('').username|password|secret}`](#envgencredsgetcred-idusernamepasswordsecret) + - [`${cmdb.creds.get('').username|password|secret}`](#cmdbcredsgetcred-idusernamepasswordsecret) - [Deprecated Calculator CLI macros](#deprecated-calculator-cli-macros) - [`BASELINE_PROJ`](#baseline_proj) @@ -1256,9 +1257,11 @@ k8s_token: ${creds.get('k8s-cred').secret} ### Deprecated Credential Macros -#### `${envgene.creds.get('').username|password|secret}` +#### `${envgen.creds.get('').username|password|secret}` -**Description:** This macro was used for processing system sensitive parameters—parameters that EnvGene uses to integrate itself with external systems, such as the login and password for a registry or a token for a GitLab instance. +**Replacement**: [`${creds.get('').username|password|secret}`](#credential-macro) + +#### `${cmdb.creds.get('').username|password|secret}` **Replacement**: [`${creds.get('').username|password|secret}`](#credential-macro) From 9694bab7d40c7062cf0faf6f4924365fd24de3a6 Mon Sep 17 00:00:00 2001 From: popoveugene <42543333+popoveugene@users.noreply.github.com> Date: Tue, 10 Mar 2026 23:03:15 +0300 Subject: [PATCH 073/161] docs: change SBOM Storage filestructure (#1092) --- README.md | 7 ++++ docs/README.md | 5 +++ docs/features/sbom-retention.md | 4 +-- docs/features/sbom.md | 28 +++++++++++++-- docs/how-to/sbom-storage-migration.md | 39 ++++++++++++++++++++ docs/tutorials/effective-set.md | 3 +- docs/use-cases/sbom-retention.md | 46 ++++++++++++------------ docs/use-cases/sbom-storage-migration.md | 35 ++++++++++++++++++ 8 files changed, 137 insertions(+), 30 deletions(-) create mode 100644 docs/how-to/sbom-storage-migration.md create mode 100644 docs/use-cases/sbom-storage-migration.md diff --git a/README.md b/README.md index 035af5eff..ce6f8e9b8 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ - [Tutorials](#tutorials) - [Core Concepts](#core-concepts) - [How-To Guides](#how-to-guides) + - [Migrations](#migrations) - [Advanced Features](#advanced-features) - [Examples \& Samples](#examples--samples) - [Development](#development) @@ -150,7 +151,11 @@ After the pipeline finishes, the Environment configuration will be generated and - [**Configure Namespace Names for Sites**](/docs/how-to/configure-ns-names-for-sites.md) - Site-specific namespace naming - [**Credential Encryption**](/docs/how-to/credential-encryption.md) - Secure credential storage and rotation + +### Migrations + - [**Migrate to Dot-Notated Parameters**](/docs/how-to/dot-notated-parameter-migration.md) - Parameter format migration +- [**Migrate SBOM Storage to Per-Application Layout**](/docs/how-to/sbom-storage-migration.md) - Transition to per-application SBOM directory layout when upgrading EnvGene ### Advanced Features @@ -167,6 +172,8 @@ After the pipeline finishes, the Environment configuration will be generated and - [**Template Composition**](/docs/features/template-composition.md) - Advanced Environment template patterns - [**Blue-Green Deployment**](/docs/features/blue-green-deployment.md) - BG domains, state management, and `bg_manage` pipeline job - [**Resource Profiles**](/docs/features/resource-profile.md) - Baselines and overrides for performance parameters +- [**SBOM**](/docs/features/sbom.md) - CycloneDX-based artifact and parameter exchange for EnvGene +- [**SBOM Retention**](/docs/features/sbom-retention.md) - Automatic cleanup of cached SBOM files to manage repository size ### Examples & Samples diff --git a/docs/README.md b/docs/README.md index ec9549f57..053af995e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,6 +5,7 @@ - [Tutorials](#tutorials) - [Core Concepts](#core-concepts) - [How-To Guides](#how-to-guides) + - [Migrations](#migrations) - [Advanced Features](#advanced-features) - [Examples \& Samples](#examples--samples) - [Development](#development) @@ -50,7 +51,11 @@ - [**Configure Namespace Names for Sites**](/docs/how-to/configure-ns-names-for-sites.md) - Site-specific namespace naming - [**Credential Encryption**](/docs/how-to/credential-encryption.md) - Secure credential storage and rotation + +## Migrations + - [**Migrate to Dot-Notated Parameters**](/docs/how-to/dot-notated-parameter-migration.md) - Parameter format migration +- [**Migrate SBOM Storage to Per-Application Layout**](/docs/how-to/sbom-storage-migration.md) - Transition to per-application SBOM directory layout when upgrading EnvGene ## Advanced Features diff --git a/docs/features/sbom-retention.md b/docs/features/sbom-retention.md index 63f40c403..b7a439de0 100644 --- a/docs/features/sbom-retention.md +++ b/docs/features/sbom-retention.md @@ -41,9 +41,9 @@ Automatic SBOM retention policy that: The version-based strategy keeps the N most recent versions for each application: -- Groups SBOM files by application name +- Each application's SBOMs are stored in `/sboms//`; cleanup processes each such subdirectory - Sorts versions by file creation time (newest first) -- Keeps the latest N versions +- Keeps the latest N versions per application directory - Deletes all older versions ### When Cleanup is Triggered diff --git a/docs/features/sbom.md b/docs/features/sbom.md index 07922ceea..c60eff368 100644 --- a/docs/features/sbom.md +++ b/docs/features/sbom.md @@ -9,6 +9,7 @@ - [Application SBOM](#application-sbom) - [Environment Template SBOM](#environment-template-sbom) - [SBOM Storage and Retention](#sbom-storage-and-retention) + - [SBOM directory layout](#sbom-directory-layout) - [Use Cases](#use-cases) - [Affection map](#affection-map) - [Links](#links) @@ -56,13 +57,36 @@ A JSON file compliant with the CycloneDX specification, describing the following ## SBOM Storage and Retention -Generated SBOM files are cached in the `/sboms/` directory of the Instance Repository to avoid expensive regeneration. +Generated SBOM files are cached in the `/sboms/` directory of the Instance Repository to avoid expensive regeneration. Each application's SBOMs are stored in a subdirectory named after the application. To manage repository size and prevent reaching the 1500 MB limit, EnvGene provides automatic SBOM retention. See [SBOM Retention](/docs/features/sbom-retention.md) for configuration details. +### SBOM directory layout + +SBOM files are stored under `/sboms//`. Each file is named `-.sbom.json`. + +- **Root:** `/sboms/` in the Instance Repository +- **Per application:** one subdirectory `/sboms//` +- **Filename:** `-.sbom.json` + +**Example:** + +```text +... +/sboms +├── Cloud-BSS +| ├── Cloud-BSS-1.2.3.sbom.json +| └── Cloud-BSS-1.2.4.sbom.json +└── cloud-oss + └── cloud-oss-2.0.1.sbom.json +``` + +Full path to a single SBOM: `/sboms//-.sbom.json`. This layout keeps all versions of an application together and simplifies retention cleanup per application. + ## Use Cases -1. TBD +- [SBOM Retention](/docs/use-cases/sbom-retention.md) - Cleanup of old SBOM versions when retention threshold is reached +- [SBOM storage migration](/docs/use-cases/sbom-storage-migration.md) - Automatic migration to per-application layout on first run after upgrading EnvGene ## Affection map diff --git a/docs/how-to/sbom-storage-migration.md b/docs/how-to/sbom-storage-migration.md new file mode 100644 index 000000000..3ace9b072 --- /dev/null +++ b/docs/how-to/sbom-storage-migration.md @@ -0,0 +1,39 @@ +# Migrate SBOM Storage to Per-Application Layout + +This guide describes how migration to the per-application SBOM layout works when you run EnvGene with a version that uses the new storage structure. + +- [Migrate SBOM Storage to Per-Application Layout](#migrate-sbom-storage-to-per-application-layout) + - [When to use this guide](#when-to-use-this-guide) + - [How migration works](#how-migration-works) + - [What to expect](#what-to-expect) + +## When to use this guide + +Use this guide when: + +- Your Instance Repository currently has SBOM files stored directly under `/sboms/` (flat layout) +- You are upgrading EnvGene (Instance pipeline) to a version that uses the new per-application SBOM layout + +After the first run with the new version, [SBOM Retention](/docs/features/sbom-retention.md) and Effective Set generation will use the new paths. See [SBOM directory layout](/docs/features/sbom.md#sbom-directory-layout) for the target structure. + +## How migration works + +Migration is **automatic** when you run the Instance pipeline with an EnvGene version that implements the new SBOM storage structure: + +1. **Old SBOMs are removed** - SBOM files in the previous flat location (`/sboms/*.sbom.json`) are deleted by EnvGene. +2. **New SBOMs are generated** - SBOMs are generated and written to the new layout: `/sboms//-.sbom.json`. + +You do **not** need to move or copy files manually. The first pipeline run that includes the `generate_effective_set` job (e.g. with `GENERATE_EFFECTIVE_SET: true`) and uses the new EnvGene version will clear the old flat SBOMs and produce SBOMs in the per-application directories. Because SBOM generation can be expensive, that run may take longer than usual while SBOMs are recreated. + +Example: + +- **Before:** `/sboms/Cloud-BSS-1.2.3.sbom.json`, `/sboms/cloud-oss-2.0.1.sbom.json` +- **After:** `/sboms/Cloud-BSS/Cloud-BSS-1.2.3.sbom.json`, `/sboms/cloud-oss/cloud-oss-2.0.1.sbom.json` + +For the formal scenario (pre-requisites, trigger, steps, results), see [SBOM Storage Migration Use Case](/docs/use-cases/sbom-storage-migration.md). + +## What to expect + +- **First run with new EnvGene version:** Flat SBOMs under `/sboms/` are removed; SBOMs are generated into `/sboms//`. The job may take longer while SBOMs are regenerated. +- **Later runs:** Only the new layout is used; no migration step runs again. +- **Rollback:** If you revert to an older EnvGene version that expects the flat layout, you would need to run that version again; it may regenerate SBOMs in the old flat structure (behavior depends on that version). diff --git a/docs/tutorials/effective-set.md b/docs/tutorials/effective-set.md index 78513740b..e70a4bde9 100644 --- a/docs/tutorials/effective-set.md +++ b/docs/tutorials/effective-set.md @@ -32,7 +32,6 @@ By the end of this tutorial you will know how to: - A working Instance Repository with at least one environment that has been through `env_build` - A Solution Descriptor present at `environments///Inventory/solution-descriptor/sd.yaml` -- At least one Application SBOM available in `sboms/` - Basic familiarity with EnvGene Environment Instance structure (Tenant, Cloud, Namespace objects) ## Scenario @@ -66,7 +65,7 @@ To understand why it exists, consider the problem it solves. Configuration for o This tells EnvGene that `Cloud-BSS v1.2.3` deploys to the `bss` namespace and `cloud-oss v2.0.1` to the `oss` namespace. Without an SD, EnvGene does not know what applications exist and cannot generate the `deployment`, `cleanup`, or `runtime` contexts. -- **Application SBOMs** - for each application version listed in the SD, EnvGene reads the corresponding SBOM from `sboms/`. The SBOM is an EnvGene-internal artifact generated automatically per application version. Examples of what it includes: +- **Application SBOMs** - for each application version listed in the SD, EnvGene reads the corresponding SBOM from `sboms//` (filename: `-.sbom.json`). The SBOM is an EnvGene-internal artifact generated automatically per application version. Examples of what it includes: - The list of microservices the application consists of (determines the structure of `per-service-parameters/`) - Docker image coordinates for each microservice (written to `deploy-descriptor.yaml`) - Resource Profile Baselines - named sets of CPU/memory/replica values that serve as the starting point before any overrides are applied. diff --git a/docs/use-cases/sbom-retention.md b/docs/use-cases/sbom-retention.md index 79f112259..ed93f0e08 100644 --- a/docs/use-cases/sbom-retention.md +++ b/docs/use-cases/sbom-retention.md @@ -10,7 +10,7 @@ ## Overview -This document covers use cases for [SBOM Retention](/docs/features/sbom-retention.md) - automatic cleanup of cached SBOM files to manage Instance Repository size. +This document covers use cases for [SBOM Retention](/docs/features/sbom-retention.md) - automatic cleanup of cached SBOM files to manage Instance Repository size. SBOM files are stored under `/sboms//` as `-.sbom.json` (see [SBOM directory layout](/docs/features/sbom.md#sbom-directory-layout)). ## SBOM Cleanup Execution @@ -20,8 +20,8 @@ The cleanup logic is triggered during effective set generation and depends on co **Pre-requisites:** -1. Instance Repository exists with `/sboms/` directory -2. SBOM files exist in `/sboms/` directory +1. Instance Repository exists with `/sboms/` directory; SBOM files are stored in `/sboms//` +2. SBOM files exist in `/sboms//` 3. SBOM retention is **disabled** in `/configuration/config.yml`: ```yaml @@ -65,8 +65,8 @@ Instance pipeline (GitLab or GitHub) is started with parameters: **Pre-requisites:** -1. Instance Repository exists with `/sboms/` directory -2. SBOM files exist in `/sboms/` directory with total size 800 MB +1. Instance Repository exists with `/sboms/` directory; SBOM files are stored in `/sboms//` +2. SBOM files exist in `/sboms//` with total size 800 MB 3. SBOM retention is **enabled** in `/configuration/config.yml`: ```yaml @@ -106,10 +106,10 @@ Instance pipeline (GitLab or GitHub) is started with parameters: **Pre-requisites:** 1. Instance Repository exists with `/sboms/` directory -2. SBOM files exist for multiple applications: - - `app-a-1.0.15.sbom.json` through `app-a-1.0.1.sbom.json` (15 versions) - - `app-b-2.0.12.sbom.json` through `app-b-2.0.1.sbom.json` (12 versions) - - `app-c-3.5.8.sbom.json` through `app-c-3.5.1.sbom.json` (8 versions) +2. SBOM files exist for multiple applications under per-application subdirectories: + - `/sboms/app-a/`: `app-a-1.0.15.sbom.json` through `app-a-1.0.1.sbom.json` (15 versions) + - `/sboms/app-b/`: `app-b-2.0.12.sbom.json` through `app-b-2.0.1.sbom.json` (12 versions) + - `/sboms/app-c/`: `app-c-3.5.8.sbom.json` through `app-c-3.5.1.sbom.json` (8 versions) 3. SBOM retention is **enabled** with default settings in `/configuration/config.yml`: ```yaml @@ -134,9 +134,8 @@ Instance pipeline (GitLab or GitHub) is started with parameters: 2. Checks SBOM retention configuration: enabled with `keep_versions_per_app: 10` 3. Checks repository size: 1300 MB > 1200 MB threshold 4. Triggers SBOM cleanup process: - 1. Scans `/sboms/` directory - 2. Groups SBOM files by application name - 3. For each application: + 1. Scans `/sboms/` directory (each subdirectory is one application) + 2. For each application subdirectory (e.g. `/sboms/app-a/`): - Sorts versions by file creation time (newest first) - Keeps 10 most recent versions - Deletes older versions @@ -146,9 +145,9 @@ Instance pipeline (GitLab or GitHub) is started with parameters: 1. Effective set is generated successfully 2. SBOM files are cleaned up for each application: - - **app-a**: Keeps versions 1.0.15 through 1.0.6 (10 versions) - - **app-b**: Keeps versions 2.0.12 through 2.0.3 (10 versions) - - **app-c**: Keeps all 8 versions (no deletion needed) + - **app-a** (under `/sboms/app-a/`): Keeps versions 1.0.15 through 1.0.6 (10 versions) + - **app-b** (under `/sboms/app-b/`): Keeps versions 2.0.12 through 2.0.3 (10 versions) + - **app-c** (under `/sboms/app-c/`): Keeps all 8 versions (no deletion needed) 3. Total files deleted: 7 SBOM files 4. Pipeline log shows: - "Repository size (1300 MB) above threshold (1200 MB), starting cleanup" @@ -160,7 +159,7 @@ Instance pipeline (GitLab or GitHub) is started with parameters: **Pre-requisites:** 1. Instance Repository exists with `/sboms/` directory -2. SBOM files exist for application `postgres`: +2. SBOM files exist for application `postgres` under `/sboms/postgres/`: - `postgres-pg16-2.10.10.sbom.json` through `postgres-pg16-2.10.1.sbom.json` (10 versions) 3. SBOM retention is **enabled** with custom settings in `/configuration/config.yml`: @@ -186,19 +185,18 @@ Instance pipeline (GitLab or GitHub) is started with parameters: 2. Checks SBOM retention configuration: enabled with `keep_versions_per_app: 3` 3. Checks repository size: 1350 MB > 1200 MB threshold 4. Triggers SBOM cleanup process: - 1. Scans `/sboms/` directory - 2. Groups SBOM files by application name (finds `postgres`) - 3. Sorts postgres versions by file creation time (newest first) - 4. Keeps 3 most recent versions: 2.10.10, 2.10.9, 2.10.8 - 5. Deletes 7 older versions: 2.10.7 through 2.10.1 + 1. Scans `/sboms/` directory (finds subdirectory `postgres`) + 2. For `/sboms/postgres/`: sorts versions by file creation time (newest first) + 3. Keeps 3 most recent versions: 2.10.10, 2.10.9, 2.10.8 + 4. Deletes 7 older versions: 2.10.7 through 2.10.1 5. Completes effective set generation **Results:** 1. Effective set is generated successfully -2. SBOM files for `postgres` are cleaned up: - - **Kept**: `postgres-pg16-2.10.10.sbom.json`, `postgres-pg16-2.10.9.sbom.json`, `postgres-pg16-2.10.8.sbom.json` - - **Deleted**: `postgres-pg16-2.10.7.sbom.json` through `postgres-pg16-2.10.1.sbom.json` (7 files) +2. SBOM files for `postgres` under `/sboms/postgres/` are cleaned up: + - **Kept**: `/sboms/postgres/postgres-pg16-2.10.10.sbom.json`, `postgres-pg16-2.10.9.sbom.json`, `postgres-pg16-2.10.8.sbom.json` + - **Deleted**: `/sboms/postgres/postgres-pg16-2.10.7.sbom.json` through `postgres-pg16-2.10.1.sbom.json` (7 files) 3. Total files deleted: 7 SBOM files 4. Pipeline log shows: - "Repository size (1350 MB) above threshold (1200 MB), starting cleanup" diff --git a/docs/use-cases/sbom-storage-migration.md b/docs/use-cases/sbom-storage-migration.md new file mode 100644 index 000000000..a3a695672 --- /dev/null +++ b/docs/use-cases/sbom-storage-migration.md @@ -0,0 +1,35 @@ +# SBOM Storage Migration Use Case + +- [SBOM Storage Migration Use Case](#sbom-storage-migration-use-case) + - [Overview](#overview) + - [UC-SBOM-MIG-1: First run after upgrade](#uc-sbom-mig-1-first-run-after-upgrade) + +## Overview + +This document describes the use case for **automatic migration** from the flat SBOM layout to the per-application layout when upgrading EnvGene. For the procedural guide (when to use it, what to expect), see [Migrate SBOM Storage to Per-Application Layout](/docs/how-to/sbom-storage-migration.md). Target layout: [SBOM directory layout](/docs/features/sbom.md#sbom-directory-layout). + +## UC-SBOM-MIG-1: First run after upgrade + +**Pre-requisites:** + +1. Instance Repository has SBOM files in the flat layout: `/sboms/-.sbom.json` +2. EnvGene (Instance pipeline) is upgraded to a version that uses the per-application SBOM layout + +**Trigger:** + +Instance pipeline is run with parameters that trigger effective set generation (e.g. `ENV_NAMES: `, `GENERATE_EFFECTIVE_SET: true`). + +**Steps:** + +1. The pipeline runs with the new EnvGene version. +2. The `generate_effective_set` job (or equivalent) detects the old flat SBOM layout. +3. EnvGene removes all SBOM files from `/sboms/` (flat location). +4. For each application version required by the Solution Descriptor, EnvGene generates the SBOM and writes it to `/sboms//-.sbom.json`. +5. Effective set generation completes using the new SBOM paths. + +**Results:** + +1. No SBOM files remain directly under `/sboms/`; all SBOMs are under `/sboms//`. +2. Effective set is generated successfully. +3. Repository contains the new layout; subsequent runs do not perform migration again. +4. The run may take longer than usual due to SBOM regeneration. From cf8cc86d6f1fa030c00425c614f02cf596ff928a Mon Sep 17 00:00:00 2001 From: ismglvd-hub Date: Wed, 11 Mar 2026 01:34:44 +0500 Subject: [PATCH 074/161] docs: add namespace filtering explanation (#1043) * docs: add namespace filtering documentation * docs: fix link * docs: fix link * docs: removed the link * docs: fix the link * docs: remove link * docs: review update * docs: wip * docs: wip --------- Co-authored-by: popoveugene --- README.md | 2 + docs/README.md | 2 + docs/envgene-objects.md | 24 ++- .../environment-instance-generation.md | 5 +- ...espace-filtering-in-template-descriptor.md | 94 ++++++++++ docs/glossary.md | 13 +- .../filter-ns-in-template-descriptor.md | 160 ++++++++++++++++++ 7 files changed, 286 insertions(+), 14 deletions(-) create mode 100644 docs/features/namespace-filtering-in-template-descriptor.md create mode 100644 docs/how-to/filter-ns-in-template-descriptor.md diff --git a/README.md b/README.md index ce6f8e9b8..151670385 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,7 @@ After the pipeline finishes, the Environment configuration will be generated and **Advanced Configuration:** - [**Configure Namespace Names for Sites**](/docs/how-to/configure-ns-names-for-sites.md) - Site-specific namespace naming +- [**Filter Namespaces in Template Descriptor**](/docs/how-to/filter-ns-in-template-descriptor.md) - Generate Environments with selected namespaces only - [**Credential Encryption**](/docs/how-to/credential-encryption.md) - Secure credential storage and rotation ### Migrations @@ -166,6 +167,7 @@ After the pipeline finishes, the Environment configuration will be generated and - [**Environment Instance Generation**](/docs/features/environment-instance-generation.md) - Generate Environment Instances from templates and inventories - [**Credential Rotation**](/docs/features/cred-rotation.md) - Automate [Credential](/docs/envgene-objects.md#credential) rotation - [**Namespace Render Filter**](/docs/features/namespace-render-filtering.md) - Render only selected [Namespaces](/docs/envgene-objects.md#namespace) +- [**Namespace Filtering in Template Descriptor**](/docs/features/namespace-filtering-in-template-descriptor.md) - Filter namespaces during Template Descriptor rendering - [**System Certificate Configuration**](/docs/features/system-certificate.md) - Auto-config system certs for internal registries or TLS services - [**Template Override**](/docs/features/template-override.md) - Use a base Environment template and override parts as needed - [**Automatic Environment Name Derivation**](/docs/features/auto-env-name-derivation.md) - Auto-detect Environment name from folder structure diff --git a/docs/README.md b/docs/README.md index 053af995e..1f7f1f294 100644 --- a/docs/README.md +++ b/docs/README.md @@ -50,6 +50,7 @@ **Advanced Configuration:** - [**Configure Namespace Names for Sites**](/docs/how-to/configure-ns-names-for-sites.md) - Site-specific namespace naming +- [**Filter Namespaces in Template Descriptor**](/docs/how-to/filter-ns-in-template-descriptor.md) - Generate Environments with selected namespaces only - [**Credential Encryption**](/docs/how-to/credential-encryption.md) - Secure credential storage and rotation ## Migrations @@ -67,6 +68,7 @@ - [**Environment Instance Generation**](/docs/features/environment-instance-generation.md) - Generate Environment Instances from templates and inventories (including BG support) - [**Credential Rotation**](/docs/features/cred-rotation.md) - Automate [Credential](/docs/envgene-objects.md#credential) rotation - [**Namespace Render Filter**](/docs/features/namespace-render-filtering.md) - Render only selected [Namespaces](/docs/envgene-objects.md#namespace) +- [**Namespace Filtering in Template Descriptor**](/docs/features/namespace-filtering-in-template-descriptor.md) - Filter namespaces during Template Descriptor rendering - [**System Certificate Configuration**](/docs/features/system-certificate.md) - Auto-config system certs for internal registries or TLS services - [**Template Override**](/docs/features/template-override.md) - Use a base Environment template and override parts as needed - [**Automatic Environment Name Derivation**](/docs/features/auto-env-name-derivation.md) - Auto-detect Environment name from folder structure diff --git a/docs/envgene-objects.md b/docs/envgene-objects.md index 0639e257d..4c3072e5c 100644 --- a/docs/envgene-objects.md +++ b/docs/envgene-objects.md @@ -65,7 +65,14 @@ This object is a describes the structure of a solution, links to solution's comp The name of this file serves as the name of the Environment Template. In the Environment Inventory, this name is used to specify which Environment Template from the artifact should be used. -**Location:** Any YAML file located in the `/templates/env_templates/` folder is considered a Template Descriptor. +**Location:** Any YAML or Jinja file located in the `/templates/env_templates/` folder is considered a Template Descriptor. + +**Supported file extensions:** + +- `.yml` / `.yaml` — Static Template Descriptor +- `.yml.j2` / `.yaml.j2` — Jinja Template Descriptor (rendered before Environment Instance generation) + +When multiple Template Descriptors with the same base name but different extensions exist, EnvGene selects them in descending priority order: `yml.j2` > `yaml.j2` > `yml` > `yaml`. Jinja Template Descriptors enable conditional namespace inclusion. See [Namespace Filtering in Template Descriptor](/docs/features/namespace-filtering-in-template-descriptor.md) for details. It has the following structure: @@ -1978,7 +1985,7 @@ authConfig: azureArtifactsResource: string # Mandatory mavenConfig: - # Optional + # Mandatory # Pointer to authentication config described in `authConfig` section # Cannot be set in if anonymous access is used authConfig: string @@ -2002,7 +2009,7 @@ mavenConfig: releaseGroup: string # Mandatory dockerConfig: - # Optional + # Mandatory # Pointer to authentication config described in `authConfig` section # Cannot be set in if anonymous access is used authConfig: string @@ -2032,7 +2039,7 @@ dockerConfig: groupName: string # Optional helmConfig: - # Optional + # Mandatory # Pointer to authentication config described in `authConfig` section # Cannot be set in if anonymous access is used authConfig: string @@ -2047,9 +2054,8 @@ helmConfig: helmTargetRelease: string # Optional helmAppConfig: - # Optional + # Mandatory # Pointer to authentication config described in `authConfig` section - # Cannot be set in if anonymous access is used authConfig: string # Mandatory # Domain name of the registry @@ -2068,7 +2074,7 @@ helmAppConfig: helmDevRepoName: string # Optional goConfig: - # Optional + # Mandatory # Pointer to authentication config described in `authConfig` section # Cannot be set in if anonymous access is used authConfig: string @@ -2087,7 +2093,7 @@ goConfig: goProxyRepository: string # Optional npmConfig: - # Optional + # Mandatory # Pointer to authentication config described in `authConfig` section # Cannot be set in if anonymous access is used authConfig: string @@ -2102,7 +2108,7 @@ npmConfig: npmTargetRelease: string # Optional rawConfig: - # Optional + # Mandatory # Pointer to authentication config described in `authConfig` section # Cannot be set in if anonymous access is used authConfig: string diff --git a/docs/features/environment-instance-generation.md b/docs/features/environment-instance-generation.md index cb75a373b..16ad7c94c 100644 --- a/docs/features/environment-instance-generation.md +++ b/docs/features/environment-instance-generation.md @@ -119,6 +119,7 @@ In this example: ## Related Features -- [Namespace Render Filtering](/docs/features/namespace-render-filtering.md) - Uses namespace folder names for filtering -- [Blue-Green Deployment](/docs/features/blue-green-deployment.md) - Describes BG Domain structure +- [Namespace Render Filter](/docs/features/namespace-render-filtering.md) - Select which Namespaces to render in a specific pipeline run +- [Namespace Filtering in Template Descriptor](/docs/features/namespace-filtering-in-template-descriptor.md) - Filter which Namespaces are included in Environment structure during Template Descriptor rendering +- [Blue-Green Deployment](/docs/features/blue-green-deployment.md) - BG domains and state management - [Effective Set Calculator](/docs/features/calculator-cli.md) - Uses folder names for effective set structure diff --git a/docs/features/namespace-filtering-in-template-descriptor.md b/docs/features/namespace-filtering-in-template-descriptor.md new file mode 100644 index 000000000..a2fc65039 --- /dev/null +++ b/docs/features/namespace-filtering-in-template-descriptor.md @@ -0,0 +1,94 @@ +# Namespace Filtering in Template Descriptor + +- [Namespace Filtering in Template Descriptor](#namespace-filtering-in-template-descriptor) + - [Overview](#overview) + - [Problem Statement](#problem-statement) + - [Proposed Approach](#proposed-approach) + - [Example (cluster type filtering)](#example-cluster-type-filtering) + - [File Resolution Priority](#file-resolution-priority) + - [Behavior During Environment Generation](#behavior-during-environment-generation) + - [Scenario: Generating an Environment with namespace filtering](#scenario-generating-an-environment-with-namespace-filtering) + - [What happens when a namespace condition is `false`](#what-happens-when-a-namespace-condition-is-false) + - [How-To](#how-to) + +## Overview + +Namespace Filtering in Template Descriptor allows generating an Environment that includes only a selected subset of namespaces from a unified Environment Template. + +This feature works during Environment Instance generation, and defines the structural composition of the generated Environment. + +## Problem Statement + +We have been using a unified Environment Template for different projects. This template contains all possible namespaces, but during deployment we often need only a subset of available namespaces. + +Currently, there is no possibility to filter namespaces during Environment Instance generation. This leads to the following: + +- The `Namespaces/` folder in the EnvGene Instance Repository contains non-relevant namespaces +- The Effective Set includes namespaces that are not required for the target deployment +- We cannot include/exclude namespaces specific to the cluster type (k8s or ocp) + +It prevents using a single unified Environment Template + +## Proposed Approach + +Treat the Template Descriptor (TD) as a Jinja template while keeping backward compatibility for non-Jinja TD. + +This enables filtering individual namespaces during Environment generation using Jinja `if` expressions. + +You can use different filtering approaches depending on your use case: + +- **Cluster-type filtering**: Use a variable to include/exclude namespaces based on cluster type (k8s/ocp) +- **Explicit namespace list**: Use shared template variables (e.g., `ns_list`) to control which namespaces are enabled +- **Solution-based filtering**: Use `current_env.solution_structure` for solution-descriptor–based scenarios + +### Example (cluster type filtering) + +```jinja +{% if current_env.additionalTemplateVariables.env_type == "ocp" %} + - template_path: "{{ templates_dir }}/env_templates/Namespaces/ingress-nginx.yml.j2" + name: ingress-nginx +{% endif %} +``` + +## File Resolution Priority + +If multiple Template Descriptor files exist, EnvGene selects them in descending priority order: + +1. `yml.j2` +2. `yaml.j2` +3. `yml` +4. `yaml` + +Jinja-based descriptors take precedence over static ones. + +## Behavior During Environment Generation + +### Scenario: Generating an Environment with namespace filtering + +1. EnvGene starts Environment Instance generation. +2. EnvGene reads the Template Descriptor (TD). +3. If the TD is a Jinja template (`*.yml.j2` / `*.yaml.j2`), EnvGene renders it first. +4. While rendering, EnvGene evaluates all Jinja `if` conditions for namespaces. +5. EnvGene keeps only the namespaces where the condition is `true`. +6. EnvGene generates the Environment Instance using the final (rendered) TD. + +### What happens when a namespace condition is `false` + +If a namespace is disabled by a condition: + +- EnvGene does **not** create the namespace folder in the Instance Repository +- EnvGene does **not** generate the namespace object +- The namespace does **not** appear in the Effective Set + +> [!NOTE] +> If you are using `NS_BUILD_FILTER`, keep in mind that this parameter only limits which namespaces are processed during a specific pipeline run — it does not add namespaces to the Environment structure. +> +> If a namespace is excluded during Template Descriptor rendering (Jinja condition = `false`), it will not be generated and will not appear in the Instance Repository or Effective Set. +> +> Therefore, such a namespace cannot be processed via `NS_BUILD_FILTER`, because it does not exist in the Environment model. +> For details about `NS_BUILD_FILTER` syntax and usage, see: [Namespace Render Filter](../features/namespace-render-filtering.md) + +## How-To + +For step-by-step instructions, see: +[How to filter namespaces in an Environment Template](/docs/how-to/filter-ns-in-template-descriptor.md) diff --git a/docs/glossary.md b/docs/glossary.md index 4ab78b3b7..ceaa08591 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -5,10 +5,11 @@ - [Deploy Postfix](#deploy-postfix) - [Environment](#environment) - [Environment Inventory](#environment-inventory) + - [Environment Template](#environment-template) - [Effective Set](#effective-set) - [Instance Repository](#instance-repository) - [Namespace](#namespace) - - [Template Artifact](#template-artifact) + - [Environment Template Artifact](#environment-template-artifact) This glossary provides definitions of key terms used in the EnvGene documentation. @@ -28,6 +29,12 @@ A logical grouping representing parameters for deployment target, defined by a u The configuration file describing a specific Environment, including template reference and parameters. See [env_definition.yml](/docs/envgene-configs.md#env_definitionyml). +## Environment Template + +A file structure within the Template Repository describing the structure and parameters of a solution type. Consists of a Template Descriptor and component templates (Tenant, Cloud, Namespaces). Template Descriptors can be Jinja templates (`.yml.j2`, `.yaml.j2`) or static YAML files (`.yml`, `.yaml`). + +A single Environment Template can be used for multiple projects or environment types (commonly referred to as a "unified Environment Template"), with namespace filtering used to include only relevant components for each specific deployment. See [Environment Template Objects](/docs/envgene-objects.md#environment-template-objects) and [Namespace Filtering in Template Descriptor](/docs/features/namespace-filtering-in-template-descriptor.md). + ## Effective Set The complete set of parameters generated for a specific Environment, used by consumers (e.g., ArgoCD). See [Effective Set Structure](/docs/features/calculator-cli.md#version-20-effective-set-structure). @@ -40,6 +47,6 @@ The Git repository containing Environment Inventories, generated Environment Ins An EnvGene object that groups parameters specific to applications within a single namespace in a cluster. Defined in the Environment Instance. See [Namespace](/docs/envgene-objects.md#namespace) -## Template Artifact +## Environment Template Artifact -A versioned template used to generate Environment Instances. Maven artifact. +A versioned Maven artifact containing one or more [Environment Templates](#environment-template) from the Template Repository. Published during Template Repository build process and referenced in the [Environment Inventory](/docs/envgene-configs.md#env_definitionyml) using `application:version` notation (e.g., `project-env-template:v1.2.3`). Used by EnvGene to generate Environment Instances. Also referred to as "Environment Template Artifact". See [Environment Template Objects](/docs/envgene-objects.md#environment-template-objects). diff --git a/docs/how-to/filter-ns-in-template-descriptor.md b/docs/how-to/filter-ns-in-template-descriptor.md new file mode 100644 index 000000000..d898262b0 --- /dev/null +++ b/docs/how-to/filter-ns-in-template-descriptor.md @@ -0,0 +1,160 @@ +# How to filter namespaces in an Environment Template + +- [How to filter namespaces in an Environment Template](#how-to-filter-namespaces-in-an-environment-template) + - [Overview](#overview) + - [Step 1. Convert Template Descriptor to a Jinja Template](#step-1-convert-template-descriptor-to-a-jinja-template) + - [Step 2. Wrap namespaces in Jinja conditions](#step-2-wrap-namespaces-in-jinja-conditions) + - [Generic pattern](#generic-pattern) + - [Example](#example) + - [Step 3. Create the namespace list file](#step-3-create-the-namespace-list-file) + - [Example ns-list.yaml](#example-ns-listyaml) + - [Step 4. Reference the file in env\_definition.yml](#step-4-reference-the-file-in-env_definitionyml) + - [Step 5. Run Environment Instance generation](#step-5-run-environment-instance-generation) + - [Step 6. Verify the result](#step-6-verify-the-result) + - [See also](#see-also) + +## Overview + +This guide explains how to generate an Environment that includes only a selected subset of namespaces from a unified Environment Template. It follows the feature described in [Namespace Filtering in Template Descriptor](/docs/features/namespace-filtering-in-template-descriptor.md). + +Use this approach if: + +- You use a unified Environment Template with multiple namespaces +- You need to include or exclude namespaces depending on specific environment need + +--- + +## Step 1. Convert Template Descriptor to a Jinja Template + +Find the Template Descriptor file: + +`/templates/env_templates/.yaml` + +Rename it to one of the following: + +- `.yaml.j2` +or +- `.yml.j2` + +After renaming, EnvGene will treat the Template Descriptor as a Jinja template and render it before Environment generation. + +--- + +## Step 2. Wrap namespaces in Jinja conditions + +Each namespace can be conditionally included using a Jinja `if` expression. + +### Generic pattern + +```jinja +namespaces: +{% if current_env.additionalTemplateVariables.ns_list.get('namespace-key', false) %} + - template_path: {{ templates_dir }}/env_templates/Namespaces/.yml.j2 + deploy_postfix: +{% endif %} +``` + +**Parameters**: + +- namespace-key - key in `ns_list` used to enable or disable the namespace +- template_path - path to the Namespace Template file +- deploy_postfix - folder name in Instance Repository (`/Namespaces//`) + +### Example + +```jinja +namespaces: +{% if current_env.additionalTemplateVariables.ns_list.get('postgresql', false) %} + - template_path: {{ templates_dir }}/env_templates/Namespaces/postgresql.yml.j2 + deploy_postfix: postgresql +{% endif %} + +{% if current_env.additionalTemplateVariables.ns_list.get('postgresql-dbaas', false) %} + - template_path: {{ templates_dir }}/env_templates/Namespaces/postgresql-dbaas.yml.j2 + deploy_postfix: postgresql-dbaas +{% endif %} + +{% if current_env.additionalTemplateVariables.ns_list.get('kafka', false) %} + - template_path: {{ templates_dir }}/env_templates/Namespaces/kafka.yml.j2 + deploy_postfix: kafka +{% endif %} + +{% if current_env.additionalTemplateVariables.ns_list.get('platform-monitoring', false) %} + - template_path: {{ templates_dir }}/env_templates/Namespaces/platform-monitoring.yml.j2 + deploy_postfix: platform-monitoring +{% endif %} +``` + +If the key is missing or set to false, the namespace will not be generated. + +--- + +## Step 3. Create the namespace list file + +Create a YAML file that defines which namespaces are enabled. The file must be in a location where EnvGene resolves [shared template variable files](/docs/envgene-configs.md#env_definitionyml) (for example, under `/environments/` or per-cluster/per-environment, depending on your Instance Repository layout). The filename (without extension) is what you will reference in `env_definition.yml` in Step 4. + +Example: if the file is named `ns-list.yaml`, use the name `ns-list` in `sharedTemplateVariables`. + +```yaml +# Example path: /environments/shared-template-variables/ns-list.yaml +# The top-level key (ns_list) becomes available as current_env.additionalTemplateVariables.ns_list + +ns_list: + : + : +``` + +### Example ns-list.yaml + +```yaml +# /environments/shared-template-variables/ns-list.yaml + +ns_list: + platform-monitoring: true + postgresql: true + postgresql-dbaas: true + kafka: false +``` + +Set `true` for namespaces that must be generated. + +## Step 4. Reference the file in env_definition.yml + +In the [Environment Inventory](/docs/envgene-configs.md#env_definitionyml) for the environment, add the shared template variable by **filename without extension** (e.g. `ns-list` for `ns-list.yaml`): + +```yaml +envTemplate: + sharedTemplateVariables: + - ns-list +``` + +The content of the file is merged into `additionalTemplateVariables`, so `ns_list` from the YAML is available during Template Descriptor rendering as `current_env.additionalTemplateVariables.ns_list`. + +## Step 5. Run Environment Instance generation + +Run the Environment Instance generation process using the standard EnvGene pipeline. + +During generation: + +- The Template Descriptor is rendered +- Jinja conditions are evaluated +- Only enabled namespaces are included + +If a condition evaluates to `false`: + +- The namespace folder is not created +- The namespace object is not generated + +## Step 6. Verify the result + +After generation, verify the directory: `/environments///Namespaces/` + +Only namespaces that passed the Jinja conditions should be present. + +--- + +## See also + +- [Namespace Filtering in Template Descriptor](/docs/features/namespace-filtering-in-template-descriptor.md) - Feature overview, file resolution priority, and behavior when a condition is `false` +- [env_definition.yml](/docs/envgene-configs.md#env_definitionyml) - Environment Inventory and `sharedTemplateVariables` +- [Template Descriptor](/docs/envgene-objects.md#template-descriptor) - Location and Jinja extensions From 09883a5357d3508894fdb4e73fc61abf90d7f3c8 Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:07:38 +0300 Subject: [PATCH 075/161] fix: Small update to GSF related to pipeline_vars after review. --- .../scripts/init.py | 32 ++++++++++++------- .../scripts/init.py | 32 ++++++++++++------- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/scripts/init.py b/gsf_packages/envgene_discovery_project/git-system-follower-package/scripts/init.py index ba68c8be3..d014d73f8 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/scripts/init.py +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/scripts/init.py @@ -138,18 +138,26 @@ def main(parameters: Parameters): create_template(parameters, template, variables) - # Restore pipeline_vars if it existed - never overwrite user's file - for f in pipeline_vars_paths: - if f in pipeline_vars_backup: - path = repo_root / f - content = pipeline_vars_backup[f] - migrated = _migrate_pipeline_vars_format(content) - if migrated != content: - print(f'\t\tFile {f} migrated to new format. Preserved') - else: - print(f'\t\tFile {f} preserved. Skip overwrite') - path.parent.mkdir(parents=True, exist_ok=True) - path.write_bytes(migrated) + # Restore pipeline_vars if it existed - never overwrite, keep only user's format + if pipeline_vars_backup: + for f in pipeline_vars_paths: + if f in pipeline_vars_backup: + path = repo_root / f + content = pipeline_vars_backup[f] + migrated = _migrate_pipeline_vars_format(content) + if migrated != content: + print(f'\t\tFile {f} migrated to new format. Preserved') + else: + print(f'\t\tFile {f} preserved. Skip overwrite') + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(migrated) + # Remove the other format - template creates yaml, user may have yml + for f in pipeline_vars_paths: + if f not in pipeline_vars_backup: + path = repo_root / f + if path.exists(): + path.unlink() + print(f'\t\tRemoved {f} (user uses different format)') internal_files_to_remove = ['history.log'] for file_name in internal_files_to_remove: diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/init.py b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/init.py index 24426bf3a..bf282dd75 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/init.py +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/init.py @@ -138,18 +138,26 @@ def main(parameters: Parameters): create_template(parameters, template, variables) - # Restore pipeline_vars if it existed - never overwrite user's file - for f in pipeline_vars_paths: - if f in pipeline_vars_backup: - path = repo_root / f - content = pipeline_vars_backup[f] - migrated = _migrate_pipeline_vars_format(content) - if migrated != content: - print(f'\t\tFile {f} migrated to new format. Preserved') - else: - print(f'\t\tFile {f} preserved. Skip overwrite') - path.parent.mkdir(parents=True, exist_ok=True) - path.write_bytes(migrated) + # Restore pipeline_vars if it existed - never overwrite, keep only user's format + if pipeline_vars_backup: + for f in pipeline_vars_paths: + if f in pipeline_vars_backup: + path = repo_root / f + content = pipeline_vars_backup[f] + migrated = _migrate_pipeline_vars_format(content) + if migrated != content: + print(f'\t\tFile {f} migrated to new format. Preserved') + else: + print(f'\t\tFile {f} preserved. Skip overwrite') + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(migrated) + # Remove the other format - template creates yaml, user may have yml + for f in pipeline_vars_paths: + if f not in pipeline_vars_backup: + path = repo_root / f + if path.exists(): + path.unlink() + print(f'\t\tRemoved {f} (user uses different format)') internal_files_to_remove = ['history.log'] for file_name in internal_files_to_remove: From 8f92ef03d980f6323231c62b86f8ea0cff89ca4b Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Wed, 11 Mar 2026 07:16:06 +0000 Subject: [PATCH 076/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index f6c94fa8b..93d60df0f 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.1" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.1" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.1" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.2" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.2" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.2" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 95356c965..0ac86ac59 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.1 +version: 1.31.2 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 702609d95..500198821 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.1 +version: 1.31.2 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 3b8689a2e..164d8efcd 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.1", + "envgene_version": "1.31.2", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 068ed7d0c141a7419d270eed754ee80248480ef3 Mon Sep 17 00:00:00 2001 From: chethana-shastry-p Date: Wed, 11 Mar 2026 13:32:15 +0530 Subject: [PATCH 077/161] fix: cmdb.creds macro support in cli (#1093) --- .../pl-01/Namespaces/monitoring-origin/namespace.yml | 1 + .../effective-set/cleanup/monitoring-origin/credentials.yaml | 1 + .../monitoring-origin/MONITORING/values/credentials.yaml | 2 ++ .../parameters/processor/expression/ExpressionLanguage.java | 3 --- .../cloud/parameters/processor/expression/binding/Binding.java | 3 +++ 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/Namespaces/monitoring-origin/namespace.yml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/Namespaces/monitoring-origin/namespace.yml index d794157dc..660474d64 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/Namespaces/monitoring-origin/namespace.yml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/Namespaces/monitoring-origin/namespace.yml @@ -36,6 +36,7 @@ deployParameters: TEST_ENVGENE_CREDS_GET_VAULT_SECRET_ID: "${creds.get( \"envgen-creds-get-vault-cred-secret-id\").secretId}" # paramset: test-deploy-creds version: 1 source: template TEST_SHARED_CREDS: "${creds.get('integration-cred').username}" # paramset: test-deploy-creds version: 1 source: template TEST_SHARED_CREDS_ACTIVATOR: "${creds.get('service-integration-cred').password}" # paramset: test-deploy-creds version: 1 source: template + TEST_CMDB_CREDS_USERNAME: "${cmdb.creds.get('creds-get-username-cred').username}" #added manually for cmdb.creds macro validation bss-app-exist: false core: # paramset: paramset-B version: 23.4 source: instance apps: diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/monitoring-origin/credentials.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/monitoring-origin/credentials.yaml index f46d1cf1f..0dfe41d91 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/monitoring-origin/credentials.yaml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/monitoring-origin/credentials.yaml @@ -21,6 +21,7 @@ TEST_ENVGENE_CREDS_GET_VAULT_ROLE: envgeneNullValue TEST_ENVGENE_CREDS_GET_VAULT_SECRET_ID: envgeneNullValue TEST_SHARED_CREDS: user-placeholder-123 TEST_SHARED_CREDS_ACTIVATOR: pass-placeholder-123 +TEST_CMDB_CREDS_USERNAME: user-placeholder-123 graphite-remote-adapter: user-placeholder-123 kafka: password: pass-placeholder-123 diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/monitoring-origin/MONITORING/values/credentials.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/monitoring-origin/MONITORING/values/credentials.yaml index c45480d21..2f494be22 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/monitoring-origin/MONITORING/values/credentials.yaml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/monitoring-origin/MONITORING/values/credentials.yaml @@ -28,6 +28,7 @@ TEST_ENVGENE_CREDS_GET_VAULT_ROLE: envgeneNullValue TEST_ENVGENE_CREDS_GET_VAULT_SECRET_ID: envgeneNullValue TEST_SHARED_CREDS: user-placeholder-123 TEST_SHARED_CREDS_ACTIVATOR: pass-placeholder-123 +TEST_CMDB_CREDS_USERNAME: user-placeholder-123 kafka: &id001 password: pass-placeholder-123 username: user-placeholder-123 @@ -62,6 +63,7 @@ global: &id002 TEST_ENVGENE_CREDS_GET_VAULT_SECRET_ID: envgeneNullValue TEST_SHARED_CREDS: user-placeholder-123 TEST_SHARED_CREDS_ACTIVATOR: pass-placeholder-123 + TEST_CMDB_CREDS_USERNAME: user-placeholder-123 graphite-remote-adapter: user-placeholder-123 kafka: *id001 alertmanager: *id002 diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/ExpressionLanguage.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/ExpressionLanguage.java index cb7a571c0..0175d6fc1 100644 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/ExpressionLanguage.java +++ b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/ExpressionLanguage.java @@ -344,9 +344,6 @@ private Map processMap(Map map, Map processed = calculateCredentialsAndPrepareStructuredParams(this); From be34c17c25e8e36ab1ed7f9833b38a06ee7e851a Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Wed, 11 Mar 2026 08:09:05 +0000 Subject: [PATCH 078/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 93d60df0f..1245845dd 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.2" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.2" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.2" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.3" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.3" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.3" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 0ac86ac59..e311188a1 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.2 +version: 1.31.3 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 500198821..e7db1f74e 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.2 +version: 1.31.3 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 164d8efcd..5cf6c26a6 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.2", + "envgene_version": "1.31.3", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 3e3bef8fa2bb0d00503584302ea8adb1c2c958c4 Mon Sep 17 00:00:00 2001 From: Geetha Gadde Date: Wed, 11 Mar 2026 17:44:47 +0530 Subject: [PATCH 079/161] feat: Hide security parameters from job logs (#1101) --- scripts/utils/pipeline_parameters.py | 33 +++++++++++++++++++++------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/scripts/utils/pipeline_parameters.py b/scripts/utils/pipeline_parameters.py index 8a125ffc2..9b90c006d 100644 --- a/scripts/utils/pipeline_parameters.py +++ b/scripts/utils/pipeline_parameters.py @@ -49,11 +49,23 @@ def get_pipeline_parameters() -> dict: class PipelineParametersHandler: def __init__(self, **kwargs): - plugins_dir='/module/scripts/pipegene_plugins/pipe_parameters' + plugins_dir = '/module/scripts/pipegene_plugins/pipe_parameters' self.params = get_pipeline_parameters() pipe_param_plugin = PluginEngine(plugins_dir=plugins_dir) + if pipe_param_plugin.modules: - pipe_param_plugin.run(pipeline_params=self.params) + pipe_param_plugin.run(pipeline_params=self.params) + + def hide_secrets(self, data): + if isinstance(data, dict): + for k, v in data.items(): + if k.lower() in {"username", "password", "secret"}: + data[k] = "***" + else: + self.hide_secrets(v) + elif isinstance(data, list): + for item in data: + self.hide_secrets(item) def log_pipeline_params(self): params_str = "Input parameters are: " @@ -63,12 +75,17 @@ def log_pipeline_params(self): params["CRED_ROTATION_PAYLOAD"] = "***" for k, v in params.items(): - try: - parsed = json.loads(v) - params[k] = json.dumps(parsed, separators=(",", ":")) - except (TypeError, ValueError): - pass + try: + parsed = json.loads(v) + + if k == "ENV_INVENTORY_CONTENT": + self.hide_secrets(parsed) + + params[k] = json.dumps(parsed, separators=(",", ":")) + + except (TypeError, ValueError): + pass - params_str += f"\n{k.upper()}: {params[k]}" + params_str += f"\n{k.upper()}: {params[k]}" logger.info(params_str) From 527888ba54f1f869660dd18e30998618c6f9a51e Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Wed, 11 Mar 2026 12:17:13 +0000 Subject: [PATCH 080/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 1245845dd..e5c6f6d20 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.3" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.3" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.3" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.4" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.4" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.4" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index e311188a1..80fa51662 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.3 +version: 1.31.4 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index e7db1f74e..fccce858a 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.3 +version: 1.31.4 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 5cf6c26a6..315ad9730 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.3", + "envgene_version": "1.31.4", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 3bbb3c85db6164b8ec90951016281b79adb6455d Mon Sep 17 00:00:00 2001 From: Siva Reddy Kunduru <35566000+sivareddyit@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:09:04 +0530 Subject: [PATCH 081/161] fix: fixed issue while building the repo url if the maven repository is missing in the dd (#1095) --- .../env_template/process_env_template.py | 10 ++++-- .../tests/env-template/test_env_template.py | 32 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/scripts/build_env/env_template/process_env_template.py b/scripts/build_env/env_template/process_env_template.py index efe3b9a94..d0d9f7da9 100644 --- a/scripts/build_env/env_template/process_env_template.py +++ b/scripts/build_env/env_template/process_env_template.py @@ -2,6 +2,7 @@ import os import tempfile from pathlib import Path +from urllib.parse import urlparse from artifact_searcher import artifact from artifact_searcher.utils.models import FileExtension, Credentials, Registry, Application @@ -70,7 +71,7 @@ async def resolve_artifact_new_logic(app_def: Application, app_version: str, tem dd_artifact_info = await artifact.check_artifact_async(app_def, FileExtension.JSON, app_version, cred) if dd_artifact_info: logger.info("Loading environment template artifact info from deployment descriptor...") - dd_url, dd_repo = dd_artifact_info + dd_url, (repo_name, _) = dd_artifact_info logger.info(f"Resolved deployment descriptor URL: {dd_url}") if "-SNAPSHOT" in app_version: resolved_version = extract_snapshot_version(dd_url, app_version) @@ -82,7 +83,12 @@ async def resolve_artifact_new_logic(app_def: Application, app_version: str, tem if not all([group_id, artifact_id, version]): raise ValueError(f"Invalid maven coordinates from deployment descriptor {dd_url}") - repo_url = dd_config.get("configurations", [{}])[0].get("maven_repository") or dd_repo + repo_url = dd_config.get("configurations", [{}])[0].get("maven_repository") + + if not repo_url: + parsed = urlparse(dd_url) + repo_url = f"{parsed.scheme}://{parsed.netloc}/{repo_name}" + logger.info(f"building repo url from the repo name : {repo_url}") template_url = artifact.check_artifact(repo_url, group_id, artifact_id, version, FileExtension.ZIP, cred) validate_url(template_url, group_id, artifact_id, version) else: diff --git a/scripts/build_env/tests/env-template/test_env_template.py b/scripts/build_env/tests/env-template/test_env_template.py index c6425aec4..14748dd67 100644 --- a/scripts/build_env/tests/env-template/test_env_template.py +++ b/scripts/build_env/tests/env-template/test_env_template.py @@ -43,6 +43,12 @@ f"{ARTIFACT_ZIP_ID}-{ZIP_VERSION}.zip" ) +SNAPSHOT_ZIP_URL = ( + f"{SNAPSHOT_BASE}/{PROJECT_GROUP_PATH}/" + f"{ARTIFACT_ZIP_ID}/{ZIP_VERSION}/" + f"{ARTIFACT_ZIP_ID}-{ZIP_VERSION}.zip" +) + TMPL_ZIP_URL = ( f"{TMPL_SNAPSHOT_BASE}/{PROJECT_GROUP_PATH}/" f"{ARTIFACT_ZIP_ID}/{ZIP_VERSION}/" @@ -94,6 +100,15 @@ def mock_aio_response(): }] } +dd_json_without_mvn_repo = { + "configurations": [{ + "artifacts": [{ + "id": f"{PROJECT_GROUP_ID}:{ARTIFACT_ZIP_ID}:{ZIP_VERSION}", + "type": "zip", + "classifier": "" + }] + }] +} def set_env(name: str): environ["ENVIRONMENT_NAME"] = name @@ -116,6 +131,8 @@ def mock_dd_exists(aio_mock=None, exists=True): def mock_dd_response(): responses.add(responses.GET, DD_URL, json=dd_json, status=200) +def mock_dd_response_without_mvn_repo(): + responses.add(responses.GET, DD_URL, json=dd_json_without_mvn_repo, status=200) def mock_zip(url): responses.add( @@ -165,6 +182,21 @@ def test_new_logic_with_dd(self, mock_aio_response): assert responses.calls[0].request.url == DD_URL assert responses.calls[1].request.url == STAGING_ZIP_URL + @responses.activate + def test_new_logic_with_dd_without_mvn_repo(self, mock_aio_response): + set_env("env-01") + + mock_metadata(mock_aio_response) + mock_dd_exists(mock_aio_response, exists=True) + mock_dd_response_without_mvn_repo() + mock_zip(SNAPSHOT_ZIP_URL) + + process_env_template() + + assert len(responses.calls) == 3 + assert responses.calls[0].request.url == DD_URL + assert responses.calls[1].request.url == SNAPSHOT_ZIP_URL + @responses.activate def test_new_logic_with_zip(self, mock_aio_response): set_env("env-01") From a08d825f9b9278092ba8fdb9e82303eadd6c73f4 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Thu, 12 Mar 2026 08:44:55 +0000 Subject: [PATCH 082/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index e5c6f6d20..62d6824a2 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.4" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.4" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.4" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.5" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.5" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.5" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 80fa51662..db94015ab 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.4 +version: 1.31.5 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index fccce858a..c41dcf4f9 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.4 +version: 1.31.5 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 315ad9730..26e7e9b6c 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.4", + "envgene_version": "1.31.5", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 70c68b404b1dc38ce1d23e962e1554cac4e0efae Mon Sep 17 00:00:00 2001 From: chethana-shastry-p Date: Thu, 12 Mar 2026 20:51:22 +0530 Subject: [PATCH 083/161] fix: temporary fix for tmp folder overwrite (#1111) --- build_pipegene/scripts/appregdef_render_job.py | 13 +++++++++++-- build_pipegene/scripts/env_build_jobs.py | 10 ++++++++-- build_pipegene/scripts/gitlab_ci.py | 4 +++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/build_pipegene/scripts/appregdef_render_job.py b/build_pipegene/scripts/appregdef_render_job.py index 763a57051..41cbc1832 100644 --- a/build_pipegene/scripts/appregdef_render_job.py +++ b/build_pipegene/scripts/appregdef_render_job.py @@ -17,6 +17,16 @@ def prepare_appregdef_render_job(pipeline, params, full_env, environment_name, c script.append('python3 /build_env/scripts/build_env/appregdef_render.py') + script.append( + 'if [ -d "$CI_PROJECT_DIR/tmp" ]; then ' + 'DEST="$CI_PROJECT_DIR/$CI_PIPELINE_ID/tmp"; ' + 'echo "Copying tmp in $CI_PROJECT_DIR to $DEST"; ' + 'mkdir -p "$DEST"; ' + 'cp -r "$CI_PROJECT_DIR/tmp/." "$DEST/"; ' + 'else echo "tmp directory does not exist in $CI_PROJECT_DIR, skipping copy"; ' + 'fi' + ) + appregdef_render_params = { "name": f'app_reg_def_render.{full_env}', "image": '${envgen_image}', @@ -42,8 +52,7 @@ def prepare_appregdef_render_job(pipeline, params, full_env, environment_name, c appregdef_render_job = job_instance(params=appregdef_render_params, vars=appregdef_render_vars) appregdef_render_job.artifacts.add_paths("${CI_PROJECT_DIR}/environments/" + full_env) - appregdef_render_job.artifacts.add_paths("${CI_PROJECT_DIR}/configuration") - appregdef_render_job.artifacts.add_paths("${CI_PROJECT_DIR}/tmp") + appregdef_render_job.artifacts.add_paths("${CI_PROJECT_DIR}/configuration") appregdef_render_job.artifacts.when = WhenStatement.ALWAYS pipeline.add_children(appregdef_render_job) diff --git a/build_pipegene/scripts/env_build_jobs.py b/build_pipegene/scripts/env_build_jobs.py index 48dbf7f01..79f73b277 100644 --- a/build_pipegene/scripts/env_build_jobs.py +++ b/build_pipegene/scripts/env_build_jobs.py @@ -8,6 +8,13 @@ def prepare_env_build_job(pipeline, is_template_test, full_env, enviroment_name, logger.info(f'prepare env_build job for {full_env}') script = [ + 'echo "PIPELINE=$CI_PIPELINE_ID JOB=$CI_JOB_NAME"', + 'if [ -d "$CI_PROJECT_DIR/$CI_PIPELINE_ID/tmp" ] && [ -d "$CI_PROJECT_DIR/tmp" ]; then', + 'echo "Copying $CI_PROJECT_DIR/$CI_PIPELINE_ID/tmp -> $CI_PROJECT_DIR/tmp";', + 'rm -rf "$CI_PROJECT_DIR/tmp/"* 2>/dev/null || echo "Warning: Failed to remove some files in tmp"', + 'cp -r "$CI_PROJECT_DIR/$CI_PIPELINE_ID/tmp/." "$CI_PROJECT_DIR/tmp/" || echo "Warning: Failed to copy tmp contents"', + 'rm -rf "$CI_PROJECT_DIR/$CI_PIPELINE_ID" || echo "Warning: Failed to delete pipeline directory"', + 'fi', 'cd /build_env; python3 /build_env/scripts/build_env/main.py' ] @@ -45,8 +52,7 @@ def prepare_env_build_job(pipeline, is_template_test, full_env, enviroment_name, env_build_job.artifacts.add_paths("${CI_PROJECT_DIR}/set_variable.txt") else: env_build_job.artifacts.add_paths("${CI_PROJECT_DIR}/environments/" + f"{full_env}") - env_build_job.artifacts.add_paths("${CI_PROJECT_DIR}/configuration") - env_build_job.artifacts.add_paths("${CI_PROJECT_DIR}/tmp") + env_build_job.artifacts.add_paths("${CI_PROJECT_DIR}/configuration") env_build_job.artifacts.when = WhenStatement.ALWAYS pipeline.add_children(env_build_job) return env_build_job diff --git a/build_pipegene/scripts/gitlab_ci.py b/build_pipegene/scripts/gitlab_ci.py index 2376cf58c..2f70ebd4b 100644 --- a/build_pipegene/scripts/gitlab_ci.py +++ b/build_pipegene/scripts/gitlab_ci.py @@ -201,7 +201,9 @@ def build_pipeline(params: dict) -> None: 'environments/', 'configuration/', 'sboms/', - 'templates/' + 'templates/', + 'tmp/', + '$CI_PIPELINE_ID/tmp' ) is_first_job = job.needs is None or len(job.needs) == 0 From c94c4e141a917bea2a896a6ce3831efdbe988e40 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Thu, 12 Mar 2026 15:28:39 +0000 Subject: [PATCH 084/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 62d6824a2..01d5bd1bd 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.5" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.5" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.5" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.6" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.6" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.6" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index db94015ab..a77d70810 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.5 +version: 1.31.6 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index c41dcf4f9..f424ca457 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.5 +version: 1.31.6 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 26e7e9b6c..ba9ca70db 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.5", + "envgene_version": "1.31.6", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From ae3037e66690c857cc30d12fb350ce6574b95822 Mon Sep 17 00:00:00 2001 From: basudev91 Date: Fri, 13 Mar 2026 01:37:29 +0530 Subject: [PATCH 085/161] docs: added new doc for how to define complex parameters (#1065) * docs: added new doc for defining complex params in yml * docs: fix linter issue * docs: fix linter issue * docs: wip --------- Co-authored-by: popoveugene --- ...g-complex-parameters-using-yaml-objects.md | 370 ++++++++++++++++++ 1 file changed, 370 insertions(+) create mode 100644 docs/how-to/defining-complex-parameters-using-yaml-objects.md diff --git a/docs/how-to/defining-complex-parameters-using-yaml-objects.md b/docs/how-to/defining-complex-parameters-using-yaml-objects.md new file mode 100644 index 000000000..2c9890ae9 --- /dev/null +++ b/docs/how-to/defining-complex-parameters-using-yaml-objects.md @@ -0,0 +1,370 @@ +# Defining and Managing Complex Parameters in EnvGene Using YAML Objects + +- [Defining and Managing Complex Parameters in EnvGene Using YAML Objects](#defining-and-managing-complex-parameters-in-envgene-using-yaml-objects) + - [Overview](#overview) + - [When to Use This Guide](#when-to-use-this-guide) + - [Core Rule](#core-rule) + - [Why YAML Objects Are Required](#why-yaml-objects-are-required) + - [Structural Integrity](#structural-integrity) + - [Clean and Meaningful Git Diffs](#clean-and-meaningful-git-diffs) + - [No Manual Escaping](#no-manual-escaping) + - [How to Define Complex Parameters Correctly](#how-to-define-complex-parameters-correctly) + - [Defining a Map (Object)](#defining-a-map-object) + - [Defining a List](#defining-a-list) + - [How EnvGene Processes Complex Parameters](#how-envgene-processes-complex-parameters) + - [During Environment Instance Generation](#during-environment-instance-generation) + - [During CMDB Import](#during-cmdb-import) + - [End-to-End Example](#end-to-end-example) + - [YAML Definition in Template or Environment-specific parameters](#yaml-definition-in-template-or-environment-specific-parameters) + - [Effective Set Output](#effective-set-output) + - [CMDB Imported Representation](#cmdb-imported-representation) + - [Design Principles for YAML](#design-principles-for-yaml) + - [Treat YAML as Structured Data](#treat-yaml-as-structured-data) + - [Preserve Type Integrity](#preserve-type-integrity) + - [Avoid Embedded JSON Inside YAML](#avoid-embedded-json-inside-yaml) + - [Migration Strategy for Existing Multiline or JSON Strings](#migration-strategy-for-existing-multiline-or-json-strings) + - [Operational Impact](#operational-impact) + - [Final Recommendation](#final-recommendation) + +## Overview + +This guide explains how to define complex parameters (maps and lists) in EnvGene using native YAML objects instead of multiline or JSON strings. It is intended for engineers working with Git-managed EnvGene instance or template repositories. + +### When to Use This Guide + +Use this guide when: + +- Defining new parameters in EnvGene +- Refactoring existing multiline string parameters +- Debugging CMDB import formatting issues +- Reviewing pull requests involving complex configuration +- Establishing configuration standards across teams + +### Core Rule + +> Always define complex parameters as structured YAML objects. Never use multiline strings or JSON strings for structured configuration. + +Complex parameters are maps (objects) and lists (arrays). Maps can contain other maps or lists as values. + +--- + +## Why YAML Objects Are Required + +### Structural Integrity + +When defined as YAML objects: + +- Types are preserved (map, list, boolean, number, string) +- Nested attributes retain structure +- YAML schema validation works +- Linters and IDE tooling function correctly + +When defined as multiline strings: + +- Everything becomes a string +- Structure is lost +- No validation is applied +- Type safety is broken + +--- + +### Clean and Meaningful Git Diffs + +Structured YAML enables semantic diffs; only the changed keys appear in the diff: + +```diff + deploymentConfig: + replicas: 2 + strategy: + type: RollingUpdate + maxUnavailable: 0 + resources: + limits: +- memory: 512Mi ++ memory: 1Gi + cpu: 500m + requests: + memory: 256Mi + cpu: 250m +``` + +Escaped JSON in a string shows the entire value as one long line; a single field change (e.g. `memory`) is buried in escaped quotes and hard to review: + +```diff +- deploymentConfig: "{ \"replicas\":2,\"strategy\":{\"type\":\"RollingUpdate\",\"maxUnavailable\":0},\"resources\":{\"limits\":{\"memory\":\"512Mi\",\"cpu\":\"500m\"},\"requests\":{\"memory\":\"256Mi\",\"cpu\":\"250m\"}} }" ++ deploymentConfig: "{ \"replicas\":2,\"strategy\":{\"type\":\"RollingUpdate\",\"maxUnavailable\":0},\"resources\":{\"limits\":{\"memory\":\"1Gi\",\"cpu\":\"500m\"},\"requests\":{\"memory\":\"256Mi\",\"cpu\":\"250m\"}} }" +``` + +Structured YAML improves: + +- Code reviews +- Drift detection +- Merge conflict resolution + +--- + +### No Manual Escaping + +Multiline or escaped JSON string (e.g. a small map): + +```yaml +config: "{ \"limits\": { \"cpu\": \"500m\" } }" +``` + +Native YAML: + +```yaml +config: + limits: + cpu: 500m +``` + +Manual escaping: + +- Is error-prone +- Breaks readability +- Causes CMDB import issues + +--- + +## How to Define Complex Parameters Correctly + +### Defining a Map (Object) + +Correct: + +```yaml +parameters: + resourceLimits: + cpu: 500m + memory: 1Gi +``` + +Incorrect (multiline string - map is stored as text): + +```yaml +parameters: + resourceLimits: | + cpu: 500m + memory: 1Gi +``` + +Incorrect (JSON string): + +```yaml +parameters: + resourceLimits: '{"cpu":"500m","memory":"1Gi"}' +``` + +--- + +### Defining a List + +Correct: + +```yaml +parameters: + allowedIPs: + - 10.10.0.1 + - 10.10.0.2 + - 10.10.0.3 +``` + +Incorrect (multiline string - list is stored as text): + +```yaml +parameters: + allowedIPs: | + - 10.10.0.1 + - 10.10.0.2 +``` + +Incorrect (JSON string): + +```yaml +parameters: + allowedIPs: '["10.10.0.1","10.10.0.2","10.10.0.3"]' +``` + +--- + +## How EnvGene Processes Complex Parameters + +### During Environment Instance Generation + +EnvGene preserves structure and types (map, list, boolean, number, string) and produces a structured effective set. No flattening or string conversion occurs at this stage. + +Example effective set output: + +```yaml +deploymentConfig: + replicas: 3 + strategy: + type: RollingUpdate + maxUnavailable: 1 + resources: + limits: + cpu: 1 + memory: 2Gi +``` + +--- + +### During CMDB Import + +CMDB requires complex parameters to be stored as escaped string representations. + +EnvGene automatically transforms the YAML object. + +Source YAML object: + +```yaml +deploymentConfig: + replicas: 3 + strategy: + type: RollingUpdate +``` + +CMDB representation: + +```json +deploymentConfig: "{\"replicas\":3,\"strategy\":{\"type\":\"RollingUpdate\"}}" +``` + +Key points: + +- Conversion is automatic +- No manual escaping required +- Original structure drives transformation +- Type fidelity is preserved before serialization + +--- + +## End-to-End Example + +### YAML Definition in Template or Environment-specific parameters + +```yaml +parameters: + serviceConfig: + service: + name: payment-api + port: 8080 + monitoring: + enabled: true + endpoints: + - /health + - /metrics +``` + +--- + +### Effective Set Output + +```yaml +serviceConfig: + service: + name: payment-api + port: 8080 + monitoring: + enabled: true + endpoints: + - /health + - /metrics +``` + +--- + +### CMDB Imported Representation + +```json +"{\"service\":{\"name\":\"payment-api\",\"port\":8080},\"monitoring\":{\"enabled\":true,\"endpoints\":[\"/health\",\"/metrics\"]}}" +``` + +Result: + +- Structure preserved +- Types preserved +- Converted into CMDB-compatible format automatically + +--- + +## Design Principles for YAML + +### Treat YAML as Structured Data + +If the parameter represents structured configuration, it must be a YAML object, not multiline string, not JSON string + +--- + +### Preserve Type Integrity + +Avoid: + +```yaml +replicas: "3" +enabled: "true" +``` + +Prefer: + +```yaml +replicas: 3 +enabled: true +``` + +--- + +### Avoid Embedded JSON Inside YAML + +Do not use: + +```yaml +config: '{"limits":{"cpu":"1"}}' +``` + +If you see JSON inside YAML, it is almost always incorrect. + +--- + +## Migration Strategy for Existing Multiline or JSON Strings + +If complex parameters already exist as multiline strings or JSON in a string: + +1. Identify parameters to migrate: + - **Multiline:** look for keys whose value uses `|` (or `>-` / `|`) with YAML-like or list-like content underneath. + - **JSON in string:** look for values that are single-quoted or double-quoted JSON (e.g. `'{"key":...}'` or `"{ \"key\": ... }"`). +2. Convert content into structured YAML (native map or list). +3. Validate: + - Instance generation + - Effective set output + - CMDB import result +4. Remove manual escaping where it was used for JSON. +5. Add YAML linting to CI pipelines. + +--- + +## Operational Impact + +Adopting YAML objects improves: + +- Maintainability +- Git readability +- Configuration correctness +- CI/CD validation capability +- Reduction of CMDB import failures +- Long-term configuration scalability + +--- + +## Final Recommendation + +For all complex parameters: + +- Define maps and lists as native YAML objects. +- Allow automatic transformation during CMDB import. +- Preserve structural and type integrity. +- Enforce YAML linting in CI/CD. +- Never use multiline strings for structured configuration. + +Configuration should be declarative, structured, and type-safe. YAML objects enable that. Multiline and JSON strings undermine it. From 9c7a74b7ef5408822da10cadfcddb570050b51b2 Mon Sep 17 00:00:00 2001 From: popoveugene <42543333+popoveugene@users.noreply.github.com> Date: Thu, 12 Mar 2026 23:13:04 +0300 Subject: [PATCH 086/161] docs: updated doc for how to define complex parameters (#1115) --- ...g-complex-parameters-using-yaml-objects.md | 64 +++---------------- 1 file changed, 9 insertions(+), 55 deletions(-) diff --git a/docs/how-to/defining-complex-parameters-using-yaml-objects.md b/docs/how-to/defining-complex-parameters-using-yaml-objects.md index 2c9890ae9..dbdc8fed3 100644 --- a/docs/how-to/defining-complex-parameters-using-yaml-objects.md +++ b/docs/how-to/defining-complex-parameters-using-yaml-objects.md @@ -46,28 +46,20 @@ Use this guide when: Complex parameters are maps (objects) and lists (arrays). Maps can contain other maps or lists as values. ---- - ## Why YAML Objects Are Required ### Structural Integrity When defined as YAML objects: -- Types are preserved (map, list, boolean, number, string) -- Nested attributes retain structure - YAML schema validation works - Linters and IDE tooling function correctly When defined as multiline strings: -- Everything becomes a string -- Structure is lost - No validation is applied - Type safety is broken ---- - ### Clean and Meaningful Git Diffs Structured YAML enables semantic diffs; only the changed keys appear in the diff: @@ -101,8 +93,6 @@ Structured YAML improves: - Drift detection - Merge conflict resolution ---- - ### No Manual Escaping Multiline or escaped JSON string (e.g. a small map): @@ -125,8 +115,6 @@ Manual escaping: - Breaks readability - Causes CMDB import issues ---- - ## How to Define Complex Parameters Correctly ### Defining a Map (Object) @@ -156,8 +144,6 @@ parameters: resourceLimits: '{"cpu":"500m","memory":"1Gi"}' ``` ---- - ### Defining a List Correct: @@ -186,8 +172,6 @@ parameters: allowedIPs: '["10.10.0.1","10.10.0.2","10.10.0.3"]' ``` ---- - ## How EnvGene Processes Complex Parameters ### During Environment Instance Generation @@ -208,8 +192,6 @@ deploymentConfig: memory: 2Gi ``` ---- - ### During CMDB Import CMDB requires complex parameters to be stored as escaped string representations. @@ -225,10 +207,10 @@ deploymentConfig: type: RollingUpdate ``` -CMDB representation: +CMDB stores the parameter key separately; the value is the escaped JSON string. Example value: ```json -deploymentConfig: "{\"replicas\":3,\"strategy\":{\"type\":\"RollingUpdate\"}}" +"{\"replicas\":3,\"strategy\":{\"type\":\"RollingUpdate\"}}" ``` Key points: @@ -238,8 +220,6 @@ Key points: - Original structure drives transformation - Type fidelity is preserved before serialization ---- - ## End-to-End Example ### YAML Definition in Template or Environment-specific parameters @@ -257,8 +237,6 @@ parameters: - /metrics ``` ---- - ### Effective Set Output ```yaml @@ -273,8 +251,6 @@ serviceConfig: - /metrics ``` ---- - ### CMDB Imported Representation ```json @@ -287,15 +263,11 @@ Result: - Types preserved - Converted into CMDB-compatible format automatically ---- - ## Design Principles for YAML ### Treat YAML as Structured Data -If the parameter represents structured configuration, it must be a YAML object, not multiline string, not JSON string - ---- +If the parameter represents structured configuration, it must be a YAML object, not a multiline string or a JSON string. ### Preserve Type Integrity @@ -313,26 +285,16 @@ replicas: 3 enabled: true ``` ---- - ### Avoid Embedded JSON Inside YAML -Do not use: - -```yaml -config: '{"limits":{"cpu":"1"}}' -``` - -If you see JSON inside YAML, it is almost always incorrect. - ---- +Do not embed JSON in YAML values (e.g. `config: '{"limits":{"cpu":"1"}}'`). See [No Manual Escaping](#no-manual-escaping). ## Migration Strategy for Existing Multiline or JSON Strings If complex parameters already exist as multiline strings or JSON in a string: 1. Identify parameters to migrate: - - **Multiline:** look for keys whose value uses `|` (or `>-` / `|`) with YAML-like or list-like content underneath. + - **Multiline:** look for keys whose value uses `|` or `>` (block scalars) with YAML-like or list-like content underneath. - **JSON in string:** look for values that are single-quoted or double-quoted JSON (e.g. `'{"key":...}'` or `"{ \"key\": ... }"`). 2. Convert content into structured YAML (native map or list). 3. Validate: @@ -342,8 +304,6 @@ If complex parameters already exist as multiline strings or JSON in a string: 4. Remove manual escaping where it was used for JSON. 5. Add YAML linting to CI pipelines. ---- - ## Operational Impact Adopting YAML objects improves: @@ -355,16 +315,10 @@ Adopting YAML objects improves: - Reduction of CMDB import failures - Long-term configuration scalability ---- - ## Final Recommendation -For all complex parameters: - -- Define maps and lists as native YAML objects. -- Allow automatic transformation during CMDB import. -- Preserve structural and type integrity. -- Enforce YAML linting in CI/CD. -- Never use multiline strings for structured configuration. +- Use native YAML objects only for maps and lists (see [Core Rule](#core-rule)). +- Rely on automatic CMDB transformation and enforce YAML linting in CI/CD. +- Keep configuration declarative and type-safe. -Configuration should be declarative, structured, and type-safe. YAML objects enable that. Multiline and JSON strings undermine it. +YAML objects enable that; multiline and JSON strings undermine it. From 7d0abd6c0e05da916c1face125f85ffbbbff8471 Mon Sep 17 00:00:00 2001 From: Jackson Raj A <150916845+BackendBits@users.noreply.github.com> Date: Fri, 13 Mar 2026 11:24:10 +0530 Subject: [PATCH 087/161] fix: for unit duplicated code in paramset sorting (#1099) --- .../tests/env-build/test_paramset_sorting.py | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/scripts/build_env/tests/env-build/test_paramset_sorting.py b/scripts/build_env/tests/env-build/test_paramset_sorting.py index 0099ccb50..e88b7d102 100644 --- a/scripts/build_env/tests/env-build/test_paramset_sorting.py +++ b/scripts/build_env/tests/env-build/test_paramset_sorting.py @@ -1,27 +1,15 @@ from envgenehelper.business_helper import is_from_template_dir - +from build_env import sort_paramsets_with_same_name class TestSortParamsetsWithSameName: - @staticmethod - def sort_paramsets_with_same_name(entries: list[dict]) -> list[dict]: - def sort_key(e): - path = e["filePath"] - if "from_instance" in path: - return 2, path - elif is_from_template_dir(path): - return 0, path - return 1, path - - return sorted(entries, key=sort_key) - def test_all_three_levels(self): entries = [ {"filePath": "/tmp/render/parameters/from_instance/test.yml", "envSpecific": True}, {"filePath": "/tmp/render/parameters/test.yml", "envSpecific": False}, {"filePath": "/tmp/render/parameters/from_template/test.yml", "envSpecific": False}, ] - sorted_entries = self.sort_paramsets_with_same_name(entries) + sorted_entries = sort_paramsets_with_same_name(entries) assert "from_template" in sorted_entries[0]["filePath"] assert "from_instance" not in sorted_entries[1]["filePath"] assert "from_instance" in sorted_entries[2]["filePath"] @@ -31,7 +19,7 @@ def test_template_and_instance(self): {"filePath": "/tmp/render/parameters/from_instance/test.yml", "envSpecific": True}, {"filePath": "/tmp/render/parameters/from_template/test.yml", "envSpecific": False}, ] - sorted_entries = self.sort_paramsets_with_same_name(entries) + sorted_entries = sort_paramsets_with_same_name(entries) assert "from_template" in sorted_entries[0]["filePath"] assert "from_instance" in sorted_entries[1]["filePath"] @@ -42,7 +30,7 @@ def test_origin_peer_templates(self): {"filePath": "/tmp/render/parameters/from_peer_template/test.yml", "envSpecific": False}, {"filePath": "/tmp/render/parameters/from_origin_template/test.yml", "envSpecific": False}, ] - sorted_entries = self.sort_paramsets_with_same_name(entries) + sorted_entries = sort_paramsets_with_same_name(entries) assert "from_origin_template" in sorted_entries[0]["filePath"] assert "from_peer_template" in sorted_entries[1]["filePath"] assert "from_template" in sorted_entries[2]["filePath"] @@ -54,7 +42,7 @@ def test_multiple_files_sorted_alphabetically(self): {"filePath": "/tmp/render/parameters/from_template/a_params.yml", "envSpecific": False}, {"filePath": "/tmp/render/parameters/from_template/m_params.yml", "envSpecific": False}, ] - sorted_entries = self.sort_paramsets_with_same_name(entries) + sorted_entries = sort_paramsets_with_same_name(entries) paths = [e["filePath"] for e in sorted_entries] assert paths == sorted(paths) @@ -63,9 +51,9 @@ def test_real_world_dcl_e2e(self): {"filePath": "/tmp/render/parameters/from_instance/DCL_E2E_parameters.yaml", "envSpecific": True}, {"filePath": "/tmp/render/parameters/from_template/e2e/dcl.yaml", "envSpecific": False}, ] - sorted_entries = self.sort_paramsets_with_same_name(entries) + sorted_entries = sort_paramsets_with_same_name(entries) assert "from_template" in sorted_entries[0]["filePath"] assert "from_instance" in sorted_entries[1]["filePath"] def test_empty_list(self): - assert len(self.sort_paramsets_with_same_name([])) == 0 + assert len(sort_paramsets_with_same_name([])) == 0 From c80caee1e845095c59060d50412b2d502609c111 Mon Sep 17 00:00:00 2001 From: Siva Reddy Kunduru <35566000+sivareddyit@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:42:15 +0530 Subject: [PATCH 088/161] fix: changed logic to create the repo url from registry (#1122) fix: change logic to create the repo url from registry fix: changed logic to create the repo url from registry fix: changed logic to create the repo url from registry fix: changed logic to create the repo url from registry --- .../artifact_searcher/artifact.py | 6 +- .../artifact_searcher/test_artifact.py | 76 +++++++++++++++++++ .../artifact_searcher/utils/models.py | 21 +++-- .../env_template/process_env_template.py | 4 +- 4 files changed, 91 insertions(+), 16 deletions(-) diff --git a/python/artifact-searcher/artifact_searcher/artifact.py b/python/artifact-searcher/artifact_searcher/artifact.py index f237d70e6..c40b7cde6 100644 --- a/python/artifact-searcher/artifact_searcher/artifact.py +++ b/python/artifact-searcher/artifact_searcher/artifact.py @@ -16,6 +16,7 @@ from artifact_searcher.utils.models import Registry, Application, FileExtension, Credentials, ArtifactInfo from envgenehelper import logger from requests.auth import HTTPBasicAuth +from artifact_searcher.utils.models import MavenConfig WORKSPACE = os.getenv("WORKSPACE", Path(tempfile.gettempdir()) / "zips") @@ -306,7 +307,7 @@ async def check_artifact_async( if result is not None: return result - if not app.registry.maven_config.is_nexus: + if not MavenConfig.is_nexus(app.registry.maven_config.repository_domain_name): return result # trying to edit url for nexus and repeat @@ -403,6 +404,9 @@ def check_artifact(repo_url: str, group_id: str, artifact_id: str, version: str, artifact_extension: FileExtension, cred: Credentials | None = None, classifier: str = "") -> str | None: + if MavenConfig.is_nexus(repo_url): + repo_url = convert_nexus_repo_url_to_index_view(repo_url) + base = repo_url.rstrip("/") + "/" group_id = group_id.replace(".", "/") diff --git a/python/artifact-searcher/artifact_searcher/test_artifact.py b/python/artifact-searcher/artifact_searcher/test_artifact.py index e02a46435..9ded075ff 100644 --- a/python/artifact-searcher/artifact_searcher/test_artifact.py +++ b/python/artifact-searcher/artifact_searcher/test_artifact.py @@ -2,11 +2,18 @@ import pytest from aiohttp import web +from unittest.mock import patch, Mock os.environ["DEFAULT_REQUEST_TIMEOUT"] = "0.2" # for test cases to run quicker from artifact_searcher.utils import models from artifact_searcher.artifact import check_artifact_async +from artifact_searcher.artifact import check_artifact +from artifact_searcher.utils.models import FileExtension +TEST_REPO = "https://repo.example.com/repository/" +GROUP_ID = "com.example" +ARTIFACT_ID = "demo" +VERSION = "1.0.0" class MockResponse: def __init__(self, status_code): @@ -93,3 +100,72 @@ def mock_get(url, *args, **kwargs): sample_url = f"{base_url.rstrip('/repository/')}{index_path}repo/com/example/app/1.0.0-SNAPSHOT/app-1.0.0-20240702.123456-1.json" assert full_url == sample_url, f"expected: {sample_url}, received: {full_url}" + +@patch("artifact_searcher.artifact.requests.head") +@patch("artifact_searcher.artifact.create_artifact_name") +@patch("artifact_searcher.artifact.version_to_folder_name") +@patch("artifact_searcher.artifact.MavenConfig.is_nexus") +def test_artifact_found(mock_nexus, mock_folder, mock_name, mock_head): + + mock_nexus.return_value = False + mock_folder.return_value = VERSION + mock_name.return_value = "demo-1.0.0.zip" + + response = Mock() + response.status_code = 200 + mock_head.return_value = response + + result = check_artifact( + TEST_REPO, + GROUP_ID, + ARTIFACT_ID, + VERSION, + FileExtension.ZIP + ) + + assert result == ( + "https://repo.example.com/repository/com/example/demo/1.0.0/demo-1.0.0.zip" + ) + + +@patch("artifact_searcher.artifact.requests.head") +@patch("artifact_searcher.artifact.create_artifact_name") +@patch("artifact_searcher.artifact.version_to_folder_name") +@patch("artifact_searcher.artifact.MavenConfig.is_nexus") +def test_artifact_not_found(mock_nexus, mock_folder, mock_name, mock_head): + + mock_nexus.return_value = False + mock_folder.return_value = VERSION + mock_name.return_value = "demo-1.0.0.zip" + + response = Mock() + response.status_code = 404 + mock_head.return_value = response + + result = check_artifact( + TEST_REPO, + GROUP_ID, + ARTIFACT_ID, + VERSION, + FileExtension.ZIP + ) + + assert result is None + + +@patch("artifact_searcher.artifact.MavenConfig.is_nexus") +@patch("artifact_searcher.artifact.convert_nexus_repo_url_to_index_view") +def test_nexus_repo_conversion(mock_convert, mock_detect): + + mock_detect.return_value = True + mock_convert.return_value = "https://nexus.example.com/service/rest/repository/browse/" + + check_artifact( + TEST_REPO, + GROUP_ID, + ARTIFACT_ID, + VERSION, + FileExtension.ZIP + ) + + mock_convert.assert_called_once() diff --git a/python/artifact-searcher/artifact_searcher/utils/models.py b/python/artifact-searcher/artifact_searcher/utils/models.py index 8d76b6b87..bf34153c2 100644 --- a/python/artifact-searcher/artifact_searcher/utils/models.py +++ b/python/artifact-searcher/artifact_searcher/utils/models.py @@ -26,8 +26,6 @@ class MavenConfig(BaseSchema): snapshot_group: Optional[str] = "" release_group: Optional[str] = "" - is_nexus: bool = False - @field_validator('full_repository_url') def check_full_repository_url(cls, full_repository_url): if full_repository_url: @@ -38,20 +36,19 @@ def check_full_repository_url(cls, full_repository_url): def ensure_trailing_slash(cls, value): return value.rstrip("/") + "/" - @model_validator(mode="after") - def detect_nexus(self): - if not self.repository_domain_name.endswith("/repository/"): - return self - base = self.repository_domain_name[: -len("repository/")] + @staticmethod + def is_nexus(repository_domain_name: str) -> bool: + if not repository_domain_name.endswith("/repository/"): + return False + + base = repository_domain_name[: -len("repository/")] status_url = f"{base}service/rest/v1/status" + try: resp = requests.get(status_url, timeout=DEFAULT_REQUEST_TIMEOUT) - self.is_nexus = resp.status_code == 200 + return resp.status_code == 200 except Exception: - self.is_nexus = False - - return self - + return False class DockerConfig(BaseSchema): snapshot_uri: Optional[str] = "" diff --git a/scripts/build_env/env_template/process_env_template.py b/scripts/build_env/env_template/process_env_template.py index d0d9f7da9..aa00ebe03 100644 --- a/scripts/build_env/env_template/process_env_template.py +++ b/scripts/build_env/env_template/process_env_template.py @@ -2,7 +2,6 @@ import os import tempfile from pathlib import Path -from urllib.parse import urlparse from artifact_searcher import artifact from artifact_searcher.utils.models import FileExtension, Credentials, Registry, Application @@ -86,8 +85,7 @@ async def resolve_artifact_new_logic(app_def: Application, app_version: str, tem repo_url = dd_config.get("configurations", [{}])[0].get("maven_repository") if not repo_url: - parsed = urlparse(dd_url) - repo_url = f"{parsed.scheme}://{parsed.netloc}/{repo_name}" + repo_url = f"{app_def.registry.maven_config.repository_domain_name}/{repo_name}" logger.info(f"building repo url from the repo name : {repo_url}") template_url = artifact.check_artifact(repo_url, group_id, artifact_id, version, FileExtension.ZIP, cred) validate_url(template_url, group_id, artifact_id, version) From cda345fec6976e59a2e651200303764eabbffe34 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Tue, 17 Mar 2026 10:16:35 +0000 Subject: [PATCH 089/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 01d5bd1bd..c86a62ab5 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.6" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.6" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.6" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.7" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.7" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.7" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index a77d70810..d2b62d8db 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.6 +version: 1.31.7 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index f424ca457..5e324ea17 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.6 +version: 1.31.7 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index ba9ca70db..225d6f0fd 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.6", + "envgene_version": "1.31.7", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 8b26748166b8f22976608cbff68aee27cd68771d Mon Sep 17 00:00:00 2001 From: miyamuraga <198181742+miyamuraga@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:36:21 +0500 Subject: [PATCH 090/161] fix: if replacing profile by env specific profile, name under of associated ns does not change (#1112) --- python/envgene/envgenehelper/file_helper.py | 12 ++- scripts/build_env/build_env.py | 40 ++++++--- scripts/build_env/resource_profiles.py | 86 ++++++++++--------- .../Namespaces/billing/namespace.yml | 2 +- 4 files changed, 83 insertions(+), 57 deletions(-) diff --git a/python/envgene/envgenehelper/file_helper.py b/python/envgene/envgenehelper/file_helper.py index 4210ba9c9..6b547612c 100644 --- a/python/envgene/envgenehelper/file_helper.py +++ b/python/envgene/envgenehelper/file_helper.py @@ -199,18 +199,22 @@ def findFiles(fileList: list[Path], pattern, notPattern="", additionalRegexpPatt for filePath in fileList: # this ensures that pattern matching works correctly on both Windows (\) and Unix (/) file_path_posix = Path(filePath).as_posix() + pattern_posix = Path(pattern).as_posix() if pattern else "" + not_pattern_posix = Path(notPattern).as_posix() if notPattern else "" if ( - pattern in file_path_posix - and (notPattern == "" or notPattern not in file_path_posix) + pattern_posix in file_path_posix + and (not_pattern_posix == "" or not_pattern_posix not in file_path_posix) and (additionalRegexpPattern == "" or re.match(additionalRegexpPattern, file_path_posix)) and (additionalRegexpNotPattern == "" or not re.match(additionalRegexpNotPattern, file_path_posix)) ): result.append(filePath) logger.debug( - f"Path {filePath} match pattern: {pattern} or notPattern: {notPattern} or additionalPattern: {additionalRegexpPattern}") + f"Path {filePath} match pattern: {pattern_posix} or notPattern: {not_pattern_posix} " + f"or additionalPattern: {additionalRegexpPattern}") else: logger.debug( - f"Path {filePath} doesn't match pattern: {pattern} or notPattern: {notPattern} or additionalPattern: {additionalRegexpPattern}") + f"Path {filePath} doesn't match pattern: {pattern_posix} or notPattern: {not_pattern_posix} " + f"or additionalPattern: {additionalRegexpPattern}") return result diff --git a/scripts/build_env/build_env.py b/scripts/build_env/build_env.py index 1490bb243..dd40aec66 100644 --- a/scripts/build_env/build_env.py +++ b/scripts/build_env/build_env.py @@ -1,14 +1,10 @@ -import os -import copy import yaml -import re -import pathlib -from pathlib import Path - from envgenehelper import * -from resource_profiles import processResourceProfiles -from schema_validation import checkEnvSpecificParametersBySchema + from cloud_passport import process_cloud_passport +from resource_profiles import collect_resource_profiles, override_by_env_specific_profiles, has_valid_profile_name, \ + update_profile_name +from schema_validation import checkEnvSpecificParametersBySchema # const GENERATED_HEADER = "The contents of this file is generated from template artifact: %s.\nContents will be overwritten by next generation.\nPlease modify this contents only for development purposes or as workaround." @@ -421,8 +417,7 @@ def processTemplate(templatePath, templateName, env_instances_dir, schema_path, env_instances_dir) templateContent["technicalConfigurationParameterSets"] = [] # preparing map for needed resource profiles - if "profile" in templateContent and templateContent["profile"] and "name" in templateContent["profile"] and \ - templateContent["profile"]["name"]: + if has_valid_profile_name(templateContent): rpName = templateContent["profile"]["name"] resource_profiles_map[templateName] = rpName writeYamlToFile(templatePath, templateContent) @@ -570,5 +565,26 @@ def build_env(env_name, env_instances_dir, parameters_dir, env_template_dir, res checkEnvSpecificParametersBySchema(env_dir, env_specific_parameters_map, template_namespace_names) # process resource profiles - processResourceProfiles(env_dir, resource_profiles_dir, profiles_schema, needed_resource_profiles_map, - env_specific_resource_profile_map, render_context, header_text=generated_header_text) + result_profiles_dir = Path(f"{env_dir}/Profiles") + all_profiles = collect_resource_profiles(result_profiles_dir, resource_profiles_dir, profiles_schema, + needed_resource_profiles_map, render_context) + override_profile_map = override_by_env_specific_profiles(all_profiles, env_specific_resource_profile_map, + render_context) + + if override_profile_map: + for profile_key, profile_file_path in override_profile_map.items(): + all_profiles[profile_key] = profile_file_path + profile_name = openYaml(profile_file_path, {}).get("name") + + if profile_key == 'cloud': + update_profile_name(cloudTemlatePath, profile_name) + + for ns in namespaces: + if profile_key == ns.postfix: + update_profile_name(ns.definition_path, profile_name) + + for profile_key, profile_file_path in all_profiles.items(): + logger.info(f"Copying '{profile_key}' to resulting directory '{result_profiles_dir}'") + copy_path(profile_file_path, f"{result_profiles_dir}/") + resulting_profile_path = result_profiles_dir / Path(profile_file_path).name + beautifyYaml(resulting_profile_path, profiles_schema, generated_header_text) diff --git a/scripts/build_env/resource_profiles.py b/scripts/build_env/resource_profiles.py index 77ab1ea41..b710ec9e1 100644 --- a/scripts/build_env/resource_profiles.py +++ b/scripts/build_env/resource_profiles.py @@ -1,4 +1,3 @@ -import copy from envgenehelper import * from render_config_env import EnvGenerator @@ -20,7 +19,8 @@ def get_env_specific_resource_profiles(env_dir, instances_dir, rp_schema): logger.debug(f"Searching for env specific resource profiles for template '{templateType}'") profileFileName = envSepcificResourceProfileNames[templateType] logger.debug(f"Searching for {profileFileName} for template type {templateType}") - resourceProfileFiles = findResourcesBottomTop(env_dir, instances_dir, f"/{profileFileName}.", f"{env_dir}/Profiles/") + resourceProfileFiles = findResourcesBottomTop(env_dir, instances_dir, f"/{profileFileName}.", + f"{env_dir}/Profiles/") if len(resourceProfileFiles) == 1: yamlPath = resourceProfileFiles[0] result[templateType] = yamlPath @@ -30,7 +30,8 @@ def get_env_specific_resource_profiles(env_dir, instances_dir, rp_schema): logger.error( f"Duplicate resource profile files with key '{profileFileName}' found in '{instances_dir}': \n\t" + ",\n\t".join( str(x) for x in resourceProfileFiles)) - raise ReferenceError( f"Duplicate resource profile files with key '{profileFileName}' found. See logs above.") + raise ReferenceError( + f"Duplicate resource profile files with key '{profileFileName}' found. See logs above.") else: raise ReferenceError(f"Resource profile file with key '{profileFileName}' not found in '{instances_dir}'") logger.info(f"Env specific resource profiles are: \n{dump_as_yaml_format(result)}") @@ -130,53 +131,58 @@ def validate_resource_profiles(needed_resource_profiles: dict[str, str], source_ return profiles_map -def processResourceProfiles(env_dir, resource_profiles_dir, profiles_schema, needed_resource_profiles_map, - env_specific_resource_profile_map, render_context: EnvGenerator, header_text=""): - logger.info(f"Needed profiles map: \n{dump_as_yaml_format(needed_resource_profiles_map)}") - render_context.generate_profiles(set(needed_resource_profiles_map.values())) +def collect_resource_profiles(result_profiles_dir, render_profiles_dir, profiles_schema, + required_resource_profiles_map, render_context: EnvGenerator): + logger.info(f"Required profiles map:\n{dump_as_yaml_format(required_resource_profiles_map)}") + render_context.generate_profiles(set(required_resource_profiles_map.values())) + all_profiles = getResourceProfilesFromDir(render_profiles_dir) | getResourceProfilesFromDir(result_profiles_dir) + logger.info(f"All existing resource profiles map is:\n{dump_as_yaml_format(all_profiles)}") + profiles_map = validate_resource_profiles(required_resource_profiles_map, all_profiles, profiles_schema) + return profiles_map + + +def override_by_env_specific_profiles(all_profiles, env_specific_resource_profile_map, render_context: EnvGenerator): + override_profile_map = {} render_context.generate_profiles(set(env_specific_resource_profile_map.values())) - # map for profiles from templates - templateProfilesMap = getResourceProfilesFromDir(resource_profiles_dir) - envRpDir = f"{env_dir}/Profiles" - environmentDirProfilesMap = getResourceProfilesFromDir(envRpDir) - # joining resource profiles with the result of Jinja generation - sourceProfilesMap = templateProfilesMap | environmentDirProfilesMap - logger.info(f"All resource profiles map is: \n{dump_as_yaml_format(sourceProfilesMap)}") - # check that all required resource profiles exists and are valid - profilesMap = validate_resource_profiles(needed_resource_profiles_map, sourceProfilesMap, profiles_schema) - # iterate through env specific resource profiles and perform override - for templateName, envSpecificProfileFile in env_specific_resource_profile_map.items(): - if templateName not in profilesMap: - logger.error( - f"No override profile for {templateName} found. Can't apply environment specific resource profile {envSpecificProfileFile}" - ) + for profile_key, env_specific_profile_path in env_specific_resource_profile_map.items(): + if profile_key not in all_profiles: raise ReferenceError( - f"Can't apply environment specific resource profile for namespace {templateName}. Please set override profile in templates first." + f"Environment specific profile '{env_specific_profile_path}' cannot be applied " + f"for profile key '{profile_key}', because no base template profile was found" ) - logger.info(f"Found template override profile for namespace '{templateName}' with environment specific profile {envSpecificProfileFile}") - templateProfileFilePath = profilesMap[templateName] - templateProfileYaml = openYaml(templateProfileFilePath) - envSpecificProfileYaml = openYaml(envSpecificProfileFile) + logger.info(f"Found template override profile for profile key '{profile_key}'" + f" with environment specific profile {env_specific_profile_path}") + template_profile_file_path = all_profiles[profile_key] + template_profile_yaml = openYaml(template_profile_file_path) + env_specific_profile_yaml = openYaml(env_specific_profile_path) combination_mode_key = "mergeEnvSpecificResourceProfiles" try: combination_mode = render_context.ctx.env_definition['inventory']['config'][combination_mode_key] except KeyError: - logger.info(f"inventory.config.{combination_mode_key} key not found in env_definition, default value is 'true'") + logger.info( + f"inventory.config.{combination_mode_key} key not found in env_definition, default value is 'true'") combination_mode = 'true' - common_msg = (f"profile overrides, because {combination_mode_key} is set to {combination_mode}") - # decide here whether to merge or replace + common_msg = f"profile overrides, because {combination_mode_key} is set to {combination_mode}" + if str(combination_mode).lower() == 'true': logger.info(f"Joining {common_msg}") - merge_resource_profiles(templateProfileYaml, envSpecificProfileYaml, extractNameFromFile(envSpecificProfileFile)) - writeYamlToFile(templateProfileFilePath, templateProfileYaml) + merge_resource_profiles(template_profile_yaml, env_specific_profile_yaml, + extractNameFromFile(env_specific_profile_path)) + writeYamlToFile(template_profile_file_path, template_profile_yaml) else: logger.info(f"Replacing {common_msg}") - profilesMap[templateName] = envSpecificProfileFile - - for profileKey, profileFilePath in profilesMap.items(): - logger.info(f"Copying '{profileKey}' to resulting directory '{envRpDir}'") - copy_path(profileFilePath, f"{envRpDir}/") - resultingProfilePath = f"{envRpDir}/{extractNameFromFile(profileFilePath)}.yml" - resultingProfilePath = identify_yaml_extension(resultingProfilePath) - beautifyYaml(resultingProfilePath, profiles_schema, header_text) + override_profile_map[profile_key] = env_specific_profile_path + return override_profile_map + + +def has_valid_profile_name(content: dict) -> bool: + profile = content.get("profile") + return isinstance(profile, dict) and bool(profile.get("name")) + + +def update_profile_name(file_path, profile_name): + data = openYaml(file_path, {}) + if has_valid_profile_name(data): + set_nested_yaml_attribute(data, "profile.name", profile_name) + writeYamlToFile(file_path, data) diff --git a/test_data/test_environments/cluster03/rpo-replacement-mode/Namespaces/billing/namespace.yml b/test_data/test_environments/cluster03/rpo-replacement-mode/Namespaces/billing/namespace.yml index 2cd8de8ee..707fa0f3c 100644 --- a/test_data/test_environments/cluster03/rpo-replacement-mode/Namespaces/billing/namespace.yml +++ b/test_data/test_environments/cluster03/rpo-replacement-mode/Namespaces/billing/namespace.yml @@ -10,8 +10,8 @@ labels: cleanInstallApprovalRequired: true mergeDeployParametersAndE2EParameters: false profile: - name: "dev_billing_override" baseline: "dev" + name: "medium_billing_override" deployParameters: ENVGENE_CONFIG_REF_NAME: "branch_name" ENVGENE_CONFIG_TAG: "No Ref tag" From b136ffc5d62eb5253f8d3e6183dc997c63f33af5 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Tue, 17 Mar 2026 13:25:15 +0000 Subject: [PATCH 091/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index c86a62ab5..c102d1f7a 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.7" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.7" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.7" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.8" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.8" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.8" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index d2b62d8db..bf4cc6438 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.7 +version: 1.31.8 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 5e324ea17..d2def9b5a 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.7 +version: 1.31.8 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 225d6f0fd..8865df4e6 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.7", + "envgene_version": "1.31.8", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 0b145cbafef1acf8d52b5db80bc7691e66f26ba4 Mon Sep 17 00:00:00 2001 From: popoveugene <42543333+popoveugene@users.noreply.github.com> Date: Wed, 18 Mar 2026 02:04:54 +0300 Subject: [PATCH 092/161] docs: update Reg Def v2 (#1045) * docs: update Reg Def v2 * docs: reg def art def model change * docs: add UC --- docs/envgene-objects.md | 183 ++- docs/use-cases/artifact-downloading.md | 1325 ++++++++++++++++++++ docs/use-cases/env-template-downloading.md | 69 - schemas/artifact-definition-v2.schema.json | 143 ++- schemas/regdef-v2.schema.json | 147 ++- 5 files changed, 1725 insertions(+), 142 deletions(-) create mode 100644 docs/use-cases/artifact-downloading.md delete mode 100644 docs/use-cases/env-template-downloading.md diff --git a/docs/envgene-objects.md b/docs/envgene-objects.md index 4c3072e5c..7041f56bb 100644 --- a/docs/envgene-objects.md +++ b/docs/envgene-objects.md @@ -1502,7 +1502,8 @@ registry: # Supports advanced authentication methods including public cloud registries authConfig: : - # Mandatory + # Optional + # Not used in case of `authMethod: anonymous` # Pointer to the EnvGene Credential object. # Depending on `authType`, it can be: # access key (username) + secret (password) for longLived @@ -1512,18 +1513,19 @@ registry: # Public cloud registry authentication strategy # Used in case of public cloud registries authType: enum [ shortLived, longLived ] - # Optional - # Public cloud registry type - # Used in case of public cloud registries - provider: enum [ aws, azure, gcp ] - # Optional + # Mandatory + # Registry type + provider: enum [ aws, azure, gcp, nexus, artifactory ] + # Mandatory # In case of non-cloud public registries, `user_pass` is used # In case of public cloud registries valid values, depends on `provider`: - # `aws`: `secret` or `assume_role` - # `gcp`: `federation` or `service_account` - # `azure`: `oauth2` - authMethod: enum [ secret, assume_role, federation, service_account, oauth2, user_pass ] - # Mandatory + # `nexus`: `user_pass` or `anonymous` + # `artifactory`: `user_pass` or `anonymous` + # `aws`: `secret`, `assume_role` or `anonymous` + # `gcp`: `federation`, `service_account` or `anonymous` + # `azure`: `oauth2` or `anonymous` + authMethod: enum [ secret, assume_role, federation, service_account, oauth2, user_pass, anonymous ] + # Optional # Region of the AWS cloud # Used with `provider: aws` only awsRegion: string @@ -1587,28 +1589,31 @@ registry: azureArtifactsResource: string # Mandatory mavenConfig: - # Optional + # Mandatory # Pointer to authentication config described in `authConfig` section - # Cannot be set if anonymous access is used authConfig: string # Mandatory # Domain name of the registry repositoryDomainName: string - # Mandatory + # Optional + # Used in case of provider nexus or artifactory only # Snapshot repository name # EnvGene checks repositories in this order: release -> staging -> snapshot # It stops when it finds the artifact targetSnapshot: string - # Mandatory + # Optional + # Used in case of provider nexus or artifactory only # Staging repository name targetStaging: string - # Mandatory + # Optional + # Used in case of provider nexus or artifactory only # Release repository name targetRelease: string - # Mandatory + # Optional + # Used in case of provider nexus or artifactory only # Snapshot Maven repository group name snapshotGroup: string - # Mandatory + # Optional # Release Maven repository group name releaseGroup: string ``` @@ -1625,6 +1630,7 @@ registry: authConfig: maven-auth: authType: longLived + provider: nexus authMethod: user_pass credentialsId: "artifactory-cred" mavenConfig: @@ -1729,6 +1735,42 @@ registry: releaseGroup: "maven-releases-group" ``` +**Authentication Configuration Dependencies:** + +The `authConfig` section has complex dependencies between attributes. The following table shows which fields are required based on `provider` and `authMethod` values: + +| Field | Condition | Required | +|--------------------------|-----------------------------------------------------|---------------| +| `provider` | Always | **REQUIRED** | +| `authMethod` | Always | **REQUIRED** | +| `credentialsId` | `authMethod != "anonymous"` | **REQUIRED** | +| `authType` | `provider IN ["aws", "azure", "gcp"]` | OPTIONAL | +| `awsRegion` | `provider == "aws"` | OPTIONAL | +| `awsDomain` | `provider == "aws"` (required for CodeArtifact) | **REQUIRED** | +| `awsRoleARN` | `provider == "aws" AND authMethod == "assume_role"` | **REQUIRED** | +| `awsRoleSessionPrefix` | `provider == "aws" AND authMethod == "assume_role"` | OPTIONAL | +| `gcpOIDC` | `provider == "gcp" AND authMethod == "federation"` | **REQUIRED** | +| `gcpOIDC.URL` | Inside `gcpOIDC` | **REQUIRED** | +| `gcpOIDC.customParams` | Inside `gcpOIDC` | OPTIONAL | +| `gcpRegProject` | `provider == "gcp" AND authMethod == "federation"` | OPTIONAL | +| `gcpRegPoolId` | `provider == "gcp" AND authMethod == "federation"` | OPTIONAL | +| `gcpRegProviderId` | `provider == "gcp" AND authMethod == "federation"` | OPTIONAL | +| `gcpRegSAEmail` | `provider == "gcp" AND authMethod == "federation"` | OPTIONAL | +| `azureTenantId` | `provider == "azure"` | OPTIONAL | +| `azureACRResource` | `provider == "azure"` | OPTIONAL | +| `azureACRName` | `provider == "azure"` (required for ACR) | **REQUIRED** | +| `azureArtifactsResource` | `provider == "azure"` | OPTIONAL | + +**Valid `authMethod` values per `provider`:** + +| Provider | Valid authMethod values | +|---------------|----------------------------------------------| +| `nexus` | `user_pass`, `anonymous` | +| `artifactory` | `user_pass`, `anonymous` | +| `aws` | `secret`, `assume_role`, `anonymous` | +| `gcp` | `federation`, `service_account`, `anonymous` | +| `azure` | `oauth2`, `anonymous` | + [Artifact Definition v2.0 JSON schema](/schemas/artifact-definition-v2.schema.json) ### Registry Definition @@ -1900,7 +1942,8 @@ name: string # Authentication configs authConfig: : - # Mandatory + # Optional + # Not used in case of `authMethod: anonymous` # Pointer to the EnvGene Credential object. # Depending on `authType`, it can be: # access key (username) + secret (password) for longLived @@ -1910,18 +1953,19 @@ authConfig: # Public cloud registry authentication strategy # Used in case of public cloud registries authType: enum [ shortLived, longLived ] - # Optional - # Public cloud registry type - # Used in case of public cloud registries - provider: enum [ aws, azure, gcp ] - # Optional + # Mandatory + # Registry type + provider: enum [ aws, azure, gcp, nexus, artifactory ] + # Mandatory # In case of non-cloud public registries, `user_pass` is used # In case of public cloud registries valid values, depends on `provider`: - # `aws`: `secret` or `assume_role` - # `gcp`: `federation` or `service_account` - # `azure`: `oauth2` - authMethod: enum [ secret, assume_role, federation, service_account, oauth2, user_pass ] - # Mandatory + # `nexus`: `user_pass` or `anonymous` + # `artifactory`: `user_pass` or `anonymous` + # `aws`: `secret`, `assume_role` or `anonymous` + # `gcp`: `federation`, `service_account` or `anonymous` + # `azure`: `oauth2` or `anonymous` + authMethod: enum [ secret, assume_role, federation, service_account, oauth2, user_pass, anonymous ] + # Optional # Region of the AWS cloud # Used with `provider: aws` only awsRegion: string @@ -1971,6 +2015,10 @@ authConfig: # Used with `provider: azure` only azureTenantId: string # Optional + # Region of the GCP cloud + # Used with `provider: gcp` only + gcpRegion: string + # Optional # Target resource for ACR # Used with `provider: azure` only azureACRResource: string @@ -1987,31 +2035,33 @@ authConfig: mavenConfig: # Mandatory # Pointer to authentication config described in `authConfig` section - # Cannot be set in if anonymous access is used authConfig: string # Mandatory # Domain name of the registry repositoryDomainName: string - # Mandatory + # Optional + # Used in case of authMethod nexus or artifactory only # Snapshot Maven repository name targetSnapshot: string - # Mandatory + # Optional + # Used in case of authMethod nexus or artifactory only # Staging Maven repository name targetStaging: string - # Mandatory + # Optional + # Used in case of authMethod nexus or artifactory only # Release Maven repository name targetRelease: string - # Mandatory + # Optional + # Used in case of authMethod nexus or artifactory only # Snapshot Maven repository name snapshotGroup: string - # Mandatory + # Optional # Release Maven repository name releaseGroup: string -# Mandatory +# Optional dockerConfig: # Mandatory # Pointer to authentication config described in `authConfig` section - # Cannot be set in if anonymous access is used authConfig: string # Mandatory # URI for Docker snapshot registry @@ -2041,7 +2091,6 @@ dockerConfig: helmConfig: # Mandatory # Pointer to authentication config described in `authConfig` section - # Cannot be set in if anonymous access is used authConfig: string # Mandatory # Domain name of the registry @@ -2076,14 +2125,12 @@ helmAppConfig: goConfig: # Mandatory # Pointer to authentication config described in `authConfig` section - # Cannot be set in if anonymous access is used authConfig: string # Mandatory # Domain name of the registry repositoryDomainName: string # Mandatory - # Pointer to authentication config described in `authConfig` section - # Cannot be set in if anonymous access is used + # Go snapshot repository name goTargetSnapshot: string # Mandatory # Go release repository name @@ -2095,7 +2142,6 @@ goConfig: npmConfig: # Mandatory # Pointer to authentication config described in `authConfig` section - # Cannot be set in if anonymous access is used authConfig: string # Mandatory # Domain name of the registry @@ -2110,14 +2156,12 @@ npmConfig: rawConfig: # Mandatory # Pointer to authentication config described in `authConfig` section - # Cannot be set in if anonymous access is used authConfig: string # Mandatory # Domain name of the registry repositoryDomainName: string # Mandatory - # Pointer to authentication config described in `authConfig` section - # Cannot be set in if anonymous access is used + # Raw snapshot repository name rawTargetSnapshot: string # Mandatory # Raw release repository name @@ -2130,6 +2174,43 @@ rawConfig: rawTargetProxy: string ``` +**Authentication Configuration Dependencies:** + +The `authConfig` section has complex dependencies between attributes. The following table shows which fields are required based on `provider` and `authMethod` values: + +| Field | Condition | Required | +|--------------------------|-----------------------------------------------------|---------------| +| `provider` | Always | **REQUIRED** | +| `authMethod` | Always | **REQUIRED** | +| `credentialsId` | `authMethod != "anonymous"` | **REQUIRED** | +| `authType` | `provider IN ["aws", "azure", "gcp"]` | OPTIONAL | +| `awsRegion` | `provider == "aws"` | OPTIONAL | +| `awsDomain` | `provider == "aws"` (required for CodeArtifact) | **REQUIRED** | +| `awsRoleARN` | `provider == "aws" AND authMethod == "assume_role"` | **REQUIRED** | +| `awsRoleSessionPrefix` | `provider == "aws" AND authMethod == "assume_role"` | OPTIONAL | +| `gcpOIDC` | `provider == "gcp" AND authMethod == "federation"` | **REQUIRED** | +| `gcpOIDC.URL` | Inside `gcpOIDC` | **REQUIRED** | +| `gcpOIDC.customParams` | Inside `gcpOIDC` | OPTIONAL | +| `gcpRegProject` | `provider == "gcp" AND authMethod == "federation"` | OPTIONAL | +| `gcpRegPoolId` | `provider == "gcp" AND authMethod == "federation"` | OPTIONAL | +| `gcpRegProviderId` | `provider == "gcp" AND authMethod == "federation"` | OPTIONAL | +| `gcpRegSAEmail` | `provider == "gcp" AND authMethod == "federation"` | OPTIONAL | +| `gcpRegion` | `provider == "gcp"` | OPTIONAL | +| `azureTenantId` | `provider == "azure"` | OPTIONAL | +| `azureACRResource` | `provider == "azure"` | OPTIONAL | +| `azureACRName` | `provider == "azure"` (required for ACR) | **REQUIRED** | +| `azureArtifactsResource` | `provider == "azure"` | OPTIONAL | + +**Valid `authMethod` values per `provider`:** + +| Provider | Valid authMethod values | +|---------------|----------------------------------------------| +| `nexus` | `user_pass`, `anonymous` | +| `artifactory` | `user_pass`, `anonymous` | +| `aws` | `secret`, `assume_role`, `anonymous` | +| `gcp` | `federation`, `service_account`, `anonymous` | +| `azure` | `oauth2`, `anonymous` | + **Examples of different auth sections**: ```yaml @@ -2185,8 +2266,13 @@ authConfig: helm-nexus: authType: longLived + provider: nexus authMethod: user_pass credentialsId: cred-nexus + + docker-anonymous: + provider: nexus + authMethod: anonymous ``` **Example:** @@ -2205,8 +2291,12 @@ authConfig: awsRoleARN: arn:aws:iam::123456789012:role/YourRole helm: authType: longLived + provider: nexus authMethod: user_pass credentialsId: cred-nexus + public-repo: + provider: nexus + authMethod: anonymous mavenConfig: authConfig: aws repositoryDomainName: https://codeartifact.eu-west-1.amazonaws.com/maven/app @@ -2238,15 +2328,18 @@ helmAppConfig: helmReleaseRepoName: helm-releases helmGroupRepoName: helm-group goConfig: + authConfig: public-repo repositoryDomainName: https://nexus.mycompany.internal/repository/go goTargetSnapshot: go-snapshots goTargetRelease: go-releases goProxyRepository: https://goproxy.internal/go/ npmConfig: + authConfig: public-repo repositoryDomainName: https://mycompany.internal npmTargetSnapshot: npm-snapshots npmTargetRelease: npm-releases rawConfig: + authConfig: public-repo repositoryDomainName: https://proxy.raw.local/raw rawTargetSnapshot: raw/snapshots rawTargetRelease: raw/releases diff --git a/docs/use-cases/artifact-downloading.md b/docs/use-cases/artifact-downloading.md new file mode 100644 index 000000000..5a30adf78 --- /dev/null +++ b/docs/use-cases/artifact-downloading.md @@ -0,0 +1,1325 @@ +# Artifact Downloading Use Cases + +- [Artifact Downloading Use Cases](#artifact-downloading-use-cases) + - [Overview](#overview) + - [Supported Configurations](#supported-configurations) + - [Valid Configuration Combinations for SD/DD Artifacts](#valid-configuration-combinations-for-sddd-artifacts) + - [Valid Configuration Combinations for Environment Template Artifacts](#valid-configuration-combinations-for-environment-template-artifacts) + - [SD/DD Artifact Download](#sddd-artifact-download) + - [UC-AD-SD-1: Download SD from Artifactory with User/Password (AppDef v1 + RegDef v1)](#uc-ad-sd-1-download-sd-from-artifactory-with-userpassword-appdef-v1--regdef-v1) + - [UC-AD-SD-2: Download SD from Artifactory with Anonymous Access (AppDef v1 + RegDef v1)](#uc-ad-sd-2-download-sd-from-artifactory-with-anonymous-access-appdef-v1--regdef-v1) + - [UC-AD-SD-3: Download SD from Nexus with User/Password (AppDef v1 + RegDef v1)](#uc-ad-sd-3-download-sd-from-nexus-with-userpassword-appdef-v1--regdef-v1) + - [UC-AD-SD-4: Download SD from Nexus with Anonymous Access (AppDef v1 + RegDef v1)](#uc-ad-sd-4-download-sd-from-nexus-with-anonymous-access-appdef-v1--regdef-v1) + - [UC-AD-SD-5: Download SD from Artifactory with User/Password (AppDef v1 + RegDef v2)](#uc-ad-sd-5-download-sd-from-artifactory-with-userpassword-appdef-v1--regdef-v2) + - [UC-AD-SD-6: Download SD from Artifactory with Anonymous Access (AppDef v1 + RegDef v2)](#uc-ad-sd-6-download-sd-from-artifactory-with-anonymous-access-appdef-v1--regdef-v2) + - [UC-AD-SD-7: Download SD from Nexus with User/Password (AppDef v1 + RegDef v2)](#uc-ad-sd-7-download-sd-from-nexus-with-userpassword-appdef-v1--regdef-v2) + - [UC-AD-SD-8: Download SD from Nexus with Anonymous Access (AppDef v1 + RegDef v2)](#uc-ad-sd-8-download-sd-from-nexus-with-anonymous-access-appdef-v1--regdef-v2) + - [UC-AD-SD-9: Download SD from AWS CodeArtifact with Secret (AppDef v1 + RegDef v2)](#uc-ad-sd-9-download-sd-from-aws-codeartifact-with-secret-appdef-v1--regdef-v2) + - [UC-AD-SD-10: Download SD from GCP Artifact Registry with Service Account (AppDef v1 + RegDef v2)](#uc-ad-sd-10-download-sd-from-gcp-artifact-registry-with-service-account-appdef-v1--regdef-v2) + - [UC-AD-SD-11: Download Specific Version SD](#uc-ad-sd-11-download-specific-version-sd) + - [Environment Template Artifact Download](#environment-template-artifact-download) + - [UC-AD-ENV-9: Download Template from Artifactory with GAV notation](#uc-ad-env-9-download-template-from-artifactory-with-gav-notation) + - [UC-AD-ENV-10: Download Template from Artifactory with GAV notation and Anonymous Access](#uc-ad-env-10-download-template-from-artifactory-with-gav-notation-and-anonymous-access) + - [UC-AD-ENV-11: Download Template from Nexus with GAV notation](#uc-ad-env-11-download-template-from-nexus-with-gav-notation) + - [UC-AD-ENV-12: Download Template from Nexus with GAV notation and Anonymous Access](#uc-ad-env-12-download-template-from-nexus-with-gav-notation-and-anonymous-access) + - [UC-AD-ENV-13: Download Template with app ver notation from Artifactory (ArtDef v1)](#uc-ad-env-13-download-template-with-app-ver-notation-from-artifactory-artdef-v1) + - [UC-AD-ENV-14: Download Template with app ver notation from Artifactory and Anonymous Access (ArtDef v1)](#uc-ad-env-14-download-template-with-app-ver-notation-from-artifactory-and-anonymous-access-artdef-v1) + - [UC-AD-ENV-15: Download Template with app ver notation from Nexus (ArtDef v1)](#uc-ad-env-15-download-template-with-app-ver-notation-from-nexus-artdef-v1) + - [UC-AD-ENV-16: Download Template with app ver notation from Nexus and Anonymous Access (ArtDef v1)](#uc-ad-env-16-download-template-with-app-ver-notation-from-nexus-and-anonymous-access-artdef-v1) + - [UC-AD-ENV-17: Download Template from Artifactory with app ver notation (ArtDef v2)](#uc-ad-env-17-download-template-from-artifactory-with-app-ver-notation-artdef-v2) + - [UC-AD-ENV-18: Download Template from Artifactory with app ver notation and Anonymous Access (ArtDef v2)](#uc-ad-env-18-download-template-from-artifactory-with-app-ver-notation-and-anonymous-access-artdef-v2) + - [UC-AD-ENV-19: Download Template from Nexus with app ver notation (ArtDef v2)](#uc-ad-env-19-download-template-from-nexus-with-app-ver-notation-artdef-v2) + - [UC-AD-ENV-20: Download Template from Nexus with app ver notation and Anonymous Access (ArtDef v2)](#uc-ad-env-20-download-template-from-nexus-with-app-ver-notation-and-anonymous-access-artdef-v2) + - [UC-AD-ENV-21: Download Template from AWS CodeArtifact with app ver notation (ArtDef v2)](#uc-ad-env-21-download-template-from-aws-codeartifact-with-app-ver-notation-artdef-v2) + - [UC-AD-ENV-22: Download Template from GCP Artifact Registry with app ver notation (ArtDef v2)](#uc-ad-env-22-download-template-from-gcp-artifact-registry-with-app-ver-notation-artdef-v2) + - [UC-AD-ENV-23: Download SNAPSHOT Template Version](#uc-ad-env-23-download-snapshot-template-version) + - [UC-AD-ENV-24: Download Specific Template Version](#uc-ad-env-24-download-specific-template-version) + - [Error Handling](#error-handling) + - [UC-AD-ERR-1: Handle Missing Application Definition](#uc-ad-err-1-handle-missing-application-definition) + - [UC-AD-ERR-2: Handle Missing Registry Definition](#uc-ad-err-2-handle-missing-registry-definition) + - [UC-AD-ERR-3: Handle Authentication Failure](#uc-ad-err-3-handle-authentication-failure) + - [UC-AD-ERR-4: Handle Missing Artifact Definition](#uc-ad-err-4-handle-missing-artifact-definition) + - [Configuration Examples](#configuration-examples) + - [Registry Definition Examples](#registry-definition-examples) + - [Artifactory / Nexus (RegDef v1.0)](#artifactory--nexus-regdef-v10) + - [Artifactory (RegDef v2.0)](#artifactory-regdef-v20) + - [Nexus (RegDef v2.0)](#nexus-regdef-v20) + - [AWS CodeArtifact (RegDef v2.0)](#aws-codeartifact-regdef-v20) + - [GCP Artifact Registry (RegDef v2.0)](#gcp-artifact-registry-regdef-v20) + - [Artifact Definition Examples](#artifact-definition-examples) + - [Artifact Definition v1.0](#artifact-definition-v10) + - [Artifact Definition v2.0](#artifact-definition-v20) + - [Credentials Configuration Examples](#credentials-configuration-examples) + - [User/Password Authentication](#userpassword-authentication) + - [AWS Secret Authentication](#aws-secret-authentication) + - [GCP Service Account Authentication](#gcp-service-account-authentication) + - [Environment Inventory Examples](#environment-inventory-examples) + - [Template with GAV notation](#template-with-gav-notation) + - [Template with app ver notation](#template-with-app-ver-notation) + +## Overview + +EnvGene downloads three types of artifacts: + +1. **SD (Solution Descriptor)** - processed in `sd_processing` and `effective_set_generation` +2. **DD (Deployment Descriptor)** - processed in `effective_set_generation` +3. **Environment Template** - processed in `app_reg_def_process` + +All artifacts are Maven artifacts and can be stored in various registry types with different authentication methods. + +> [!TIP] +> Complete configuration examples (Registry Definitions, Application Definitions, Artifact Definitions) are available in the [Configuration Examples](#configuration-examples) section at the end of this document. + +## Supported Configurations + +This section provides a quick reference for valid configuration combinations across all supported registry types. + +### Valid Configuration Combinations for SD/DD Artifacts + +| Registry Type | AppDef Version | RegDef Version | Auth Method | Supported | SNAPSHOT | Notes | +|----------------------|----------------|----------------|------------------|------------|-----------|------------------------------------------------------------| +| Artifactory | v1.0 | v1.0 | user_pass | ✅ Yes | ❌ No | Legacy, maintained indefinitely | +| Artifactory | v1.0 | v1.0 | anonymous | ✅ Yes | ❌ No | Legacy, maintained indefinitely | +| Artifactory | v1.0 | v2.0 | user_pass | ✅ Yes | ❌ No | Recommended for new implementations | +| Artifactory | v1.0 | v2.0 | anonymous | ✅ Yes | ❌ No | Recommended for new implementations | +| Nexus | v1.0 | v1.0 | user_pass | ✅ Yes | ❌ No | Legacy, maintained indefinitely | +| Nexus | v1.0 | v1.0 | anonymous | ✅ Yes | ❌ No | Legacy, maintained indefinitely | +| Nexus | v1.0 | v2.0 | user_pass | ✅ Yes | ❌ No | Recommended for new implementations | +| Nexus | v1.0 | v2.0 | anonymous | ✅ Yes | ❌ No | Recommended for new implementations | +| AWS CodeArtifact | v1.0 | v1.0 | any | ❌ No | ❌ No | v1.0 RegDef cannot support AWS auth | +| AWS CodeArtifact | v1.0 | v2.0 | secret | ✅ Yes | ❌ No | Required for AWS. Only secret supported. | +| AWS CodeArtifact | v1.0 | v2.0 | anonymous | ❌ No | ❌ No | Anonymous access not supported for public cloud registries | +| AWS CodeArtifact | v1.0 | v2.0 | assume_role | ❌ No | ❌ No | Not supported | +| GCP GAR | v1.0 | v1.0 | any | ❌ No | ❌ No | v1.0 RegDef cannot support GCP auth | +| GCP GAR | v1.0 | v2.0 | service_account | ✅ Yes | ❌ No | Required for GCP. Only service_account supported. | +| GCP GAR | v1.0 | v2.0 | anonymous | ❌ No | ❌ No | Anonymous access not supported for public cloud registries | +| GCP GAR | v1.0 | v2.0 | federation | ❌ No | ❌ No | Not supported | +| Azure Artifacts | any | any | oauth2 | ❌ No | ❌ No | Azure not supported | + +### Valid Configuration Combinations for Environment Template Artifacts + +| Registry Type | Notation | ArtDef Version | Auth Method | Supported | SNAPSHOT | Notes | +|----------------------|----------|----------------|------------------|------------|-----------|------------------------------------------------------------| +| Artifactory | GAV | N/A (Legacy) | user_pass | ✅ Yes | ✅ Yes | Legacy logic, does NOT use Artifact Definitions | +| Artifactory | GAV | N/A (Legacy) | anonymous | ✅ Yes | ✅ Yes | Legacy logic, does NOT use Artifact Definitions | +| Artifactory | app:ver | v1.0 | user_pass | ✅ Yes | ✅ Yes | Legacy, maintained indefinitely | +| Artifactory | app:ver | v1.0 | anonymous | ✅ Yes | ✅ Yes | Legacy, maintained indefinitely | +| Artifactory | app:ver | v2.0 | user_pass | ✅ Yes | ✅ Yes | Recommended for new implementations | +| Artifactory | app:ver | v2.0 | anonymous | ✅ Yes | ✅ Yes | Recommended for new implementations | +| Nexus | GAV | N/A (Legacy) | user_pass | ✅ Yes | ✅ Yes | Legacy logic, does NOT use Artifact Definitions | +| Nexus | GAV | N/A (Legacy) | anonymous | ✅ Yes | ✅ Yes | Legacy logic, does NOT use Artifact Definitions | +| Nexus | app:ver | v1.0 | user_pass | ✅ Yes | ✅ Yes | Legacy, maintained indefinitely | +| Nexus | app:ver | v1.0 | anonymous | ✅ Yes | ✅ Yes | Legacy, maintained indefinitely | +| Nexus | app:ver | v2.0 | user_pass | ✅ Yes | ✅ Yes | Recommended for new implementations | +| Nexus | app:ver | v2.0 | anonymous | ✅ Yes | ✅ Yes | Recommended for new implementations | +| AWS CodeArtifact | GAV | N/A | any | ❌ No | ❌ No | GAV notation limited to Artifactory/Nexus | +| AWS CodeArtifact | app:ver | v1.0 | any | ❌ No | ❌ No | v1.0 cannot support AWS auth | +| AWS CodeArtifact | app:ver | v2.0 | secret | ✅ Yes | ✅ Yes | Required for AWS. Only secret supported. | +| AWS CodeArtifact | app:ver | v2.0 | anonymous | ❌ No | ❌ No | Anonymous access not supported for public cloud registries | +| AWS CodeArtifact | app:ver | v2.0 | assume_role | ❌ No | ❌ No | Not supported | +| GCP GAR | GAV | N/A | any | ❌ No | ❌ No | GAV notation limited to Artifactory/Nexus | +| GCP GAR | app:ver | v1.0 | any | ❌ No | ❌ No | v1.0 cannot support GCP auth | +| GCP GAR | app:ver | v2.0 | service_account | ✅ Yes | ✅ Yes | Required for GCP. Only service_account supported. | +| GCP GAR | app:ver | v2.0 | anonymous | ❌ No | ❌ No | Anonymous access not supported for public cloud registries | +| GCP GAR | app:ver | v2.0 | federation | ❌ No | ❌ No | Not supported | +| Azure Artifacts | any | any | oauth2 | ❌ No | ❌ No | Azure not supported | + +## SD/DD Artifact Download + +This group covers use cases for downloading Solution Descriptors (SD) and Deployment Descriptors (DD) from various registries. DD artifacts follow the same patterns as SD artifacts. + +### UC-AD-SD-1: Download SD from Artifactory with User/Password (AppDef v1 + RegDef v1) + +**Pre-requisites:** + +1. Application Definition v1.0 exists for SD application +2. Registry Definition v1.0 exists with Artifactory configuration + (See [Artifactory / Nexus RegDef v1.0 example](#artifactory--nexus-regdef-v10)) +3. Credentials stored in `/configuration/credentials/credentials.yml` + (See [User/Password authentication example](#userpassword-authentication)) + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `SD_SOURCE_TYPE: artifact` +3. `SD_VERSION: ` + +**Steps:** + +1. The `process_sd` job runs in the pipeline: + 1. Parses `SD_VERSION` parameter (format: `application:version`) + 2. Resolves application name to Application Definition v1.0 + 3. Extracts `registryName` from AppDef + 4. Resolves registry to Registry Definition v1.0 + 5. Extracts Maven coordinates and Artifactory URL + 6. Authenticates using username/password from credentials + 7. Downloads SD artifact from Artifactory + +**Results:** + +1. SD artifact is downloaded successfully +2. Artifact is available for SD processing in subsequent pipeline jobs + +### UC-AD-SD-2: Download SD from Artifactory with Anonymous Access (AppDef v1 + RegDef v1) + +**Pre-requisites:** + +1. Application Definition v1.0 exists for SD application +2. Registry Definition v1.0 exists with Artifactory configuration +3. Registry Definition **does NOT have** `credentialsId` configured (anonymous access) +4. Artifactory registry allows anonymous/public access + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `SD_SOURCE_TYPE: artifact` +3. `SD_VERSION: ` + +**Steps:** + +1. The `process_sd` job runs in the pipeline: + 1. Parses `SD_VERSION` parameter (format: `application:version`) + 2. Resolves application name to Application Definition v1.0 + 3. Extracts `registryName` from AppDef + 4. Resolves registry to Registry Definition v1.0 + 5. Extracts Maven coordinates and Artifactory URL + 6. Downloads SD artifact from Artifactory without authentication + +**Results:** + +1. SD artifact is downloaded successfully without authentication +2. Artifact is available for SD processing in subsequent pipeline jobs + +### UC-AD-SD-3: Download SD from Nexus with User/Password (AppDef v1 + RegDef v1) + +**Pre-requisites:** + +1. Application Definition v1.0 exists for SD application +2. Registry Definition v1.0 exists with Nexus configuration +3. Credentials stored in `/configuration/credentials/credentials.yml` + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `SD_SOURCE_TYPE: artifact` +3. `SD_VERSION: ` + +**Steps:** + +1. The `process_sd` job runs in the pipeline: + 1. Parses `SD_VERSION` parameter (format: `application:version`) + 2. Resolves application name to Application Definition v1.0 + 3. Extracts `registryName` from AppDef + 4. Resolves registry to Registry Definition v1.0 + 5. Extracts Maven coordinates and Nexus URL + 6. Authenticates using username/password from credentials + 7. Downloads SD artifact from Nexus + +**Results:** + +1. SD artifact is downloaded successfully +2. Artifact is available for SD processing in subsequent pipeline jobs + +### UC-AD-SD-4: Download SD from Nexus with Anonymous Access (AppDef v1 + RegDef v1) + +**Pre-requisites:** + +1. Application Definition v1.0 exists for SD application +2. Registry Definition v1.0 exists with Nexus configuration +3. Registry Definition **does NOT have** `credentialsId` configured (anonymous access) +4. Nexus registry allows anonymous/public access + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `SD_SOURCE_TYPE: artifact` +3. `SD_VERSION: ` + +**Steps:** + +1. The `process_sd` job runs in the pipeline: + 1. Parses `SD_VERSION` parameter (format: `application:version`) + 2. Resolves application name to Application Definition v1.0 + 3. Extracts `registryName` from AppDef + 4. Resolves registry to Registry Definition v1.0 + 5. Extracts Maven coordinates and Nexus URL + 6. Downloads SD artifact from Nexus without authentication + +**Results:** + +1. SD artifact is downloaded successfully without authentication +2. Artifact is available for SD processing in subsequent pipeline jobs + +### UC-AD-SD-5: Download SD from Artifactory with User/Password (AppDef v1 + RegDef v2) + +**Pre-requisites:** + +1. Application Definition v1.0 exists for SD application +2. Registry Definition v2.0 exists with `authConfig` section + (See [Artifactory RegDef v2.0 example](#artifactory-regdef-v20)) +3. Credentials stored in `/configuration/credentials/credentials.yml` + (See [User/Password authentication example](#userpassword-authentication)) + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `SD_SOURCE_TYPE: artifact` +3. `SD_VERSION: ` + +**Steps:** + +1. The `process_sd` job runs in the pipeline: + 1. Parses `SD_VERSION` parameter (format: `application:version`) + 2. Resolves application name to Application Definition v1.0 + 3. Resolves registry to Registry Definition v2.0 + 4. Extracts `mavenConfig.authConfig` reference + 5. Resolves to specific `authConfig` block with `authMethod: user_pass` + 6. Authenticates using username/password from credentials + 7. Downloads SD artifact from Artifactory + +**Results:** + +1. SD artifact is downloaded successfully using RegDef v2.0 enhanced authentication +2. Artifact is available for SD processing in subsequent pipeline jobs + +### UC-AD-SD-6: Download SD from Artifactory with Anonymous Access (AppDef v1 + RegDef v2) + +**Pre-requisites:** + +1. Application Definition v1.0 exists for SD application +2. Registry Definition v2.0 exists with Artifactory configuration +3. Registry Definition **does NOT have** `authConfig` section configured (anonymous access) +4. Artifactory registry allows anonymous/public access + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `SD_SOURCE_TYPE: artifact` +3. `SD_VERSION: ` + +**Steps:** + +1. The `process_sd` job runs in the pipeline: + 1. Parses `SD_VERSION` parameter (format: `application:version`) + 2. Resolves application name to Application Definition v1.0 + 3. Resolves registry to Registry Definition v2.0 + 4. Extracts `mavenConfig` (without authConfig reference) + 5. Downloads SD artifact from Artifactory without authentication + +**Results:** + +1. SD artifact is downloaded successfully without authentication using RegDef v2.0 +2. Artifact is available for SD processing in subsequent pipeline jobs + +### UC-AD-SD-7: Download SD from Nexus with User/Password (AppDef v1 + RegDef v2) + +**Pre-requisites:** + +1. Application Definition v1.0 exists for SD application +2. Registry Definition v2.0 exists with `authConfig` section for Nexus +3. Credentials stored in `/configuration/credentials/credentials.yml` + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `SD_SOURCE_TYPE: artifact` +3. `SD_VERSION: ` + +**Steps:** + +1. The `process_sd` job runs in the pipeline: + 1. Parses `SD_VERSION` parameter (format: `application:version`) + 2. Resolves application name to Application Definition v1.0 + 3. Resolves registry to Registry Definition v2.0 + 4. Extracts `mavenConfig.authConfig` reference + 5. Resolves to specific `authConfig` block with `authMethod: user_pass` + 6. Authenticates using username/password from credentials + 7. Downloads SD artifact from Nexus + +**Results:** + +1. SD artifact is downloaded successfully using RegDef v2.0 enhanced authentication +2. Artifact is available for SD processing in subsequent pipeline jobs + +### UC-AD-SD-8: Download SD from Nexus with Anonymous Access (AppDef v1 + RegDef v2) + +**Pre-requisites:** + +1. Application Definition v1.0 exists for SD application +2. Registry Definition v2.0 exists with Nexus configuration +3. Registry Definition **does NOT have** `authConfig` section configured (anonymous access) +4. Nexus registry allows anonymous/public access + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `SD_SOURCE_TYPE: artifact` +3. `SD_VERSION: ` + +**Steps:** + +1. The `process_sd` job runs in the pipeline: + 1. Parses `SD_VERSION` parameter (format: `application:version`) + 2. Resolves application name to Application Definition v1.0 + 3. Resolves registry to Registry Definition v2.0 + 4. Extracts `mavenConfig` (without authConfig reference) + 5. Downloads SD artifact from Nexus without authentication + +**Results:** + +1. SD artifact is downloaded successfully without authentication using RegDef v2.0 +2. Artifact is available for SD processing in subsequent pipeline jobs + +### UC-AD-SD-9: Download SD from AWS CodeArtifact with Secret (AppDef v1 + RegDef v2) + +**Pre-requisites:** + +1. Application Definition v1.0 exists for SD application +2. Registry Definition v2.0 exists with `provider: aws` and `authMethod: secret` + (See [AWS CodeArtifact RegDef v2.0 example](#aws-codeartifact-regdef-v20)) +3. AWS access key and secret stored in credentials + (See [AWS Secret authentication example](#aws-secret-authentication)) + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `SD_SOURCE_TYPE: artifact` +3. `SD_VERSION: ` + +**Steps:** + +1. The `process_sd` job runs in the pipeline: + 1. Parses `SD_VERSION` parameter (format: `application:version`) + 2. Resolves application name to Application Definition v1.0 + 3. Resolves registry to Registry Definition v2.0 + 4. Extracts AWS configuration (`awsRegion`, `awsDomain`) + 5. Authenticates using AWS access key/secret from credentials + 6. Gets temporary CodeArtifact token + 7. Downloads SD Maven artifact from AWS CodeArtifact + +**Results:** + +1. SD artifact is downloaded successfully from AWS CodeArtifact +2. Artifact is available for SD processing in subsequent pipeline jobs + +### UC-AD-SD-10: Download SD from GCP Artifact Registry with Service Account (AppDef v1 + RegDef v2) + +**Pre-requisites:** + +1. Application Definition v1.0 exists for SD application +2. Registry Definition v2.0 exists with `provider: gcp` and `authMethod: service_account` + (See [GCP Artifact Registry RegDef v2.0 example](#gcp-artifact-registry-regdef-v20)) +3. Service account JSON key stored as credential + (See [GCP Service Account authentication example](#gcp-service-account-authentication)) + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `SD_SOURCE_TYPE: artifact` +3. `SD_VERSION: ` + +**Steps:** + +1. The `process_sd` job runs in the pipeline: + 1. Parses `SD_VERSION` parameter (format: `application:version`) + 2. Resolves application name to Application Definition v1.0 + 3. Resolves registry to Registry Definition v2.0 + 4. Extracts GCP configuration (`gcpProject`, `gcpRegion`) + 5. Loads service account JSON key from credentials + 6. Authenticates to GCP using service account + 7. Downloads SD Maven artifact from GCP Artifact Registry + +**Results:** + +1. SD artifact is downloaded successfully from GCP Artifact Registry +2. Artifact is available for SD processing in subsequent pipeline jobs + +### UC-AD-SD-11: Download Specific Version SD + +**Pre-requisites:** + +1. `SD_VERSION` parameter specifies exact version (e.g., `solution:1.2.3`) +2. Application Definition and Registry Definition are configured + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `SD_SOURCE_TYPE: artifact` +3. `SD_VERSION: ` (specific version, NOT SNAPSHOT) + +**Steps:** + +1. The `process_sd` job runs in the pipeline: + 1. Parses specific version from `SD_VERSION` parameter + 2. Downloads exact version from configured registry + +**Results:** + +1. Specific SD artifact version is downloaded successfully +2. Artifact is available for SD processing in subsequent pipeline jobs + +## Environment Template Artifact Download + +This group covers use cases for downloading Environment Template artifacts from various registries using GAV notation or app:ver notation. + +### UC-AD-ENV-9: Download Template from Artifactory with GAV notation + +**Pre-requisites:** + +1. Environment Inventory exists and specifies template with GAV notation: + + ```yaml + templateArtifact: + registry: "sandbox" + artifact: + group_id: "org.qubership" + artifact_id: "env-template" + version: "1.2.3" + ``` + +2. `registry.yml` exists with Artifactory configuration + (See [Artifactory / Nexus RegDef v1.0 example](#artifactory--nexus-regdef-v10)) +3. Credentials configured for Artifactory + (See [User/Password authentication example](#userpassword-authentication)) + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `app_reg_def_process` job runs in the pipeline: + 1. Reads Environment Inventory from `/environments///Inventory/env_definition.yml` + 2. Parses GAV coordinates from `templateArtifact` section + 3. Resolves registry from `registry.yml` + 4. Authenticates using credentials + 5. Downloads template artifact from Artifactory using Maven coordinates + +**Results:** + +1. Template artifact is downloaded successfully +2. Template is available for Environment Instance generation + +### UC-AD-ENV-10: Download Template from Artifactory with GAV notation and Anonymous Access + +**Pre-requisites:** + +1. Environment Inventory exists and specifies template with GAV notation: + + ```yaml + templateArtifact: + registry: "sandbox" + artifact: + group_id: "org.qubership" + artifact_id: "env-template" + version: "1.2.3" + ``` + +2. `registry.yml` exists with Artifactory configuration +3. `registry.yml` **does NOT have** `credentialsId` configured (anonymous access) +4. Artifactory registry allows anonymous/public access + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `app_reg_def_process` job runs in the pipeline: + 1. Reads Environment Inventory + 2. Parses GAV coordinates from `templateArtifact` section + 3. Resolves registry from `registry.yml` + 4. Downloads template artifact from Artifactory without authentication + +**Results:** + +1. Template artifact is downloaded successfully without authentication +2. Template is available for Environment Instance generation + +### UC-AD-ENV-11: Download Template from Nexus with GAV notation + +**Pre-requisites:** + +1. Environment Inventory exists and specifies template with GAV notation (similar to UC-AD-ENV-9, but with Nexus registry) +2. `registry.yml` exists with Nexus configuration +3. Credentials configured for Nexus + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `app_reg_def_process` job runs in the pipeline: + 1. Reads Environment Inventory + 2. Parses GAV coordinates from `templateArtifact` section + 3. Resolves registry from `registry.yml` + 4. Authenticates using credentials + 5. Downloads template artifact from Nexus + +**Results:** + +1. Template artifact is downloaded successfully +2. Template is available for Environment Instance generation + +### UC-AD-ENV-12: Download Template from Nexus with GAV notation and Anonymous Access + +**Pre-requisites:** + +1. Environment Inventory exists and specifies template with GAV notation (similar to UC-AD-ENV-9) +2. `registry.yml` exists with Nexus configuration +3. `registry.yml` **does NOT have** `credentialsId` configured (anonymous access) +4. Nexus registry allows anonymous/public access + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `app_reg_def_process` job runs in the pipeline: + 1. Reads Environment Inventory + 2. Parses GAV coordinates from `templateArtifact` section + 3. Resolves registry from `registry.yml` + 4. Downloads template artifact from Nexus without authentication + +**Results:** + +1. Template artifact is downloaded successfully without authentication +2. Template is available for Environment Instance generation + +### UC-AD-ENV-13: Download Template with app ver notation from Artifactory (ArtDef v1) + +**Pre-requisites:** + +1. Environment Inventory exists and specifies template with `application:version` notation: + + ```yaml + envTemplate: + artifact: "env-template:1.2.3" + ``` + +2. Artifact Definition v1.0 exists at `/configuration/artifact_definitions/env-template.yaml` +3. Registry configuration in Artifact Definition points to Artifactory +4. Credentials configured + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `app_reg_def_process` job runs in the pipeline: + 1. Reads Environment Inventory + 2. Parses `application:version` from `envTemplate.artifact` + 3. Resolves to Artifact Definition v1.0 + 4. Extracts Maven GAV coordinates and registry information + 5. Authenticates using credentials + 6. Downloads template artifact from Artifactory + +**Results:** + +1. Template artifact is downloaded successfully using ArtDef v1.0 +2. Template is available for Environment Instance generation + +### UC-AD-ENV-14: Download Template with app ver notation from Artifactory and Anonymous Access (ArtDef v1) + +**Pre-requisites:** + +1. Environment Inventory exists and specifies template with `application:version` notation: + + ```yaml + envTemplate: + artifact: "env-template:1.2.3" + ``` + +2. Artifact Definition v1.0 exists at `/configuration/artifact_definitions/env-template.yaml` +3. Registry configuration in Artifact Definition **does NOT have** `credentialsId` (anonymous access) +4. Artifactory registry allows anonymous/public access + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `app_reg_def_process` job runs in the pipeline: + 1. Reads Environment Inventory + 2. Parses `application:version` from `envTemplate.artifact` + 3. Resolves to Artifact Definition v1.0 + 4. Extracts Maven GAV coordinates and registry information + 5. Downloads template artifact from Artifactory without authentication + +**Results:** + +1. Template artifact is downloaded successfully without authentication using ArtDef v1.0 +2. Template is available for Environment Instance generation + +### UC-AD-ENV-15: Download Template with app ver notation from Nexus (ArtDef v1) + +**Pre-requisites:** + +1. Environment Inventory exists and specifies template with `application:version` notation: + + ```yaml + envTemplate: + artifact: "env-template:1.2.3" + ``` + +2. Artifact Definition v1.0 exists at `/configuration/artifact_definitions/env-template.yaml` + (See [Artifact Definition v1.0 example](#artifact-definition-v10)) +3. Registry configuration in Artifact Definition points to Nexus + (See [Artifactory / Nexus RegDef v1.0 example](#artifactory--nexus-regdef-v10)) +4. Credentials configured + (See [User/Password authentication example](#userpassword-authentication)) + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `app_reg_def_process` job runs in the pipeline: + 1. Reads Environment Inventory + 2. Parses `application:version` from `envTemplate.artifact` + 3. Resolves to Artifact Definition v1.0 + 4. Extracts Maven GAV coordinates and registry information + 5. Authenticates using credentials + 6. Downloads template artifact from Nexus + +**Results:** + +1. Template artifact is downloaded successfully using ArtDef v1.0 +2. Template is available for Environment Instance generation + +### UC-AD-ENV-16: Download Template with app ver notation from Nexus and Anonymous Access (ArtDef v1) + +**Pre-requisites:** + +1. Environment Inventory exists and specifies template with `application:version` notation (similar to UC-AD-ENV-15) +2. Artifact Definition v1.0 exists with Nexus registry configuration +3. Registry configuration **does NOT have** `credentialsId` (anonymous access) +4. Nexus registry allows anonymous/public access + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `app_reg_def_process` job runs in the pipeline: + 1. Reads Environment Inventory + 2. Parses `application:version` from `envTemplate.artifact` + 3. Resolves to Artifact Definition v1.0 + 4. Extracts Maven GAV coordinates and registry information + 5. Downloads template artifact from Nexus without authentication + +**Results:** + +1. Template artifact is downloaded successfully without authentication using ArtDef v1.0 +2. Template is available for Environment Instance generation + +### UC-AD-ENV-17: Download Template from Artifactory with app ver notation (ArtDef v2) + +**Pre-requisites:** + +1. Environment Inventory specifies template with `application:version` format + (See [Template with app:ver notation example](#template-with-app-ver-notation)) +2. Artifact Definition v2.0 exists with `authConfig` section + (See [Artifact Definition v2.0 example](#artifact-definition-v20)) +3. `authConfig` specifies `authMethod: user_pass` +4. Credentials configured + (See [User/Password authentication example](#userpassword-authentication)) + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `app_reg_def_process` job runs in the pipeline: + 1. Reads Environment Inventory + 2. Parses `application:version` from `envTemplate.artifact` + 3. Resolves to Artifact Definition v2.0 + 4. Extracts `authConfig` reference + 5. Authenticates using username/password from credentials + 6. Downloads template artifact from Artifactory + +**Results:** + +1. Template artifact is downloaded successfully using ArtDef v2.0 with enhanced authentication +2. Template is available for Environment Instance generation + +### UC-AD-ENV-18: Download Template from Artifactory with app ver notation and Anonymous Access (ArtDef v2) + +**Pre-requisites:** + +1. Environment Inventory specifies template with `application:version` format +2. Artifact Definition v2.0 exists +3. **No** `authConfig` section in ArtDef v2.0 (anonymous access) +4. Artifactory registry allows anonymous/public access + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `app_reg_def_process` job runs in the pipeline: + 1. Reads Environment Inventory + 2. Parses `application:version` from `envTemplate.artifact` + 3. Resolves to Artifact Definition v2.0 + 4. Extracts `mavenConfig` (without authConfig reference) + 5. Downloads template artifact from Artifactory without authentication + +**Results:** + +1. Template artifact is downloaded successfully without authentication using ArtDef v2.0 +2. Template is available for Environment Instance generation + +### UC-AD-ENV-19: Download Template from Nexus with app ver notation (ArtDef v2) + +**Pre-requisites:** + +1. Environment Inventory specifies template with `application:version` format +2. Artifact Definition v2.0 exists for Nexus +3. `authConfig` specifies `authMethod: user_pass` +4. Credentials configured + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `app_reg_def_process` job runs in the pipeline: + 1. Reads Environment Inventory + 2. Parses `application:version` from `envTemplate.artifact` + 3. Resolves to Artifact Definition v2.0 + 4. Extracts `authConfig` reference + 5. Authenticates using username/password from credentials + 6. Downloads template artifact from Nexus + +**Results:** + +1. Template artifact is downloaded successfully using ArtDef v2.0 with enhanced authentication +2. Template is available for Environment Instance generation + +### UC-AD-ENV-20: Download Template from Nexus with app ver notation and Anonymous Access (ArtDef v2) + +**Pre-requisites:** + +1. Environment Inventory specifies template with `application:version` format +2. Artifact Definition v2.0 exists for Nexus +3. **No** `authConfig` section in ArtDef v2.0 (anonymous access) +4. Nexus registry allows anonymous/public access + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `app_reg_def_process` job runs in the pipeline: + 1. Reads Environment Inventory + 2. Parses `application:version` from `envTemplate.artifact` + 3. Resolves to Artifact Definition v2.0 + 4. Extracts `mavenConfig` (without authConfig reference) + 5. Downloads template artifact from Nexus without authentication + +**Results:** + +1. Template artifact is downloaded successfully without authentication using ArtDef v2.0 +2. Template is available for Environment Instance generation + +### UC-AD-ENV-21: Download Template from AWS CodeArtifact with app ver notation (ArtDef v2) + +**Pre-requisites:** + +1. Environment Inventory specifies template with `application:version` format +2. Artifact Definition v2.0 exists with `provider: aws` and `authMethod: secret` +3. AWS credentials configured (access key + secret) + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `app_reg_def_process` job runs in the pipeline: + 1. Reads Environment Inventory + 2. Parses `application:version` from `envTemplate.artifact` + 3. Resolves to Artifact Definition v2.0 + 4. Authenticates to AWS using access key/secret from credentials + 5. Gets temporary CodeArtifact token + 6. Downloads template Maven artifact from AWS CodeArtifact + +**Results:** + +1. Template artifact is downloaded successfully from AWS CodeArtifact +2. Template is available for Environment Instance generation + +### UC-AD-ENV-22: Download Template from GCP Artifact Registry with app ver notation (ArtDef v2) + +**Pre-requisites:** + +1. Environment Inventory specifies template with `application:version` format +2. Artifact Definition v2.0 exists with `provider: gcp` and `authMethod: service_account` +3. GCP service account JSON key configured + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `app_reg_def_process` job runs in the pipeline: + 1. Reads Environment Inventory + 2. Parses `application:version` from `envTemplate.artifact` + 3. Resolves to Artifact Definition v2.0 + 4. Loads service account JSON key + 5. Authenticates to GCP using service account + 6. Downloads template Maven artifact from GCP Artifact Registry + +**Results:** + +1. Template artifact is downloaded successfully from GCP Artifact Registry +2. Template is available for Environment Instance generation + +### UC-AD-ENV-23: Download SNAPSHOT Template Version + +**Pre-requisites:** + +1. Environment Inventory specifies template with SNAPSHOT version +2. Registry supports SNAPSHOT resolution + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `app_reg_def_process` job runs in the pipeline: + 1. Reads Environment Inventory + 2. Detects SNAPSHOT version in template specification + 3. Resolves SNAPSHOT to latest available version in registry + 4. Downloads latest artifact version + +**Results:** + +1. Latest SNAPSHOT template artifact is downloaded successfully +2. Template is available for Environment Instance generation + +### UC-AD-ENV-24: Download Specific Template Version + +**Pre-requisites:** + +1. Environment Inventory specifies template with exact version (not SNAPSHOT) + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `app_reg_def_process` job runs in the pipeline: + 1. Reads Environment Inventory + 2. Parses exact version from template specification + 3. Downloads specified version from registry + +**Results:** + +1. Specific template artifact version is downloaded successfully +2. Template is available for Environment Instance generation + +## Error Handling + +This group covers error scenarios that can occur during artifact download operations. + +### UC-AD-ERR-1: Handle Missing Application Definition + +**Pre-requisites:** + +1. Pipeline parameter specifies `SD_VERSION` or `DD_VERSION` with `application:version` format +2. Application Definition for specified application does NOT exist + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters that trigger artifact download (e.g., `SD_VERSION: `) + +**Steps:** + +1. Artifact download process attempts to resolve Application Definition +2. Definition file is not found at expected location +3. Pipeline job fails with clear error message indicating missing AppDef + +**Results:** + +1. Pipeline execution fails +2. Error message clearly indicates which Application Definition is missing +3. Error message includes expected file path + +### UC-AD-ERR-2: Handle Missing Registry Definition + +**Pre-requisites:** + +1. Application Definition or Artifact Definition references a registry +2. Registry Definition for specified registry does NOT exist + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters that trigger artifact download + +**Steps:** + +1. Artifact download process attempts to resolve Registry Definition +2. Definition file is not found at expected location +3. Pipeline job fails with clear error message indicating missing RegDef + +**Results:** + +1. Pipeline execution fails +2. Error message clearly indicates which Registry Definition is missing +3. Error message includes expected file path and registry name + +### UC-AD-ERR-3: Handle Authentication Failure + +**Pre-requisites:** + +1. Artifact download requires authentication +2. Authentication credentials are invalid, expired, or missing + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters that trigger artifact download from authenticated registry + +**Steps:** + +1. Artifact download process attempts authentication +2. Authentication fails (invalid credentials, network issue, or authorization problem) +3. Pipeline implements retry logic for transient failures +4. If retries exhausted, pipeline job fails with clear error message + +**Results:** + +1. For transient failures: Retry mechanism attempts to recover +2. For permanent failures: Pipeline execution fails with clear error message +3. Error message indicates authentication failure and suggests troubleshooting steps + +### UC-AD-ERR-4: Handle Missing Artifact Definition + +**Pre-requisites:** + +1. Environment Inventory specifies template with `application:version` notation +2. Artifact Definition for specified application does NOT exist + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `ENV_BUILDER: true` + +**Steps:** + +1. Template download process attempts to resolve Artifact Definition +2. Definition file is not found at expected location +3. Pipeline job fails with clear error message indicating missing ArtDef + +**Results:** + +1. Pipeline execution fails +2. Error message clearly indicates which Artifact Definition is missing +3. Error message includes expected file path + +## Configuration Examples + +This section provides complete configuration examples for definitions referenced in the use cases above. + +### Registry Definition Examples + +#### Artifactory / Nexus (RegDef v1.0) + +> [!NOTE] +> The structure is identical for both Artifactory and Nexus. Only the values differ (registry names, URLs, repository names). +> +> RegDef v1.0 does NOT have a `version` field - the absence of this field indicates v1.0. +> +> This example shows only `mavenConfig` section relevant to Maven artifact download use cases. Full Registry Definition v1.0 requires additional sections (`dockerConfig`, `helmConfig`, etc.) - see [Registry Definition schema](/docs/envgene-objects.md#registry-definition-v10) for complete structure. + +```yaml +name: "artifactory-maven" +credentialsId: "artifactory-creds" +mavenConfig: + repositoryDomainName: "artifactory.example.com" + fullRepositoryUrl: "https://artifactory.example.com/artifactory" + targetSnapshot: "libs-snapshot-local" + targetStaging: "libs-staging-local" + targetRelease: "libs-release-local" + snapshotGroup: "libs-snapshot" + releaseGroup: "libs-release" +dockerConfig: + snapshotUri: "" + stagingUri: "" + releaseUri: "" + groupUri: "" + snapshotRepoName: "" + stagingRepoName: "" + releaseRepoName: "" + groupName: "" +``` + +#### Artifactory (RegDef v2.0) + +> [!NOTE] +> For `provider: nexus` or `provider: artifactory`, the `repositoryDomainName` field contains a full URL. +> For cloud providers (`aws`, `gcp`, `azure`), it also contains a full URL. +> This is a known gap in the model naming convention and will be addressed in future versions. + +```yaml +version: "2.0" +name: "artifactory-maven" +authConfig: + artifactory-auth: + provider: "artifactory" + authMethod: "user_pass" + credentialsId: "artifactory-creds" +mavenConfig: + authConfig: "artifactory-auth" + repositoryDomainName: "https://artifactory.example.com/artifactory" + targetSnapshot: "libs-snapshot-local" + targetStaging: "libs-staging-local" + targetRelease: "libs-release-local" + snapshotGroup: "libs-snapshot" + releaseGroup: "libs-release" +``` + +#### Nexus (RegDef v2.0) + +> [!NOTE] +> For `provider: nexus` or `provider: artifactory`, the `repositoryDomainName` field contains a full URL. +> For cloud providers (`aws`, `gcp`, `azure`), it also contains a full URL. +> This is a known gap in the model naming convention and will be addressed in future versions. + +```yaml +version: "2.0" +name: "nexus-maven" +authConfig: + nexus-auth: + provider: "nexus" + authMethod: "user_pass" + credentialsId: "nexus-creds" +mavenConfig: + authConfig: "nexus-auth" + repositoryDomainName: "https://nexus.example.com/repository" + targetSnapshot: "maven-snapshots" + targetStaging: "maven-staging" + targetRelease: "maven-releases" + snapshotGroup: "maven-public-snapshots" + releaseGroup: "maven-public" +``` + +#### AWS CodeArtifact (RegDef v2.0) + +> [!NOTE] +> For `provider: nexus` or `provider: artifactory`, the `repositoryDomainName` field contains a full URL. +> For cloud providers (`aws`, `gcp`, `azure`), it also contains a full URL. +> This is a known gap in the model naming convention and will be addressed in future versions. + +```yaml +version: "2.0" +name: "aws-codeartifact" +authConfig: + aws-auth: + provider: "aws" + authMethod: "secret" + credentialsId: "aws-secret-creds" + awsRegion: "us-east-1" + awsDomain: "my-domain" +mavenConfig: + authConfig: "aws-auth" + repositoryDomainName: "https://my-domain-123456789012.d.codeartifact.us-east-1.amazonaws.com/maven/maven-central-store" +``` + +#### GCP Artifact Registry (RegDef v2.0) + +> [!NOTE] +> For `provider: nexus` or `provider: artifactory`, the `repositoryDomainName` field contains a full URL. +> For cloud providers (`aws`, `gcp`, `azure`), it also contains a full URL. +> This is a known gap in the model naming convention and will be addressed in future versions. + +```yaml +version: "2.0" +name: "gcp-artifact-registry" +authConfig: + gcp-auth: + provider: "gcp" + authMethod: "service_account" + credentialsId: "gcp-sa-key" + gcpRegion: "us-central1" +mavenConfig: + authConfig: "gcp-auth" + repositoryDomainName: "https://us-central1-maven.pkg.dev/my-project/maven-repo" +``` + +### Artifact Definition Examples + +#### Artifact Definition v1.0 + +```yaml +name: "env-template" +groupId: "com.example.templates" +artifactId: "env-template" +registry: + name: "artifactory-maven" + credentialsId: "artifactory-creds" + mavenConfig: + repositoryDomainName: "https://artifactory.example.com" + targetSnapshot: "libs-snapshot-local" + targetStaging: "libs-staging-local" + targetRelease: "libs-release-local" +``` + +#### Artifact Definition v2.0 + +```yaml +version: "2.0" +name: "env-template" +groupId: "com.example.templates" +artifactId: "env-template" +registry: + name: "artifactory-maven" + authConfig: + artifactory-auth: + provider: "artifactory" + authMethod: "user_pass" + credentialsId: "artifactory-creds" + mavenConfig: + authConfig: "artifactory-auth" + repositoryDomainName: "https://artifactory.example.com" + targetSnapshot: "libs-snapshot-local" + targetStaging: "libs-staging-local" + targetRelease: "libs-release-local" + snapshotGroup: "libs-snapshot" + releaseGroup: "libs-release" +``` + +### Credentials Configuration Examples + +#### User/Password Authentication + +```yaml +artifactory-auth: + type: usernamePassword + data: + username: "user-placeholder-123" + password: "pass-placeholder-123" +``` + +#### AWS Secret Authentication + +```yaml +aws-secret-creds: + type: secret + data: + secret: TBD +``` + +#### GCP Service Account Authentication + +```yaml +gcp-sa-key: + type: secret + data: + secret: TBD +``` + +### Environment Inventory Examples + +#### Template with GAV notation + +```yaml +templateArtifact: + registry: "artifactory-maven" + artifact: + group_id: "com.example.templates" + artifact_id: "env-template" + version: "1.2.3" +``` + +#### Template with app ver notation + +```yaml +envTemplate: + artifact: "env-template:1.2.3" +``` diff --git a/docs/use-cases/env-template-downloading.md b/docs/use-cases/env-template-downloading.md deleted file mode 100644 index c7d08cac5..000000000 --- a/docs/use-cases/env-template-downloading.md +++ /dev/null @@ -1,69 +0,0 @@ -# Environment Template Downloading Use Cases - -- [Environment Template Downloading Use Cases](#environment-template-downloading-use-cases) - - [Template Artifact Downloading Details](#template-artifact-downloading-details) - - [Use Cases](#use-cases) - -## Template Artifact Downloading Details - -The process for downloading environment template artifacts in EnvGene can be categorized along four primary axes: - -1. **Version Type:** Explicit version or `SNAPSHOT` version (latest available) - -2. **Artifact Coordinate Notation:** Either GAV (Group, Artifact, Version) or app:ver notation. - - Template artifact can be specified in [Environment Inventory](/docs/envgene-configs.md#env_definitionyml) with: - - 1. app:ver notation. To use this, [Artifact Definition](/docs/envgene-objects.md#artifact-definition) is needed. - - Example of `env_definition.yml`: - - ```yaml - - envTemplate: - artifact: string - ``` - - 2. GAV notation. To use this, [registry.yaml](/docs/envgene-configs.md#registryyml) is needed. - - Example of `env_definition.yml`: - - ```yaml - templateArtifact: - registry: string - repository: string - templateRepository: string - artifact: - group_id: string - artifact_id: string - version: string - ``` - -3. **Artifact Source Registry:** Artifact repositories supported include: - - 1. Artifactory - 2. Nexus - 3. AWS CodeArtifact - 4. Azure Artifacts - 5. GCP Artifact Registry - - The GAV form is limited to Artifactory/Nexus, while app:ver supports all. - -4. **Artifact Content Type:** Either a ZIP archive or Delivery Unit (DU) - - > [!NOTE] The core EnvGene does not have built-in support for Delivery Unit (DU) processing; this is provided as an extension point. - -## Use Cases - -The use cases below enumerate combinations of these axes that EnvGene supports: - -| Use Case | Coordinate Notation | Version Type | Registry Scope | Artifact Type | Prerequisites | Result | -|:--------:|:-------------------:|:------------:|:----------------------:|:-------------:|:-----------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------| -| UC-1-1 | GAV | Specific | Artifactory/Nexus | ZIP | `registry.yml` must include registry configuration | Retrieve a ZIP artifact by explicit GAV coordinates and fixed version from Artifactory or Nexus. | -| UC-1-2 | GAV | Specific | Artifactory/Nexus | DU | `registry.yml` must include registry configuration | Retrieve a DU artifact by explicit GAV coordinates and fixed version from Artifactory or Nexus. | -| UC-1-3 | GAV | SNAPSHOT | Artifactory/Nexus | ZIP | `registry.yml` must include registry configuration | Retrieve the latest available ZIP artifact by GAV coordinates with SNAPSHOT version from Artifactory or Nexus. | -| UC-1-4 | GAV | SNAPSHOT | Artifactory/Nexus | DU | `registry.yml` must include registry configuration | Retrieve the latest available DU artifact by GAV coordinates with SNAPSHOT version from Artifactory or Nexus. | -| UC-2-1 | app:ver | Specific | Any supported registry | ZIP | Artifact Definition must exist for the specified app:ver | Retrieve a ZIP artifact by explicit app:ver notation and fixed version from any supported registry. | -| UC-2-2 | app:ver | Specific | Any supported registry | DU | Artifact Definition must exist for the specified app:ver | Retrieve a DU artifact by explicit app:ver notation and fixed version from any supported registry. | -| UC-2-3 | app:ver | SNAPSHOT | Any supported registry | ZIP | Artifact Definition must exist for the specified app:ver | Retrieve the latest available ZIP artifact by app:ver notation with SNAPSHOT version from any registry. | -| UC-2-4 | app:ver | SNAPSHOT | Any supported registry | DU | Artifact Definition must exist for the specified app:ver | Retrieve the latest available DU artifact by app:ver notation with SNAPSHOT version from any registry. | diff --git a/schemas/artifact-definition-v2.schema.json b/schemas/artifact-definition-v2.schema.json index 2cd6e2d56..8a61e73fd 100644 --- a/schemas/artifact-definition-v2.schema.json +++ b/schemas/artifact-definition-v2.schema.json @@ -93,7 +93,7 @@ "credentialsId": { "type": "string", "title": "Credentials ID", - "description": "Pointer to the EnvGene Credential object. Depending on authType, it can be access key (username) + secret (password) for longLived. Credential with this ID must be located in /configuration/credentials/credentials.yml" + "description": "Not used in case of authMethod: anonymous. Pointer to the EnvGene Credential object. Depending on authType, it can be access key (username) + secret (password) for longLived. Credential with this ID must be located in /configuration/credentials/credentials.yml" }, "authType": { "type": "string", @@ -109,10 +109,12 @@ "enum": [ "aws", "azure", - "gcp" + "gcp", + "nexus", + "artifactory" ], "title": "Provider", - "description": "Public cloud registry type. Used in case of public cloud registries" + "description": "Registry type" }, "authMethod": { "type": "string", @@ -122,10 +124,11 @@ "federation", "service_account", "oauth2", - "user_pass" + "user_pass", + "anonymous" ], "title": "Authentication Method", - "description": "In case of non-cloud public registries, user_pass is used. In case of public cloud registries valid values depend on provider: aws: secret or assume_role, gcp: federation or service_account, azure: oauth2" + "description": "In case of non-cloud public registries, user_pass is used. In case of public cloud registries valid values depend on provider: nexus: user_pass or anonymous, artifactory: user_pass or anonymous, aws: secret, assume_role or anonymous, gcp: federation, service_account or anonymous, azure: oauth2 or anonymous" }, "awsRegion": { "type": "string", @@ -216,20 +219,134 @@ } }, "required": [ - "credentialsId" + "provider", + "authMethod" ], "allOf": [ + { + "if": { + "properties": { + "authMethod": { + "not": { + "const": "anonymous" + } + } + } + }, + "then": { + "required": ["credentialsId"] + } + }, { "if": { "properties": { "provider": { "const": "aws" + }, + "authMethod": { + "const": "assume_role" + } + }, + "required": ["provider", "authMethod"] + }, + "then": { + "required": ["awsRoleARN"] + } + }, + { + "if": { + "properties": { + "provider": { + "const": "gcp" + }, + "authMethod": { + "const": "federation" } }, - "required": ["provider"] + "required": ["provider", "authMethod"] + }, + "then": { + "required": ["gcpOIDC"] + } + }, + { + "if": { + "properties": { + "provider": { + "const": "nexus" + } + } + }, + "then": { + "properties": { + "authMethod": { + "enum": ["user_pass", "anonymous"] + } + } + } + }, + { + "if": { + "properties": { + "provider": { + "const": "artifactory" + } + } }, "then": { - "required": ["awsRegion"] + "properties": { + "authMethod": { + "enum": ["user_pass", "anonymous"] + } + } + } + }, + { + "if": { + "properties": { + "provider": { + "const": "aws" + } + } + }, + "then": { + "properties": { + "authMethod": { + "enum": ["secret", "assume_role", "anonymous"] + } + } + } + }, + { + "if": { + "properties": { + "provider": { + "const": "gcp" + } + } + }, + "then": { + "properties": { + "authMethod": { + "enum": ["federation", "service_account", "anonymous"] + } + } + } + }, + { + "if": { + "properties": { + "provider": { + "const": "azure" + } + } + }, + "then": { + "properties": { + "authMethod": { + "enum": ["oauth2", "anonymous"] + } + } } } ] @@ -243,7 +360,7 @@ "authConfig": { "type": "string", "title": "Authentication Config Reference", - "description": "Pointer to authentication config described in authConfig section. Cannot be set if anonymous access is used" + "description": "Pointer to authentication config described in authConfig section" }, "repositoryDomainName": { "type": "string", @@ -301,12 +418,8 @@ } }, "required": [ - "repositoryDomainName", - "targetSnapshot", - "targetStaging", - "targetRelease", - "snapshotGroup", - "releaseGroup" + "authConfig", + "repositoryDomainName" ] } } diff --git a/schemas/regdef-v2.schema.json b/schemas/regdef-v2.schema.json index 106ba441a..ff671b696 100644 --- a/schemas/regdef-v2.schema.json +++ b/schemas/regdef-v2.schema.json @@ -43,8 +43,7 @@ "required": [ "version", "name", - "mavenConfig", - "dockerConfig" + "mavenConfig" ], "definitions": { "AuthConfig": { @@ -66,7 +65,9 @@ "enum": [ "aws", "azure", - "gcp" + "gcp", + "nexus", + "artifactory" ] }, "authMethod": { @@ -77,7 +78,8 @@ "federation", "service_account", "oauth2", - "user_pass" + "user_pass", + "anonymous" ] }, "awsRegion": { @@ -125,6 +127,9 @@ "gcpRegSAEmail": { "type": "string" }, + "gcpRegion": { + "type": "string" + }, "azureTenantId": { "type": "string" }, @@ -139,20 +144,134 @@ } }, "required": [ - "credentialsId" + "provider", + "authMethod" ], "allOf": [ + { + "if": { + "properties": { + "authMethod": { + "not": { + "const": "anonymous" + } + } + } + }, + "then": { + "required": ["credentialsId"] + } + }, { "if": { "properties": { "provider": { "const": "aws" + }, + "authMethod": { + "const": "assume_role" } }, - "required": ["provider"] + "required": ["provider", "authMethod"] }, "then": { - "required": ["awsRegion"] + "required": ["awsRoleARN"] + } + }, + { + "if": { + "properties": { + "provider": { + "const": "gcp" + }, + "authMethod": { + "const": "federation" + } + }, + "required": ["provider", "authMethod"] + }, + "then": { + "required": ["gcpOIDC"] + } + }, + { + "if": { + "properties": { + "provider": { + "const": "nexus" + } + } + }, + "then": { + "properties": { + "authMethod": { + "enum": ["user_pass", "anonymous"] + } + } + } + }, + { + "if": { + "properties": { + "provider": { + "const": "artifactory" + } + } + }, + "then": { + "properties": { + "authMethod": { + "enum": ["user_pass", "anonymous"] + } + } + } + }, + { + "if": { + "properties": { + "provider": { + "const": "aws" + } + } + }, + "then": { + "properties": { + "authMethod": { + "enum": ["secret", "assume_role", "anonymous"] + } + } + } + }, + { + "if": { + "properties": { + "provider": { + "const": "gcp" + } + } + }, + "then": { + "properties": { + "authMethod": { + "enum": ["federation", "service_account", "anonymous"] + } + } + } + }, + { + "if": { + "properties": { + "provider": { + "const": "azure" + } + } + }, + "then": { + "properties": { + "authMethod": { + "enum": ["oauth2", "anonymous"] + } + } } } ] @@ -190,6 +309,7 @@ } }, "required": [ + "authConfig", "groupName", "groupUri", "releaseRepoName", @@ -227,12 +347,8 @@ } }, "required": [ - "releaseGroup", - "repositoryDomainName", - "snapshotGroup", - "targetRelease", - "targetSnapshot", - "targetStaging" + "authConfig", + "repositoryDomainName" ] }, "GoConfig": { @@ -256,6 +372,7 @@ } }, "required": [ + "authConfig", "repositoryDomainName", "goTargetSnapshot", "goTargetRelease", @@ -286,6 +403,7 @@ } }, "required": [ + "authConfig", "repositoryDomainName", "rawTargetSnapshot", "rawTargetRelease", @@ -311,6 +429,7 @@ } }, "required": [ + "authConfig", "repositoryDomainName", "npmTargetSnapshot", "npmTargetRelease" @@ -334,6 +453,7 @@ } }, "required": [ + "authConfig", "repositoryDomainName", "helmTargetStaging", "helmTargetRelease" @@ -363,6 +483,7 @@ } }, "required": [ + "authConfig", "repositoryDomainName", "helmStagingRepoName", "helmReleaseRepoName", From 4b0b188f3e8f90322f966e64cced308a8e35ac25 Mon Sep 17 00:00:00 2001 From: chethana-shastry-p Date: Wed, 18 Mar 2026 19:46:59 +0530 Subject: [PATCH 093/161] fix: adding GIT_STRATEGY (#1137) --- build_pipegene/scripts/appregdef_render_job.py | 10 ---------- build_pipegene/scripts/env_build_jobs.py | 8 -------- build_pipegene/scripts/gitlab_ci.py | 5 ++--- build_pipegene/scripts/process_sd_job.py | 3 +-- 4 files changed, 3 insertions(+), 23 deletions(-) diff --git a/build_pipegene/scripts/appregdef_render_job.py b/build_pipegene/scripts/appregdef_render_job.py index 41cbc1832..200a88c71 100644 --- a/build_pipegene/scripts/appregdef_render_job.py +++ b/build_pipegene/scripts/appregdef_render_job.py @@ -17,16 +17,6 @@ def prepare_appregdef_render_job(pipeline, params, full_env, environment_name, c script.append('python3 /build_env/scripts/build_env/appregdef_render.py') - script.append( - 'if [ -d "$CI_PROJECT_DIR/tmp" ]; then ' - 'DEST="$CI_PROJECT_DIR/$CI_PIPELINE_ID/tmp"; ' - 'echo "Copying tmp in $CI_PROJECT_DIR to $DEST"; ' - 'mkdir -p "$DEST"; ' - 'cp -r "$CI_PROJECT_DIR/tmp/." "$DEST/"; ' - 'else echo "tmp directory does not exist in $CI_PROJECT_DIR, skipping copy"; ' - 'fi' - ) - appregdef_render_params = { "name": f'app_reg_def_render.{full_env}', "image": '${envgen_image}', diff --git a/build_pipegene/scripts/env_build_jobs.py b/build_pipegene/scripts/env_build_jobs.py index 79f73b277..5a0274abf 100644 --- a/build_pipegene/scripts/env_build_jobs.py +++ b/build_pipegene/scripts/env_build_jobs.py @@ -8,13 +8,6 @@ def prepare_env_build_job(pipeline, is_template_test, full_env, enviroment_name, logger.info(f'prepare env_build job for {full_env}') script = [ - 'echo "PIPELINE=$CI_PIPELINE_ID JOB=$CI_JOB_NAME"', - 'if [ -d "$CI_PROJECT_DIR/$CI_PIPELINE_ID/tmp" ] && [ -d "$CI_PROJECT_DIR/tmp" ]; then', - 'echo "Copying $CI_PROJECT_DIR/$CI_PIPELINE_ID/tmp -> $CI_PROJECT_DIR/tmp";', - 'rm -rf "$CI_PROJECT_DIR/tmp/"* 2>/dev/null || echo "Warning: Failed to remove some files in tmp"', - 'cp -r "$CI_PROJECT_DIR/$CI_PIPELINE_ID/tmp/." "$CI_PROJECT_DIR/tmp/" || echo "Warning: Failed to copy tmp contents"', - 'rm -rf "$CI_PROJECT_DIR/$CI_PIPELINE_ID" || echo "Warning: Failed to delete pipeline directory"', - 'fi', 'cd /build_env; python3 /build_env/scripts/build_env/main.py' ] @@ -83,7 +76,6 @@ def prepare_git_commit_job(pipeline, full_env, enviroment_name, cluster_name, de "envgen_args": " -vv", "envgen_debug": "true", "module_config_default": "/module/templates/defaults.yaml", - "GIT_STRATEGY": "none", "COMMIT_ENV": "true", "GITLAB_RUNNER_TAG_NAME": tags, "DEPLOY_SESSION_ID": deployment_session_id diff --git a/build_pipegene/scripts/gitlab_ci.py b/build_pipegene/scripts/gitlab_ci.py index 2f70ebd4b..e33e2d1f3 100644 --- a/build_pipegene/scripts/gitlab_ci.py +++ b/build_pipegene/scripts/gitlab_ci.py @@ -202,12 +202,11 @@ def build_pipeline(params: dict) -> None: 'configuration/', 'sboms/', 'templates/', - 'tmp/', - '$CI_PIPELINE_ID/tmp' + 'tmp/' ) is_first_job = job.needs is None or len(job.needs) == 0 if not is_first_job: - job.add_variables(GIT_CHECKOUT="false") + job.add_variables(GIT_STRATEGY="empty") sorted_pipeline.write_yaml() diff --git a/build_pipegene/scripts/process_sd_job.py b/build_pipegene/scripts/process_sd_job.py index 8e9bdb3c8..a412ac732 100644 --- a/build_pipegene/scripts/process_sd_job.py +++ b/build_pipegene/scripts/process_sd_job.py @@ -35,8 +35,7 @@ def prepare_process_sd(pipeline, full_env, environment_name, cluster_name, artif "envgen_image": "$envgen_image", "envgen_args": " -vv", "envgen_debug": "true", - "GITLAB_RUNNER_TAG_NAME": tags, - "GIT_STRATEGY": "clone" + "GITLAB_RUNNER_TAG_NAME": tags } process_sd_job = job_instance(params=process_sd_set_params, vars=process_sd_set_vars) From fdfbbf11a5fe1437a03c1578fcc9c41b59766dfc Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Wed, 18 Mar 2026 14:19:30 +0000 Subject: [PATCH 094/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index c102d1f7a..666d522f2 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.8" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.8" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.8" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.9" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.9" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.9" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index bf4cc6438..7a14a05c2 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.8 +version: 1.31.9 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index d2def9b5a..71d9648ef 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.8 +version: 1.31.9 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 8865df4e6..b61167049 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.8", + "envgene_version": "1.31.9", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 7f7082c8514b4afcba49639d5309e80b54ccd489 Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:03:51 +0300 Subject: [PATCH 095/161] feat: Updated config.env in Github Instance Pipeline (#1144) --- .../instance-repo-pipeline/.github/configuration/config.env | 5 ----- 1 file changed, 5 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/configuration/config.env b/github_workflows/instance-repo-pipeline/.github/configuration/config.env index 825824b2d..aa4329933 100644 --- a/github_workflows/instance-repo-pipeline/.github/configuration/config.env +++ b/github_workflows/instance-repo-pipeline/.github/configuration/config.env @@ -1,11 +1,6 @@ CI_PROJECT_DIR=/workspace INSTANCES_DIR=/workspace/environments COMMIT_ENV=true -GIT_STRATEGY=none GITHUB_USER_EMAIL=envgene@qubership.org GITHUB_USER_NAME=envgene -PROJECT_DIR=/workspace SECRET_POSTFIX=custom_secret -envgen_args=-vvv -envgen_debug=true -module_config_default=/module/templates/defaults.yaml From 8d7d5807edcf3f18fb5e159f1e6bcad058a02b4e Mon Sep 17 00:00:00 2001 From: chethana-shastry-p Date: Fri, 20 Mar 2026 11:14:13 +0530 Subject: [PATCH 096/161] fix: moving path calc to script (#1159) --- build_pipegene/scripts/effective_set_job.py | 20 +++++++++++--------- build_pipegene/scripts/process_sd_job.py | 12 +++++------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/build_pipegene/scripts/effective_set_job.py b/build_pipegene/scripts/effective_set_job.py index 2da103d2a..0516a5d57 100644 --- a/build_pipegene/scripts/effective_set_job.py +++ b/build_pipegene/scripts/effective_set_job.py @@ -26,22 +26,24 @@ def prepare_generate_effective_set_job(pipeline, full_env_name, env_name, cluste is_local_app_def = artifact_app_defs_path and artifact_reg_defs_path and app_reg_defs_job - base_dir = getenv('CI_PROJECT_DIR') - base_env_path = f"{base_dir}/environments/{full_env_name}" - app_defs_path = f"{base_env_path}/AppDefs" - reg_defs_path = f"{base_env_path}/RegDefs" - sboms_path = get_sboms_dir(base_dir) + base_dir = getenv('CI_PROJECT_DIR') sd_path = Path(f'{base_dir}/environments/{full_env_name}/Inventory/solution-descriptor/sd.yaml') # TODO it is necessary to remove unnecessary calls, leave only script calls in such jobs! bad for gsf delivery script = [ + #Overriding sd_path to pick the correct value for CI_PROJECT_DIR + f'base_env_path="$CI_PROJECT_DIR/environments/{full_env_name}";', + 'app_defs_path="$base_env_path/AppDefs";', + 'reg_defs_path="$base_env_path/RegDefs";', + 'sboms_path="$CI_PROJECT_DIR/sboms";', + 'sd_path="$base_env_path/Inventory/solution-descriptor/sd.yaml";', # cert handling for java 'mkdir -p ${CI_PROJECT_DIR}/configuration/certs/', 'if [ -f /default_cert.pem ]; then cp /default_cert.pem "${CI_PROJECT_DIR}/configuration/certs/"; fi', 'for cert in "${CI_PROJECT_DIR}/configuration/certs/*" ; do [ -f "$cert" ] && keytool -import -trustcacerts -alias "$(basename "$cert")" -file "$cert" -keystore /etc/ssl/certs/keystore.jks -storepass changeit -noprompt; done', 'python3 /module/scripts/main.py decrypt_cred_files', - f'[ -n "$APP_REG_DEFS_JOB" ] && [ -n "$APP_DEFS_PATH" ] && mkdir -p {app_defs_path} && cp -rf {artifact_app_defs_path}/* {app_defs_path}', - f'[ -n "$APP_REG_DEFS_JOB" ] && [ -n "$REG_DEFS_PATH" ] && mkdir -p {reg_defs_path} && cp -fr {artifact_reg_defs_path}/* {reg_defs_path}', + f'[ -n "$APP_REG_DEFS_JOB" ] && [ -n "$APP_DEFS_PATH" ] && mkdir -p $app_defs_path && cp -rf {artifact_app_defs_path}/* $app_defs_path', + f'[ -n "$APP_REG_DEFS_JOB" ] && [ -n "$REG_DEFS_PATH" ] && mkdir -p $reg_defs_path && cp -fr {artifact_reg_defs_path}/* $reg_defs_path', 'python3 /module/scripts/main.py validate_creds', 'python3 /module/scripts/sboms_retention_policy.py' ] @@ -66,8 +68,8 @@ def prepare_generate_effective_set_job(pipeline, full_env_name, env_name, cluste if full_sd_exists or sd_data: cmdb_cli_cmd_call.extend([ "--registries=${CI_PROJECT_DIR}/configuration/registry.yml", - f"--sboms-path={str(sboms_path)}", - f"--sd-path={sd_path}", + "--sboms-path=$sboms_path", + "--sd-path=$sd_path", ]) logger.info(f'Prepare generate_effective_set job for {full_env_name}.') diff --git a/build_pipegene/scripts/process_sd_job.py b/build_pipegene/scripts/process_sd_job.py index a412ac732..bf43335bf 100644 --- a/build_pipegene/scripts/process_sd_job.py +++ b/build_pipegene/scripts/process_sd_job.py @@ -9,14 +9,12 @@ def prepare_process_sd(pipeline, full_env, environment_name, cluster_name, artifact_app_defs_path, artifact_reg_defs_path, tags): logger.info(f'Prepare process_sd job for {full_env}') - base_dir = getenv('CI_PROJECT_DIR') - base_env_path = f"{base_dir}/environments/{full_env}" - app_defs_path = f"{base_env_path}/AppDefs" - reg_defs_path = f"{base_env_path}/RegDefs" - script = [ - f'[ -n "$APP_REG_DEFS_JOB" ] && [ -n "$APP_DEFS_PATH" ] && mkdir -p {app_defs_path} && cp -rf {artifact_app_defs_path}/* {app_defs_path}', - f'[ -n "$APP_REG_DEFS_JOB" ] && [ -n "$REG_DEFS_PATH" ] && mkdir -p {reg_defs_path} && cp -fr {artifact_reg_defs_path}/* {reg_defs_path}', + f'base_env_path="$CI_PROJECT_DIR/environments/{full_env}";', + 'app_defs_path="$base_env_path/AppDefs";', + 'reg_defs_path="$base_env_path/RegDefs";', + f'[ -n "$APP_REG_DEFS_JOB" ] && [ -n "$APP_DEFS_PATH" ] && mkdir -p $app_defs_path && cp -rf {artifact_app_defs_path}/* $app_defs_path', + f'[ -n "$APP_REG_DEFS_JOB" ] && [ -n "$REG_DEFS_PATH" ] && mkdir -p $reg_defs_path && cp -fr {artifact_reg_defs_path}/* $reg_defs_path', 'python3 /build_env/scripts/build_env/process_sd.py', ] From 9a9cfacb5ab961ce3f5cf89b1d9af93ef37344c8 Mon Sep 17 00:00:00 2001 From: miyamuraga <198181742+miyamuraga@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:45:07 +0500 Subject: [PATCH 097/161] fix: traling slash (#1149) --- scripts/build_env/env_template/process_env_template.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build_env/env_template/process_env_template.py b/scripts/build_env/env_template/process_env_template.py index aa00ebe03..4c47339ac 100644 --- a/scripts/build_env/env_template/process_env_template.py +++ b/scripts/build_env/env_template/process_env_template.py @@ -83,9 +83,9 @@ async def resolve_artifact_new_logic(app_def: Application, app_version: str, tem raise ValueError(f"Invalid maven coordinates from deployment descriptor {dd_url}") repo_url = dd_config.get("configurations", [{}])[0].get("maven_repository") - + if not repo_url: - repo_url = f"{app_def.registry.maven_config.repository_domain_name}/{repo_name}" + repo_url = f"{app_def.registry.maven_config.repository_domain_name}{repo_name}" logger.info(f"building repo url from the repo name : {repo_url}") template_url = artifact.check_artifact(repo_url, group_id, artifact_id, version, FileExtension.ZIP, cred) validate_url(template_url, group_id, artifact_id, version) From aace15d00112ab800aae4c3237f55cac066237d9 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Fri, 20 Mar 2026 08:18:19 +0000 Subject: [PATCH 098/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 666d522f2..6edae1936 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.9" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.9" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.9" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.11" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.11" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.11" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 7a14a05c2..c06cd0795 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.9 +version: 1.31.11 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 71d9648ef..1f8f82920 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.9 +version: 1.31.11 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index b61167049..e2dda03fa 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.9", + "envgene_version": "1.31.11", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From eef6f4daf03026f4773f416970b7651812d0104f Mon Sep 17 00:00:00 2001 From: basudev91 Date: Mon, 23 Mar 2026 13:26:36 +0530 Subject: [PATCH 099/161] docs: Instance repository samples in GSF (#1052) * feat: Removed the wrong data and cleared the wrong history * feat: updated sample instance files with Readme * docs: updated readme doc * docs: update readme guide * docs: update sample instace readme doc * docs: review * docs: wip * docs: add configuration to gsf * feat: fixed cookiecutter issue --------- Co-authored-by: andyroode Co-authored-by: popoveugene --- docs/envgene-configs.md | 4 +- docs/how-to/envgene-maitanance.md | 11 +- .../templates/default/cookiecutter.json | 6 +- .../configuration/config.yml | 17 + .../configuration/credentials/credentials.yml | 8 + .../configuration/integration.yml | 22 + .../environments/.gitkeep | 0 .../example/README.md | 624 ++++++++++++++++++ .../template-project.yaml | 12 + .../example/configuration/config.yml | 17 + .../configuration/credentials/credentials.yml | 8 + .../example/configuration/integration.yml | 22 + .../cloud-passport/cluster-01-creds.yml | 38 ++ .../cluster-01/cloud-passport/cluster-01.yml | 62 ++ .../cluster-01/credentials/share-creds.yml | 24 + .../platform-env/Inventory/env_definition.yml | 115 ++++ .../solution-env/Inventory/env_definition.yml | 119 ++++ .../parameters/cloud-env-specific.yml | 6 + .../Inventory/parameters/oss-env-specific.yml | 4 + 19 files changed, 1113 insertions(+), 6 deletions(-) create mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/config.yml create mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/credentials/credentials.yml create mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/integration.yml create mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/environments/.gitkeep create mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/README.md create mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/configuration/artifact_definitions/template-project.yaml create mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/configuration/config.yml create mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/configuration/credentials/credentials.yml create mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/configuration/integration.yml create mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/cloud-passport/cluster-01-creds.yml create mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/cloud-passport/cluster-01.yml create mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/credentials/share-creds.yml create mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/platform-env/Inventory/env_definition.yml create mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/solution-env/Inventory/env_definition.yml create mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/solution-env/Inventory/parameters/cloud-env-specific.yml create mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/solution-env/Inventory/parameters/oss-env-specific.yml diff --git a/docs/envgene-configs.md b/docs/envgene-configs.md index 61f972cfc..f87d0af03 100644 --- a/docs/envgene-configs.md +++ b/docs/envgene-configs.md @@ -260,12 +260,12 @@ cp_discovery: # Mandatory # Authentication token for the discovery repository # Recommended to set via cred macro: - # envgen.creds.get().secret + # ${creds.get('').secret} token: string # Authentication token for EnvGene to access the instance repository # Required for EnvGene to commit changes to the instance repository # Recommended to set via cred macro: -# envgen.creds.get().secret +# ${creds.get('').secret} self_token: string ``` diff --git a/docs/how-to/envgene-maitanance.md b/docs/how-to/envgene-maitanance.md index a0e7b5d86..6e05faee2 100644 --- a/docs/how-to/envgene-maitanance.md +++ b/docs/how-to/envgene-maitanance.md @@ -52,7 +52,8 @@ Run the GSF package manager on your local machine with the following command: git-system-follower install \ -r \ -b \ - -t + -t \ + --extra self_token no-masked ``` **Parameter Details:** @@ -60,7 +61,10 @@ git-system-follower install \ - ``: Docker image path from Step 1 - ``: Project instance repository URL (format: `https://git.com/project.git`) - ``: Branch of project instance repository -- ``: Project instance repository token from Initial Setup Step 2 +- ``: Project instance repository token from Initial Setup Step 2 (used both for GSF Git operations and for EnvGene to commit changes) + +> [!NOTE] +> The same token is used twice: `-t` for GSF to authenticate git operations, and `--extra self_token` to configure EnvGene's repository access. **Example:** @@ -69,5 +73,6 @@ git-system-follower install \ docker.io/envgene/instance:1.2.3 \ -r https://git.qubership.org/configuration-management/env-instance.git \ -b master \ - -t token-placeholder-123 + -t token-placeholder-123 \ + --extra self_token token-placeholder-123 no-masked ``` diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index e2dda03fa..0bdb7485d 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -7,6 +7,10 @@ "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", "effective_set_generator_image": "qubership-effective-set-generator", - "gitlab_runner_tag_name": "gitlab-org-docker" + "gitlab_runner_tag_name": "gitlab-org-docker", + "self_token": "", + "_copy_without_render": [ + "example" + ] } diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/config.yml b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/config.yml new file mode 100644 index 000000000..ffe2508b1 --- /dev/null +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/config.yml @@ -0,0 +1,17 @@ +# See full reference at https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-configs.md#configyml +# +# Optional +# Parameter defines the encryption mode. Default - `true` +crypt: false +# Optional +# Defines the encryption technology. Default - `Fernet` +# crypt_backend: enum [Fernet, SOPS] +# Optional +# SBOM retention configuration +# sbom_retention: + # Optional, default value - false + # If `true`, SBOM retention will be enabled + # enabled: false + # Optional, default value - 10 + # Number of latest versions to keep per application when enabled + # keep_versions_per_app: 10 \ No newline at end of file diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/credentials/credentials.yml b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/credentials/credentials.yml new file mode 100644 index 000000000..c1cf3a02e --- /dev/null +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/credentials/credentials.yml @@ -0,0 +1,8 @@ +# See full reference at https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-objects.md#credential +# +# Self-token credential for EnvGene to commit changes to this repository +# Referenced in `/configuration/integration.yml` +self-token-cred: + type: secret + data: + secret: "{{cookiecutter.self_token}}" diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/integration.yml b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/integration.yml new file mode 100644 index 000000000..88f5f9020 --- /dev/null +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/integration.yml @@ -0,0 +1,22 @@ +# See full reference at https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-configs.md#integrationyml +# +# Optional +# Configuration for Cloud Passport discovery integration +# cp_discovery: + # Optional + # Parameters for GitLab-based discovery repository + # gitlab: + # Mandatory + # Full project path of the discovery repository + # project: string + # Mandatory + # Branch name of the discovery repository + # branch: master + # Mandatory + # Authentication token for the discovery repository + # token: string + +# Mandatory +# Authentication token for EnvGene to access this instance repository +# Required for EnvGene to commit changes +self_token: "${creds.get('self-token-cred').secret}" diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/environments/.gitkeep b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/environments/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/README.md b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/README.md new file mode 100644 index 000000000..41686cc7d --- /dev/null +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/README.md @@ -0,0 +1,624 @@ +# Getting Started with EnvGene: Your First Environment + +- [Getting Started with EnvGene: Your First Environment](#getting-started-with-envgene-your-first-environment) + - [What You'll Learn](#what-youll-learn) + - [Prerequisites](#prerequisites) + - [Scenario](#scenario) + - [What is a Template?](#what-is-a-template) + - [What is an Instance?](#what-is-an-instance) + - [How They Work Together](#how-they-work-together) + - [Part 1: Creating Your First Template](#part-1-creating-your-first-template) + - [Step 1: Explore the Sample Template](#step-1-explore-the-sample-template) + - [Step 2: Understand Template Structure](#step-2-understand-template-structure) + - [Step 3: Build Your First Template Artifact](#step-3-build-your-first-template-artifact) + - [Understanding the Template Artifact](#understanding-the-template-artifact) + - [Part 2: Creating Your First Environment Instance](#part-2-creating-your-first-environment-instance) + - [Step 4: Set Up Configuration Folder](#step-4-set-up-configuration-folder) + - [Step 5: Set Up environments Folder](#step-5-set-up-environments-folder) + - [Step 6: Review Platform Environment Configuration](#step-6-review-platform-environment-configuration) + - [Step 7: Review Shared Credentials](#step-7-review-shared-credentials) + - [Step 8: Build the Platform Environment](#step-8-build-the-platform-environment) + - [Step 9: Verify the Results](#step-9-verify-the-results) + - [Part 3: Building a Business Environment](#part-3-building-a-business-environment) + - [Step 10: Review Solution Environment Configuration](#step-10-review-solution-environment-configuration) + - [Step 11: Review Environment-Specific Parameters](#step-11-review-environment-specific-parameters) + - [Step 12: Build the Solution Environment](#step-12-build-the-solution-environment) + - [The Complete Flow](#the-complete-flow) + - [The Flow](#the-flow) + - [Summary](#summary) + - [Reference Documentation](#reference-documentation) + +> [!NOTE] +> **You are in an Instance Repository.** This guide covers both template creation (Part 1, done in a Template Repository) and instance configuration (Parts 2-3, done here). + +**Repository Context:** This readme can be used in both Template and Instance repositories. The instructions will guide you based on where you are. + +- **Part 1** - Creating a Template (done in a Template Repository) +- **Part 2–3** - Creating an Instance (done in an Instance Repository) + +By the end, you will have a working environment and understand how the pipeline uses your configuration. + +## What You'll Learn + +- The relationship between Templates and Instances +- How to create and publish a template artifact +- How to configure an environment +- How the pipeline processes your configuration + +## Prerequisites + +- Access to GitLab with CI/CD enabled +- Basic understanding of YAML syntax +- Git command-line knowledge + +> [!NOTE] +> **Choose your path based on where you are:** +> +> - **In a Template Repository?** Follow Part 1 to create and publish templates (requires write access to registry) +> - **In an Instance Repository?** Skip to Part 2 to configure environments (requires read access to registry) + +## Scenario + +You are setting up your first EnvGene environment in `cluster-01`. You will create two environments: + +- **platform-env** - Infrastructure services (ArangoDB, Consul, OpenSearch) +- **solution-env** - Business applications (BSS, OSS, Core) + +By the end, you will have generated the Effective Set and understand how template + instance work together. + +### What is a Template? + +A **Template** is a reusable blueprint that defines solution structure (what namespaces exist) and default configuration parameters. + +### What is an Instance? + +An **Instance** is a specific environment created from a template. It adds environment-specific values and overrides default parameters to produce deployable configuration. + +### How They Work Together + +```mermaid +flowchart LR + A[Template Repository] -->|Builds| B[Template Artifact] + B -->|Referenced by| C[Instance Repository] + C -->|Generates| D[Effective Set] + D -->|Deployed to| E[Kubernetes Cluster] +``` + +1. Template repository builds a versioned artifact +2. Instance repository references that artifact +3. Pipeline merges template + overrides = effective set +4. Effective set is used to deploy to cluster + +## Part 1: Creating Your First Template + +> [!NOTE] +> Part 1 requires access to a Template Repository (a separate repository) with CI/CD enabled. If you already have a published template artifact, skip to Part 2. + +Part 1 is done in a **template repository**, not in this instance repository. Do Part 1 only if you need to create and publish a template artifact. + +### Step 1: Explore the Sample Template + +Navigate to the example folder: + +```bash +cd example/templates/ +ls -la +``` + +You should see: + +```text +templates/ +├── env_templates/ +│ ├── Namespaces/ +│ │ ├── platform/ +│ │ │ ├── arangodb.yml.j2 +│ │ │ ├── consul.yml.j2 +│ │ │ └── opensearch.yml.j2 +│ │ ├── solution/ +│ │ │ ├── bss.yml.j2 +│ │ │ ├── oss.yml.j2 +│ │ │ └── core.yml.j2 +│ │ ├── tenant.yml.j2 +│ │ └── cloud.yml.j2 +│ │ +│ ├── platform.yml +│ └── solution.yml +│ +├── appdefs/ +├── regdefs/ +├── parameters/ +└── resource_profile/ +``` + +### Step 2: Understand Template Structure + +Let's look at a key file, a Template Descriptor - `env_templates/platform.yml`: + +```yaml +tenant: "{{ templates_dir }}/env_templates/Namespaces/tenant.yml.j2" +cloud: "{{ templates_dir }}/env_templates/Namespaces/cloud.yml.j2" +namespaces: + - template_path: "{{ templates_dir }}/env_templates/Namespaces/platform/arangodb.yml.j2" + - template_path: "{{ templates_dir }}/env_templates/Namespaces/platform/consul.yml.j2" + - template_path: "{{ templates_dir }}/env_templates/Namespaces/platform/dbaas.yml.j2" + - template_path: "{{ templates_dir }}/env_templates/Namespaces/platform/jaeger.yml.j2" + - template_path: "{{ templates_dir }}/env_templates/Namespaces/platform/kafka.yml.j2" + - template_path: "{{ templates_dir }}/env_templates/Namespaces/platform/opensearch.yml.j2" +``` + +This file defines the structure of an environment by linking all required component templates. It determines what gets deployed, ensures consistency across environments, and serves as the blueprint referenced by the Environment Inventory. + +Now look at Namespace template `env_templates/Namespaces/platform/consul.yml.j2`: + +```yaml +name: "consul" +credentialsId: "" +isServerSideMerge: false +labels: [] +cleanInstallApprovalRequired: false +mergeDeployParametersAndE2EParameters: true +deployParameters: {} +e2eParameters: {} +technicalConfigurationParameters: {} +deployParameterSets: + - "consul" +e2eParameterSets: [] +technicalConfigurationParameterSets: [] +``` + +- `.j2` extension means it's a Jinja2 template +- It will be rendered with variables you provide (this example omits Jinja syntax for simplicity) + +### Step 3: Build Your First Template Artifact + +Copy the sample template to your templates directory: + +```bash +cp -r example/templates/* templates/ +git add templates/ +git commit -m "feat: Add sample template" +git push +``` + +**Watch the pipeline:** + +1. Go to your GitLab project → CI/CD → Pipelines +2. You should see a pipeline running with these jobs: + - `dp_build` - Building and validating templates + - `report_artifacts` - Recording what was built + - `semantic_release` - Publishing the artifact + +```mermaid +flowchart LR + A[dp_build] --> B[report_artifacts] --> C[semantic_release] +``` + +**Promote / Release Pipeline:** + +Once the `semantic_release` job completes successfully, it automatically triggers a new pipeline responsible for promoting the published template artifact to the release stage. + +```mermaid +flowchart LR + A[prepromote] --> B[promote] + B --> C[update_dependent_versions] + C --> D[get_release_notes] + D --> E[update_release_notes] + E --> F[Release Completed] +``` + +**Jobs:** + +- `prepromote` → Validates promotion conditions. +- `promote` → Promotes to release branch. +- `update_dependent_versions` → Updates dependent modules. +- `get_release_notes` → Extracts commit-based release notes. +- `update_release_notes` → Publishes release documentation. + +Wait for the pipeline to complete (usually 2-5 minutes). + +### Understanding the Template Artifact + +Your pipeline created a template artifact with a version like: + +```text +artifact: template-project:feature-template-sample-20260303.022442-18 +``` + +> [!NOTE] +> Your actual version will differ - it's based on your commit timestamp and build number. + +This artifact now contains all your template files in a packaged format, ready to be used by instances. + +## Part 2: Creating Your First Environment Instance + +From here on, all steps are done **in this repository**. Copy the sample configuration and environments from the `example/` folder in this repository. + +### Step 4: Set Up Configuration Folder + +Copy the configuration folder from `example/`: + +```bash +cp -r example/configuration configuration/ +``` + +The folder structure should look like this: + +``` text +configuration/ +├── artifact_definitions/ +│ └── .yaml +├── credentials/ +│ └── credentials.yml +└── config.yml +``` + +- `.yaml` - Describes where the Environment Template artifact is stored in the registry. It is used to resolve the `application:version` format into the registry and Maven artifact parameters required to download it. + +> [!IMPORTANT] +> +> - All fields inside this file must be replaced with actual project-specific values. + +- `credentials.yml` - Stores credential definitions. +- `config.yml` - Defines EnvGene configuration. + +### Step 5: Set Up environments Folder + +Copy the sample environment: + +```bash +cp -r example/environments/cluster-01 environments/ +``` + +### Step 6: Review Platform Environment Configuration + +The `env_definition.yml` defines which template to use, which artifact version, and what parameters to override. Review its structure: + +Open `environments/cluster-01/platform-env/Inventory/env_definition.yml`: + +```yaml +inventory: {} +envTemplate: + name: "platform" + artifact: "template-project:feature-template-sample-20260303.022442-18" + additionalTemplateVariables: + use_env_prefix: true + sharedTemplateVariables: [] + envSpecificParamsets: {} + envSpecificE2EParamsets: {} + envSpecificTechnicalParamsets: {} + envSpecificResourceProfiles: {} + sharedMasterCredentialFiles: + - "share-creds" +``` + +Comments from the real file are not shown here for simplicity. + +**What each field means:** + +- `inventory`: Defines environment metadata and configuration (tenant, cloud, credentials, etc.). Acts as the recipe for creating the environment. +- `envTemplate.name`: Name of the Environment Template to use. Must match the Template Descriptor name inside the referenced artifact. +- `envTemplate.artifact`: Template artifact in application:version format used to render the environment. +- `additionalTemplateVariables`: Extra variables passed to templates during rendering. +- `sharedTemplateVariables`: Common variable files merged into template variables. +- `envSpecificParamsets`: Environment-specific deployment parameter overrides. +- `envSpecificE2EParamsets`: Environment-specific CI/CD or pipeline parameters. +- `envSpecificTechnicalParamsets`: Runtime/technical parameter overrides. +- `envSpecificResourceProfiles`: Resource (CPU/memory) overrides for this environment. + +> [!IMPORTANT] +> The `envTemplate.artifact` value must match what your template pipeline produced. + +### Step 7: Review Shared Credentials + +Shared credentials are referenced in `env_definition.yml` (via `sharedMasterCredentialFiles`) and used across all environments in this cluster. Let's look at the format: + +Open `environments/cluster-01/credentials/share-creds.yml`: + +```yaml +registry-cred: + type: "usernamePassword" + data: + username: "user-placeholder-123" + password: "pass-placeholder-123" +sso-idp-admin-login: + type: "secret" + data: + secret: "token-placeholder-123" +streaming: + type: "usernamePassword" + data: + username: "user-placeholder-123" + password: "pass-placeholder-123" +postgres-dba-cred: + type: "usernamePassword" + data: + username: "user-placeholder-123" + password: "pass-placeholder-123" +kafka-monitoring-creds: + type: "usernamePassword" + data: + username: "user-placeholder-123" + password: "pass-placeholder-123" +``` + +**Why this matters:** + +- Credentials are shared across environments in the same cluster +- In production scenarios, these would be encrypted +- For learning, we're using plain text + +### Step 8: Build the Platform Environment + +Commit and push your changes: + +```bash +git add environments/ +git commit -m "Add platform environment configuration" +git push +``` + +Trigger the instance pipeline manually: + +1. Go to GitLab → CI/CD → Pipelines → Run Pipeline +2. To generate the Effective Set for the platform environment, EnvGene needs to know which applications (and versions) are deployed. You provide this via the Solution Descriptor (SD). For platform-env, we pass SD inline via pipeline variables (`SD_SOURCE_TYPE: json` and `SD_DATA`). Later (Step 12) you'll do the same for solution-env with BSS/OSS apps. + + Set these variables: + + ```yaml + ENV_NAMES: cluster-01/platform-env + ENV_BUILDER: true + GENERATE_EFFECTIVE_SET: true + SD_SOURCE_TYPE: json + SD_DATA: + ``` + + ```text + '{"version":2.1,"type":"solutionDeploy","deployMode":"composite","applications":[{"version":"","deployPostfix":"arangodb"},{"version":"","deployPostfix":"consul"},{"version":"","deployPostfix":"opensearch"}]}' + ``` + + Replace each `` with your real `application:version` (e.g. `consul:0.11.8`). + + **What's the Solution Descriptor?** + + - Lists specific application versions to deploy + - `deployPostfix` maps to namespace (bss, oss, core) + +3. Click "Run Pipeline" + +**What's happening:** + +```mermaid +flowchart LR + A[generate_pipeline] --> B[run_generated_pipeline] + B --> C[app_reg_def_render] + C --> D[process_sd] + D --> E[env_builder] + E --> F[generate_effective_set] + F --> G[git_commit] +``` + +- `generate_pipeline` - Generates EnvGene pipeline config +- `app_reg_def_render` - Downloads EnvGene Environment template, renders application and registry definitions +- `process_sd` - Processes Solution Descriptor (SD) from pipeline variables +- `env_builder` - Generates Environment Instance - renders template and merges environment-specific overrides +- `generate_effective_set` - Generates Effective Set - final output +- `git_commit` - Commits results back to repository + +### Step 9: Verify the Results + +After the pipeline completes, pull the changes: + +```bash +git pull +``` + +Look at the generated structure: + +```bash +ls -la environments/cluster-01/platform-env/ +``` + +You should see: + +```text +environments/ +└── cluster-01/ + ├── platform-env/ + │ ├── AppDefs/ + │ ├── RegDefs/ + │ ├── Credentials/ + │ │ └── credentials.yml + │ ├── Inventory/ + │ │ ├── env_definition.yml + │ │ └── solution-descriptor/ + │ │ └── sd.yml + │ ├── Namespaces/ + │ ├── Profiles/ + │ ├── effective-set/ + │ │ ├── deployment/ + │ │ ├── topology/ + │ │ ├── pipeline/ + │ │ ├── runtime/ + │ │ └── cleanup/ + │ ├── cloud.yml + │ └── tenant.yml + │ + └── credentials/ + └── share-creds.yml +``` + +The `effective-set/` folder contains five contexts (deployment, topology, pipeline, runtime, cleanup). For example, open `effective-set/deployment///values/deployment-parameters.yaml` to see the final merged parameters that Helm will use. For details on each context, see [Tutorial: Understanding the Effective Set](https://github.com/Netcracker/qubership-envgene/blob/main/docs/tutorials/effective-set.md). + +## Part 3: Building a Business Environment + +Now let's add a second environment with business applications. + +### Step 10: Review Solution Environment Configuration + +The solution-env uses the "solution" template and references parameter sets for BSS and OSS namespaces. Review its `env_definition.yml`: + +Open `environments/cluster-01/solution-env/Inventory/env_definition.yml`: + +```yaml +inventory: {} +envTemplate: + name: "solution" + artifact: "template-project:feature-template-sample-20260303.022442-18" + additionalTemplateVariables: + use_env_prefix: true + sharedTemplateVariables: [] + envSpecificParamsets: + bss: + - "env-specific-bss" + oss: + - "oss-env-specific" + envSpecificE2EParamsets: {} + envSpecificTechnicalParamsets: {} + envSpecificResourceProfiles: {} + sharedMasterCredentialFiles: [] +``` + +Like before, comments are omitted for clarity. + +**What's different from the previous environment:** + +- `envTemplate.name: solution` - Uses the solution template +- `envSpecificParamsets` - Overrides parameters per namespace (e.g. `bss`, `oss`) +- `sharedMasterCredentialFiles: []` - Solution-env in this example does not use shared credentials + +### Step 11: Review Environment-Specific Parameters + +The solution-env `env_definition.yml` references parameter sets `env-specific-bss` and `oss-env-specific`. The `oss-env-specific.yml` file overrides parameters just for the OSS namespace in this environment. + +Open one of these files `environments/cluster-01/solution-env/Inventory/parameters/oss-env-specific.yml`: + +```yaml +name: oss-env-specific +parameters: + LISTENER_NODE_IP: 10.10.10.10 +applications: [] +``` + +**Why this is powerful:** + +- Template defines defaults +- You override only what's different +- Same template, different values per environment + +### Step 12: Build the Solution Environment + +Now that you understand how solution-env uses template + overrides, let's generate its Effective Set. + +In this step, we build the **solution-env** environment. Set these variables: + +```yaml + ENV_NAMES: cluster-01/solution-env + ENV_BUILDER: true + GENERATE_EFFECTIVE_SET: true + SD_SOURCE_TYPE: json + SD_DATA: +``` + +```text +'{"version":2.1,"type":"solutionDeploy","deployMode":"composite","applications":[{"version":"","deployPostfix":"core"},{"version":"","deployPostfix":"bss"},{"version":"","deployPostfix":"oss"}]}' +``` + +Replace each `` with your real `application:version` (e.g. `core:release-9.16.0`). + +Click "Run Pipeline" + +**What You Now Have:** + +After running pipelines for both environments (Steps 8 and 12), your repository structure looks like this: + +```text +environments/ +└── cluster-01/ + ├── platform-env/ + │ ├── AppDefs/ + │ ├── RegDefs/ + │ ├── Credentials/ + │ ├── Inventory/ + │ │ ├── env_definition.yml + │ │ └── solution-descriptor/ + │ │ └── sd.yml + │ ├── Namespaces/ + │ ├── Profiles/ + │ ├── effective-set/ + │ │ ├── deployment/ + │ │ ├── topology/ + │ │ ├── pipeline/ + │ │ ├── runtime/ + │ │ └── cleanup/ + │ ├── cloud.yml + │ └── tenant.yml + │ + ├── solution-env/ + │ ├── AppDefs/ + │ ├── RegDefs/ + │ ├── Credentials/ + │ ├── Inventory/ + │ │ ├── env_definition.yml + │ │ ├── parameters/ + │ │ └── solution-descriptor/ + │ │ └── sd.yml + │ ├── Namespaces/ + │ ├── Profiles/ + │ ├── effective-set/ + │ │ ├── deployment/ + │ │ ├── topology/ + │ │ ├── pipeline/ + │ │ ├── runtime/ + │ │ └── cleanup/ + │ ├── cloud.yml + │ └── tenant.yml + │ + └── credentials/ + └── share-creds.yml +``` + +For the meaning of Effective Set contexts (deployment, topology, pipeline, runtime, cleanup), see [Tutorial: Understanding the Effective Set](https://github.com/Netcracker/qubership-envgene/blob/main/docs/tutorials/effective-set.md). + +## The Complete Flow + +### The Flow + +```mermaid +flowchart TB + A[Template Repository] -->|1. Builds| B[Template Artifact
sample-template-SNAPSHOT] + B -->|2. Referenced by| C[Instance Repository] + C -->|3. Renders with| D[env_definition.yml] + C -->|4. Merges with| E[parameters/] + C -->|5. Applies| F[Solution Descriptor] + D --> G[env_builder] + E --> G + F --> G + G -->|6. Produces| H[Effective Set] + H -->|7. Ready for| I[Deployment] +``` + +**Congratulations!** You've successfully created your first EnvGene environment from scratch. You now understand the core workflow and are ready to build more complex environments. + +## Summary + +By following this tutorial, you: + +- Created a template artifact in a Template Repository (Part 1) or used an existing one +- Configured an Instance Repository with two environments: `platform-env` and `solution-env` (Part 2-3) +- Passed a Solution Descriptor via pipeline variables to map applications to namespaces +- Generated the Effective Set for both environments and reviewed the file structure +- Understand the flow: Template Artifact → env_definition.yml → parameters → SD → Effective Set → Deployment + +You are now ready to add more environments, customize parameters, and explore advanced features like Resource Profile Overrides ([Tutorial: Managing Resource Profiles](https://github.com/Netcracker/qubership-envgene/blob/main/docs/tutorials/resource-profiles.md)) and Effective Set contexts ([Tutorial: Understanding the Effective Set](https://github.com/Netcracker/qubership-envgene/blob/main/docs/tutorials/effective-set.md)). + +## Reference Documentation + +For detailed documentation on EnvGene objects and configuration: + +- [EnvGene Objects Reference](https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-objects.md) +- [Environment Configuration](https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-configs.md) +- [Pipeline Parameters](https://github.com/Netcracker/qubership-envgene/blob/main/docs/instance-pipeline-parameters.md) +- [How-to Guides](https://github.com/Netcracker/qubership-envgene/tree/main/docs/how-to) +- [All Documentation](https://github.com/Netcracker/qubership-envgene/tree/main/docs) +- [EnvGene Main readme](https://github.com/Netcracker/qubership-envgene/blob/main/README.md) diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/configuration/artifact_definitions/template-project.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/configuration/artifact_definitions/template-project.yaml new file mode 100644 index 000000000..3b270ccf3 --- /dev/null +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/configuration/artifact_definitions/template-project.yaml @@ -0,0 +1,12 @@ +# shoud be updated -> add here in README.md +# CMDB case +name: "template-project" +groupId: "mvn.group" +artifactId: "template-project" +registry: + name: "ghcr.io" + mavenConfig: + repositoryDomainName: "https://artifactory.qubership.org/" + targetSnapshot: "maven-target-snapshot" + targetStaging: "maven-target-staging" + targetRelease: "maven-target-release" \ No newline at end of file diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/configuration/config.yml b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/configuration/config.yml new file mode 100644 index 000000000..ffe2508b1 --- /dev/null +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/configuration/config.yml @@ -0,0 +1,17 @@ +# See full reference at https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-configs.md#configyml +# +# Optional +# Parameter defines the encryption mode. Default - `true` +crypt: false +# Optional +# Defines the encryption technology. Default - `Fernet` +# crypt_backend: enum [Fernet, SOPS] +# Optional +# SBOM retention configuration +# sbom_retention: + # Optional, default value - false + # If `true`, SBOM retention will be enabled + # enabled: false + # Optional, default value - 10 + # Number of latest versions to keep per application when enabled + # keep_versions_per_app: 10 \ No newline at end of file diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/configuration/credentials/credentials.yml b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/configuration/credentials/credentials.yml new file mode 100644 index 000000000..0a0bcfb0e --- /dev/null +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/configuration/credentials/credentials.yml @@ -0,0 +1,8 @@ +# See full reference at https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-objects.md#credential +# +# Self-token credential for EnvGene to commit changes to this repository +# Referenced in `/configuration/integration.yml` +self-token-cred: + type: secret + data: + secret: "token-placeholder-123" diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/configuration/integration.yml b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/configuration/integration.yml new file mode 100644 index 000000000..88f5f9020 --- /dev/null +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/configuration/integration.yml @@ -0,0 +1,22 @@ +# See full reference at https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-configs.md#integrationyml +# +# Optional +# Configuration for Cloud Passport discovery integration +# cp_discovery: + # Optional + # Parameters for GitLab-based discovery repository + # gitlab: + # Mandatory + # Full project path of the discovery repository + # project: string + # Mandatory + # Branch name of the discovery repository + # branch: master + # Mandatory + # Authentication token for the discovery repository + # token: string + +# Mandatory +# Authentication token for EnvGene to access this instance repository +# Required for EnvGene to commit changes +self_token: "${creds.get('self-token-cred').secret}" diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/cloud-passport/cluster-01-creds.yml b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/cloud-passport/cluster-01-creds.yml new file mode 100644 index 000000000..175bb1c18 --- /dev/null +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/cloud-passport/cluster-01-creds.yml @@ -0,0 +1,38 @@ +cloud-deploy-token: + type: "secret" + data: + secret: "token-placeholder-123" +consul-admin-cred: + type: "secret" + data: + secret: "token-placeholder-123" +cse-graylog-cred: + type: "usernamePassword" + data: + username: "user-placeholder-123" + password: "pass-placeholder-123" +dbaas-cluster-dba-cred: + type: "usernamePassword" + data: + username: "user-placeholder-123" + password: "pass-placeholder-123" +maas-cred: + type: "usernamePassword" + data: + username: "user-placeholder-123" + password: "pass-placeholder-123" +minio-storage-cred: + type: "usernamePassword" + data: + username: "user-placeholder-123" + password: "pass-placeholder-123" +oss-streaming-cred: + type: "usernamePassword" + data: + username: "user-placeholder-123" + password: "pass-placeholder-123" +ssm-cmdb-cred: + type: "usernamePassword" + data: + username: "user-placeholder-123" + password: "pass-placeholder-123" \ No newline at end of file diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/cloud-passport/cluster-01.yml b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/cloud-passport/cluster-01.yml new file mode 100644 index 000000000..afd095fda --- /dev/null +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/cloud-passport/cluster-01.yml @@ -0,0 +1,62 @@ +version: 1.5 +cloud: + CLOUD_API_HOST: api.example-cloud.managed.qubership.org + CLOUD_API_PORT: "6443" + CLOUD_DEPLOY_TOKEN: cloud-deploy-token + CLOUD_PUBLIC_HOST: example-cloud.managed.qubership.org + CLOUD_PRIVATE_HOST: example-cloud.managed.qubership.org + CLOUD_DASHBOARD_URL: https://dashboard.example-cloud.managed.qubership.org + CLOUD_PROTOCOL: https + PRODUCTION_MODE: false + GRAYLOG_UI_URL: https://graylog-logging-example-cloud.managed.qubership.org + TRACING_UI_URL: https://jaeger.example-cloud.managed.qubership.org + GRAFANA_UI_URL: https://grafana-platform-monitoring.example-cloud.managed.qubership.org + CMDB_URL: https://cloud-deployer.qubership.org +dbaas: + API_DBAAS_ADDRESS: http://dbaas-aggregator.dbaas:8080 + DBAAS_AGGREGATOR_ADDRESS: https://aggregator-dbaas.example-cloud.managed.qubership.org + DBAAS_CLUSTER_DBA_CREDENTIALS_USERNAME: ${creds.get("dbaas-cluster-dba-cred").username} + DBAAS_CLUSTER_DBA_CREDENTIALS_PASSWORD: ${creds.get("dbaas-cluster-dba-cred").password} +maas: + MAAS_INTERNAL_ADDRESS: http://maas-service.maas:8080 + MAAS_SERVICE_ADDRESS: http://maas-service.maas:8080 + MAAS_CREDENTIALS_USERNAME: ${creds.get("maas-cred").username} + MAAS_CREDENTIALS_PASSWORD: ${creds.get("maas-cred").password} +consul: + CONSUL_URL: http://consul-server.consul:8500 + CONSUL_ENABLED: true + CONSUL_PUBLIC_URL: https://consul.example-cloud.managed.qubership.org + CONSUL_ADMIN_TOKEN: ${creds.get("consul-admin-cred").secret} +zookeeper: + ZOOKEEPER_URL: ${ZOOKEEPER_ADDRESS} + ZOOKEEPER_ADDRESS: zookeeper.zookeeper:2181 +storage: + STORAGE_SERVER_URL: https://s3.qubership.org + STORAGE_PROVIDER: s3 + STORAGE_REGION: "us-west-2" + STORAGE_USERNAME: ${creds.get("minio-storage-cred").username} + STORAGE_PASSWORD: ${creds.get("minio-storage-cred").password} + CDN_STORAGE_SERVER_URL: ${STORAGE_SERVER_URL} + CDN_STORAGE_PROVIDER: ${STORAGE_PROVIDER} + CDN_STORAGE_REGION: ${STORAGE_REGION} + CDN_STORAGE_USERNAME: ${STORAGE_USERNAME} + CDN_STORAGE_PASSWORD: ${STORAGE_PASSWORD} + DOC_STORAGE_SERVER_URL: ${STORAGE_SERVER_URL} + DOC_STORAGE_PROVIDER: ${STORAGE_PROVIDER} + DOC_STORAGE_REGION: ${STORAGE_REGION} + DOC_STORAGE_USERNAME: ${STORAGE_USERNAME} + DOC_STORAGE_PASSWORD: ${STORAGE_PASSWORD} + STORAGE_RWO_CLASS: rwo-sc + STORAGE_RWX_CLASS: rwx-sc +core: + DEFAULT_TENANT_NAME: default + DEFAULT_TENANT_ADMIN_LOGIN: user-placeholder-123 + DEFAULT_TENANT_ADMIN_PASSWORD: pass-placeholder-123 + MAVEN_REPO_URL: https://artifactory.qubership.org/ + MAVEN_REPO_NAME: global.mvn.group + MAVEN_REPO_STAGING_NAME: ${MAVEN_REPO_NAME} + MAVEN_REPO_DEV_NAME: ${MAVEN_REPO_NAME} +global: + MONITORING_ENABLED: "true" + TRACING_ENABLED: "true" + TRACING_HOST: diagnostic-agent \ No newline at end of file diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/credentials/share-creds.yml b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/credentials/share-creds.yml new file mode 100644 index 000000000..73381a3d3 --- /dev/null +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/credentials/share-creds.yml @@ -0,0 +1,24 @@ +registry-cred: + type: "usernamePassword" + data: + username: "user-placeholder-123" + password: "pass-placeholder-123" +sso-idp-admin-login: + type: "secret" + data: + secret: "token-placeholder-123" +streaming: + type: "usernamePassword" + data: + username: "user-placeholder-123" + password: "pass-placeholder-123" +postgres-dba-cred: + type: "usernamePassword" + data: + username: "user-placeholder-123" + password: "pass-placeholder-123" +kafka-monitoring-creds: + type: "usernamePassword" + data: + username: "user-placeholder-123" + password: "pass-placeholder-123" \ No newline at end of file diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/platform-env/Inventory/env_definition.yml b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/platform-env/Inventory/env_definition.yml new file mode 100644 index 000000000..de8f5ab14 --- /dev/null +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/platform-env/Inventory/env_definition.yml @@ -0,0 +1,115 @@ +# See full reference at https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-configs.md#integrationyml +# +# Mandatory +inventory: {} + # Optional + # Name of the Environment + # environmentName: string + # Optional + # Name of the Tenant for the Environment + # tenantName: string + # Optional + # Name of the Cloud for the Environment + # cloudName: string + # Optional + # URL of the Cluster as specified in kubeconfig + # clusterUrl: string + # Optional + # Reference to Cloud Passport + # cloudPassport: string + # Optional + # Reference to external CMDB system where the Environment Instance can be imported + # deployer: string + # Optional + # Environment description + # description: string + # Optional + # Environment owners + # owners: string + # Optional + # config: + # Optional. Default value - `false` + # If `true`, during CMDB import credentials IDs will be unique for each environment + # updateCredIdsWithEnvName: boolean + # Optional. Default value - `false` + # If `true`, during CMDB import resource profile override names will be unique for each environment + # updateRPOverrideNameWithEnvName: boolean + # Optional. Default value - `true` + # If `true`, environment-specific Resource Profile Overrides defined in envTemplate.envSpecificParamsets + # are merged with Resource Profile Overrides from the Environment Template + # If `false`, they completely replace the Environment Template's Resource Profile Overrides + # mergeEnvSpecificResourceProfiles: boolean +envTemplate: + # Mandatory + # Name of the template in the Environment Template artifact + name: platform + # Mandatory + # Template artifact in application:version notation + artifact: "template-project:feature-template-sample-SNAPSHOT" + # Optional + # bgNsArtifacts: + # Mandatory + # Template artifact in application:version notation for origin namespace + # origin: string + # Mandatory + # Template artifact in application:version notation for peer namespace + # peer: string + # Optional + # Additional variables that will be available during template rendering + # Value is a key-value hashmap + additionalTemplateVariables: + use_env_prefix: true + # Optional + # Array of filenames containing parameters that will be merged with `additionalTemplateVariables` + # Files must contain key-value hashmap + # Example: + # sharedTemplateVariables: + # - prod-template-variables + # - sample-cloud-template-variables + sharedTemplateVariables: [] + # Optional + # Set of environment-specific deployment parameters + # Keys can be either the `cloud` name or the Namespace identifier (which is defined by the `deploy_postfix` + # in the Template Descriptor, or by the Namespace template filename without extension) + # Example: + # envSpecificParamsets: + # bss: + # - env-specific-bss + # - prod-shared + envSpecificParamsets: {} + # Optional + # Environment specific pipeline (e2e) parameters set + # Keys can be either the `cloud` name or the Namespace identifier (which is defined by the `deploy_postfix` + # in the Template Descriptor, or by the Namespace template filename without extension) + # Example: + # envSpecificE2EParamsets: + # cloud: + # - cloud-level-params + envSpecificE2EParamsets: {} + # Optional + # Environment specific technical parameters set + # Keys can be either the `cloud` name or the Namespace identifier (which is defined by the `deploy_postfix` + # in the Template Descriptor, or by the Namespace template filename without extension) + # Example: + # envSpecificTechnicalParamsets: + # bss: + # - env-specific-tech + envSpecificTechnicalParamsets: {} + # Optional + # Environment specific resource profile overrides + # Keys can be either the `cloud` name or the Namespace identifier (which is defined by the `deploy_postfix` + # in the Template Descriptor, or by the Namespace template filename without extension) + # Example: + # envSpecificResourceProfiles: + # bss: + # - bss-override + envSpecificResourceProfiles: {} + # Optional + # Array of filenames containing credentials that will be merged with `additionalTemplateVariables` + # File must contain key-value hashmap + # Example: + # sharedMasterCredentialFiles: + # - prod-integration-creds + # - share-creds + sharedMasterCredentialFiles: + - "share-creds" diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/solution-env/Inventory/env_definition.yml b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/solution-env/Inventory/env_definition.yml new file mode 100644 index 000000000..8fc372cfd --- /dev/null +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/solution-env/Inventory/env_definition.yml @@ -0,0 +1,119 @@ +# See full reference at https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-configs.md#integrationyml +# +# Mandatory +inventory: {} + # Optional + # Name of the Environment + # environmentName: string + # Optional + # Name of the Tenant for the Environment + # tenantName: string + # Optional + # Name of the Cloud for the Environment + # cloudName: string + # Optional + # URL of the Cluster as specified in kubeconfig + # clusterUrl: string + # Optional + # Reference to Cloud Passport + # cloudPassport: string + # Optional + # Reference to external CMDB system where the Environment Instance can be imported + # deployer: string + # Optional + # Environment description + # description: string + # Optional + # Environment owners + # owners: string + # Optional + # config: + # Optional. Default value - `false` + # If `true`, during CMDB import credentials IDs will be unique for each environment + # updateCredIdsWithEnvName: boolean + # Optional. Default value - `false` + # If `true`, during CMDB import resource profile override names will be unique for each environment + # updateRPOverrideNameWithEnvName: boolean + # Optional. Default value - `true` + # If `true`, environment-specific Resource Profile Overrides defined in envTemplate.envSpecificParamsets + # are merged with Resource Profile Overrides from the Environment Template + # If `false`, they completely replace the Environment Template's Resource Profile Overrides + # mergeEnvSpecificResourceProfiles: boolean +envTemplate: + # Mandatory + # Name of the template in the Environment Template artifact + name: solution + # Mandatory + # Template artifact in application:version notation + artifact: "template-project:feature-template-sample-SNAPSHOT" + # Optional + # bgNsArtifacts: + # Mandatory + # Template artifact in application:version notation for origin namespace + # origin: string + # Mandatory + # Template artifact in application:version notation for peer namespace + # peer: string + # Optional + # Additional variables that will be available during template rendering + # Value is a key-value hashmap + additionalTemplateVariables: + use_env_prefix: true + # Optional + # Array of filenames containing parameters that will be merged with `additionalTemplateVariables` + # Files must contain key-value hashmap + # Example: + # sharedTemplateVariables: + # - prod-template-variables + # - sample-cloud-template-variables + sharedTemplateVariables: [] + # Optional + # Set of environment-specific deployment parameters + # Keys can be either the `cloud` name or the Namespace identifier (which is defined by the `deploy_postfix` + # in the Template Descriptor, or by the Namespace template filename without extension) + # Example: + # envSpecificParamsets: + # bss: + # - env-specific-bss + # - prod-shared + envSpecificParamsets: + bss: + - "env-specific-bss" + oss: + - "oss-env-specific" + # Optional + # Environment specific pipeline (e2e) parameters set + # Keys can be either the `cloud` name or the Namespace identifier (which is defined by the `deploy_postfix` + # in the Template Descriptor, or by the Namespace template filename without extension) + # Example: + # envSpecificE2EParamsets: + # cloud: + # - cloud-level-params + envSpecificE2EParamsets: {} + # Optional + # Environment specific technical parameters set + # Keys can be either the `cloud` name or the Namespace identifier (which is defined by the `deploy_postfix` + # in the Template Descriptor, or by the Namespace template filename without extension) + # Example: + # envSpecificTechnicalParamsets: + # bss: + # - env-specific-tech + envSpecificTechnicalParamsets: {} + # Optional + # Environment specific resource profile overrides + # Keys can be either the `cloud` name or the Namespace identifier (which is defined by the `deploy_postfix` + # in the Template Descriptor, or by the Namespace template filename without extension) + # Example: + # envSpecificResourceProfiles: + # bss: + # - bss-override + envSpecificResourceProfiles: {} + # Optional + # Array of filenames containing credentials that will be merged with `additionalTemplateVariables` + # File must contain key-value hashmap + # Example: + # sharedMasterCredentialFiles: + # - prod-integration-creds + # - share-creds + sharedMasterCredentialFiles: + - "share-creds" diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/solution-env/Inventory/parameters/cloud-env-specific.yml b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/solution-env/Inventory/parameters/cloud-env-specific.yml new file mode 100644 index 000000000..effd1b81b --- /dev/null +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/solution-env/Inventory/parameters/cloud-env-specific.yml @@ -0,0 +1,6 @@ +name: cloud-env-specific +parameters: + PAAS_PLATFORM: KUBERNETES + PAAS_VERSION: v1.32 + INGRESS_CLASS: nginx +applications: [] \ No newline at end of file diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/solution-env/Inventory/parameters/oss-env-specific.yml b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/solution-env/Inventory/parameters/oss-env-specific.yml new file mode 100644 index 000000000..d72df7679 --- /dev/null +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/solution-env/Inventory/parameters/oss-env-specific.yml @@ -0,0 +1,4 @@ +name: oss-env-specific +parameters: + LISTENER_NODE_IP: 10.10.10.10 +applications: [] \ No newline at end of file From 55b0849d71e906d5481a5355bdf9cc06d0098d3e Mon Sep 17 00:00:00 2001 From: basudev91 Date: Mon, 23 Mar 2026 15:43:48 +0530 Subject: [PATCH 100/161] docs: added envgeneNullValue tutorial doc (#1141) * docs: envgeneNullValue doc * docs: fixed linter issue * docs: wip * docs: rename --------- Co-authored-by: popoveugene --- docs/tutorials/envgene-null-value.md | 200 +++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 docs/tutorials/envgene-null-value.md diff --git a/docs/tutorials/envgene-null-value.md b/docs/tutorials/envgene-null-value.md new file mode 100644 index 000000000..c384ffb96 --- /dev/null +++ b/docs/tutorials/envgene-null-value.md @@ -0,0 +1,200 @@ +# Working with envgeneNullValue + +- [Working with envgeneNullValue](#working-with-envgenenullvalue) + - [What You Will Learn](#what-you-will-learn) + - [Prerequisites](#prerequisites) + - [Overview](#overview) + - [Scenario: Credentials Placeholder](#scenario-credentials-placeholder) + - [Problem](#problem) + - [Credential Type 1: usernamePassword](#credential-type-1-usernamepassword) + - [Generated `credentials.yml` (username/password)](#generated-credentialsyml-usernamepassword) + - [Behavior When Values Are Missing](#behavior-when-values-are-missing) + - [Credential Type 2: secret](#credential-type-2-secret) + - [Generated `credentials.yml` (secret)](#generated-credentialsyml-secret) + - [Behavior When Value Is Missing](#behavior-when-value-is-missing) + - [How to Resolve Credentials](#how-to-resolve-credentials) + - [Option 1: Cloud Passport](#option-1-cloud-passport) + - [Option 2: Shared Credentials](#option-2-shared-credentials) + - [usernamePassword example](#usernamepassword-example) + - [secret example](#secret-example) + - [Verification Step (Required)](#verification-step-required) + - [Before / After Example](#before--after-example) + - [Summary](#summary) + +## What You Will Learn + +By the end of this tutorial you will understand: + +- What `envgeneNullValue` is +- Why EnvGene uses it +- A **practical scenario: credentials placeholder** covering: + + - `usernamePassword` credentials + - `secret` credentials + +## Prerequisites + +- A working EnvGene setup (Template Repository + Instance Repository) +- Basic understanding of environment generation + +## Overview + +`envgeneNullValue` is a special placeholder used by EnvGene to represent **missing or unresolved values**. + +It is intentionally used to: + +- Mark values that must be provided later +- Prevent incomplete or insecure deployments + +Common use case: + +If a required value remains `envgeneNullValue` where a real value is mandatory, validation fails and deployment is blocked. + +## Scenario: Credentials Placeholder + +### Problem + +When EnvGene fills the [Environment Credentials File](/docs/envgene-objects.md#environment-credentials-file) (`Credentials/credentials.yml`), +it not have access to actual secret values. + +Instead, credential fields can be set to `envgeneNullValue` until you resolve them. + +## Credential Type 1: usernamePassword + +### Generated `credentials.yml` (username/password) + +```yaml +dbaas-cluster-dba-cred: + type: "usernamePassword" + data: + username: "envgeneNullValue" # FillMe + password: "envgeneNullValue" # FillMe +``` + +### Behavior When Values Are Missing + +If credentials are not resolved: + +- Validation fails during environment generation +- Deployment is blocked + +Example error: + +```text +envgenehelper.errors.ValidationError: Error while validating credentials: + credId: dbaas-cluster-dba-cred - username or password is not set +``` + +## Credential Type 2: secret + +### Generated `credentials.yml` (secret) + +```yaml +consul-admin-cred: + type: "secret" + data: + secret: "envgeneNullValue" # FillMe +``` + +### Behavior When Value Is Missing + +If the secret is not resolved: + +- Validation fails during environment generation +- Deployment is blocked + +Example error: + +```text +Error while validating credentials: + credId: consul-admin-cred - secret is not set +``` + +## How to Resolve Credentials + +`envgeneNullValue` must be replaced before deployment using one of the supported methods. + +### Option 1: Cloud Passport + +Provide credential values via Cloud Passport configuration. + +### Option 2: Shared Credentials + +See [Shared Credentials File](/docs/envgene-objects.md#shared-credentials-file) in `envgene-objects.md` for locations and merge behavior. + +#### usernamePassword example + +```yaml +dbaas-cluster-dba-cred: + type: "usernamePassword" + data: + username: "real_user" + password: "secure_password" +``` + +#### secret example + +```yaml +consul-admin-cred: + type: "secret" + data: + secret: "secret-123" +``` + +## Verification Step (Required) + +Before deployment, ensure no placeholders remain. + +PowerShell example: + +```powershell +Get-ChildItem -Recurse -Include *.yml,*.yaml | + Select-String -Pattern 'envgeneNullValue' +``` + +If any matches are found: + +- Do **not** proceed with deployment + +## Before / After Example + +**Before resolution** (same structure as the generated `credentials.yml` examples above): + +```yaml +dbaas-cluster-dba-cred: + type: "usernamePassword" + data: + username: "envgeneNullValue" # FillMe + password: "envgeneNullValue" # FillMe + +consul-admin-cred: + type: "secret" + data: + secret: "envgeneNullValue" # FillMe +``` + +**After resolution:** + +```yaml +dbaas-cluster-dba-cred: + type: "usernamePassword" + data: + username: "prod_user" + password: "secure_password" + +consul-admin-cred: + type: "secret" + data: + secret: "secret-123" +``` + +## Summary + +- `envgeneNullValue` is an **intentional placeholder** +- Used in **credentials generation** for: + + - `usernamePassword` + - `secret` types + +- Prevents incomplete or insecure deployments when validation requires real values +- Must be replaced with real values before deployment wherever your pipeline enforces it From 7f06f1b48497ef997ecfcc8aede8565c0ef179ca Mon Sep 17 00:00:00 2001 From: popoveugene Date: Mon, 23 Mar 2026 22:16:23 +0300 Subject: [PATCH 101/161] docs: add tenant cloud definition and change ES generation --- docs/envgene-objects.md | 378 +++++++++++++++++++++++++++++++- docs/features/calculator-cli.md | 42 ++-- 2 files changed, 395 insertions(+), 25 deletions(-) diff --git a/docs/envgene-objects.md b/docs/envgene-objects.md index 7041f56bb..70594413a 100644 --- a/docs/envgene-objects.md +++ b/docs/envgene-objects.md @@ -177,11 +177,73 @@ namespaces: #### Tenant Template -TBD +This is a Jinja template file used to render the [Tenant](#tenant) object. It defines tenant-level parameters for Environment Instance generation. + +The Tenant template must be developed so that after Jinja rendering, the result is a valid Tenant object according to the [schema](/schemas/tenant.schema.json). + +[Macros](/docs/template-macros.md) are available for use when developing the template. + +**Location:** The Tenant template is located at `/templates/env_templates/*/` + +**Example:** + +```yaml +name: "Applications" +registryName: "" +description: "For development" +owners: "{{ current_env.owners }}" +credential: "" +labels: [] +``` #### Cloud Template -TBD +This is a Jinja template file used to render the [Cloud](#cloud) object. It defines cluster-level parameters for Environment Instance generation. + +The Cloud template must be developed so that after Jinja rendering, the result is a valid Cloud object according to the [schema](/schemas/cloud.schema.json). + +[Macros](/docs/template-macros.md) are available for use when developing the template. + +**Location:** The Cloud template is located at `/templates/env_templates/*/` + +**Example:** + +```yaml +name: "{{ current_env.cloudNameWithCluster }}" +apiUrl: "{{ current_env.cluster.cloud_api_url }}" +apiPort: "{{ current_env.cluster.cloud_api_port }}" +privateUrl: "" +publicUrl: "{{ current_env.cluster.cloud_public_url }}" +dashboardUrl: "https://dashboard.{{ current_env.cluster.cloud_public_url }}" +labels: [] +defaultCredentialsId: "token" +protocol: "{{ current_env.cluster.cloud_api_protocol }}" +deployParameters: {} +e2eParameters: {} +technicalConfigurationParameters: {} +deployParameterSets: [] +e2eParameterSets: [] +technicalConfigurationParameterSets: [] +maasConfig: + credentialsId: "maas" + maasUrl: "http://maas-service-maas.{{ current_env.cluster.cloud_public_url }}" + maasInternalAddress: "http://maas-service.maas:8080" + enable: true +vaultConfig: + url: "" + credentialsId: "" + enable: false +dbaasConfigs: + - credentialsId: "dbaas" + apiUrl: 'http://dbaas-aggregator.dbaas:8080' + aggregatorUrl: 'https://aggregator-dbaas.{{ current_env.cluster.cloud_public_url }}' + enable: true +consulConfig: + tokenSecret: "consul-token" + publicUrl: 'https://consul.{{ current_env.cluster.cloud_public_url }}' + enabled: true + internalUrl: 'http://consul-server.consul:8500' +``` #### Namespace Template @@ -548,11 +610,319 @@ EnvGene validates each Environment Instance object against the corresponding [JS #### Tenant -TBD +The Tenant object holds tenant-level parameters describing the tenancy, including registry configuration, ownership information, and pipeline parameters. These parameters are common to all environments within the tenant. + +The Tenant object is used to generate Effective Set. + +The Tenant object is generated during Environment Instance generation based on: + +- [Tenant Template](#tenant-template) +- [Template ParamSet](#template-parameterset) +- [Instance ParamSet](#environment-specific-parameterset) + +For each parameter in the Tenant, a comment is added indicating the source Parameter Set from which this parameter originated. This is used for traceability in the generation of the environment instance. + +**Location:** `/environments///tenant.yml`. + +```yaml +# Mandatory +# Field is used to uniquely identify the Tenant +# The name of the tenant +name: string +# Mandatory +# Deprecated +# Not processed by EnvGene +registryName: string +# Optional +# Description of the tenant +# Used for documentation and identification purposes +description: string +# Optional +# Tenant owners +# Used to identify responsible parties for the tenant +owners: string +# Optional +# Deprecated +# Not processed by EnvGene +gitRepository: string +# Optional +# Deprecated +# Not processed by EnvGene +defaultBranch: string +# Optional +# The identifier for credentials used by the deployment +# Used for authentication when performing deployment operations +credential: string +# Optional +# List of labels for Tenant +# A list of labels that should be applied to the tenant +# Used for filtering, organization, and grouping +labels: list +# Optional +# Deprecated +# Not processed by EnvGene +globalE2EParameters: + # Optional + # Deprecated + # Not processed by EnvGene + pipelineDefaultRecipients: string + # Optional + # Deprecated + # Not processed by EnvGene + recipientsStrategy: string + # Optional + # Deprecated + # Not processed by EnvGene + mergeTenantsAndE2EParameters: boolean + # Optional + # Deprecated + # Not processed by EnvGene + environmentParameters: hashmap +# Optional +# Deprecated +# Not processed by EnvGene +deployParameters: hashmap +``` + +**Example:** + +```yaml +# The contents of this file is generated from template artifact: sample-template:v1.2.3. +# Contents will be overwritten by next generation. +# Please modify this contents only for development purposes or as workaround. +name: "tenant" +registryName: "" +description: "Composite Full Sample" +owners: "Qubership team" +credential: "" +labels: [] +``` + +[Tenant JSON schema](/schemas/tenant.schema.json) #### Cloud -TBD +The Cloud object holds cluster-level parameters describing the cluster and platform applications installed in it. These parameters are common to all namespaces in the environment. + +The Cloud object is used to generate Effective Set. + +The Cloud object is generated during Environment Instance generation based on: + +- [Cloud Template](#cloud-template) +- [Template ParamSet](#template-parameterset) +- [Instance ParamSet](#environment-specific-parameterset) +- [Cloud Passport](/docs/envgene-objects.md#cloud-passport) data (when used) + +For each parameter in the Cloud, a comment is added indicating the source Parameter Set from which this parameter originated. This is used for traceability in the generation of the environment instance. + +**Location:** `/environments///cloud.yml`. + +```yaml +# Mandatory +# The name of the cloud configuration +# Typically combines cluster and environment name +name: string +# Mandatory +# The URL of the API endpoint of the cloud +# Used to connect to the Kubernetes cluster API server +apiUrl: string +# Mandatory +# The port on which the API runs +# Used to connect to the Kubernetes cluster API server +apiPort: integer|string +# Optional +# The private-facing URL for internal access +# Used to form service URLs accessible from within the cluster +privateUrl: string +# Optional +# The public-facing URL for external access +# Used to form service URLs accessible from outside the cluster +# Calculator macros are generated based on this URL +publicUrl: string +# Mandatory +# The URL for accessing the cloud's k8s dashboard +# Used for monitoring and management +dashboardUrl: string +# Mandatory +# A list of labels for categorizing or tagging the cloud +# Used for filtering, organization, and grouping +labels: list +# Mandatory +# The identifier for credentials used by the deployment +# Used for authentication when performing deployment +defaultCredentialsId: string +# Mandatory +# The communication protocol used +# HTTP or HTTPS +protocol: string +# Optional +# Deprecated +# Not processed by EnvGene +version: number +# Optional +# Deprecated +# Not processed by EnvGene +dbMode: string +# Optional +# Deprecated +# Not processed by EnvGene +databases: array +# Optional +# Deprecated +# Not processed by EnvGene +mergeDeployParametersAndE2EParameters: boolean +# Mandatory +# Configuration for the monitoring-as-a-service (MaaS) +maasConfig: + # Optional + # Credentials identifier for MaaS + # Used for authentication when accessing MaaS + credentialsId: string + # Mandatory + # Flag to enable or disable MaaS + # Controls whether MaaS-related parameters appear in the Effective Set + enable: boolean + # Optional + # URL for accessing MaaS + # Used to configure external access to MaaS + maasUrl: string + # Optional + # Internal address for MaaS + # Used to configure internal cluster access to MaaS + maasInternalAddress: string +# Mandatory +# Configuration for the vault service +vaultConfig: + # Optional + # Credentials identifier for the vault + # Used for authentication when accessing Vault + credentialsId: string + # Mandatory + # Flag to enable or disable vault integration + # Controls whether Vault-related parameters appear in the Effective Set + enable: boolean + # Optional + # The vault service URL + # Used to configure access to Vault + url: string +# Optional +# Database-as-a-service (DBaaS) configurations +# Multiple DBaaS instances can be configured +dbaasConfigs: + - # Optional + # Credentials identifier for DBaaS + # Used for authentication when accessing DBaaS + credentialsId: string + # Mandatory + # Flag to enable or disable DBaaS + # Controls whether DBaaS-related parameters appear in the Effective Set + enable: boolean + # Optional + # API URL for DBaaS + # Used to configure internal cluster access to DBaaS + apiUrl: string + # Optional + # URL for the DBaaS aggregator + # Used to configure external access to DBaaS + aggregatorUrl: string +# Mandatory +# Configuration for Consul service integration +consulConfig: + # Optional + # Secret token for Consul authentication + # Used for authentication when accessing Consul + tokenSecret: string + # Mandatory + # Flag to enable or disable Consul integration + # Controls whether Consul-related parameters appear in the Effective Set + enabled: boolean + # Optional + # The public URL for accessing Consul + # Used to configure external access to Consul + publicUrl: string + # Optional + # The internal URL for accessing Consul + # Used to configure internal cluster access to Consul + internalUrl: string +# Optional +# Key-value pairs of deployment parameters at the cloud level +# Used to set parameters that will be used for rendering Helm charts of applications in this cloud +deployParameters: hashmap +# Optional +# Key-value pairs of e2e parameters at the cloud level +# Used to configure the systems/pipelines managing the Environment lifecycle for this cloud +e2eParameters: hashmap +# Optional +# Key-value pairs of technical configuration parameters at the cloud level +# Used to set parameters that can be applied to the application at runtime +# without redeployment for this cloud +technicalConfigurationParameters: hashmap +# Optional +# List of deployment Parameter Set names to include at the cloud level +# Used to set parameters that will be used for rendering Helm charts of applications in this cloud +deployParameterSets: list +# Optional +# List of e2e Parameter Set names to include at the cloud level +# Used to configure the systems/pipelines managing the Environment lifecycle for this cloud +e2eParameterSets: list +# Optional +# List of technical configuration Parameter Set names to include at the cloud level +# Used to include predefined sets of parameters that can be applied to the application at runtime +# without redeployment for this cloud +technicalConfigurationParameterSets: list +``` + +**Example:** + +```yaml +# The contents of this file is generated from template artifact: sample-template:v1.2.3. +# Contents will be overwritten by next generation. +# Please modify this contents only for development purposes or as workaround. +name: "cluster_01_env_01" +apiUrl: "api.cluster-01.qubership.org" # cloud passport: cluster-01 version: 1.5 +apiPort: "6443" # cloud passport: cluster-01 version: 1.5 +privateUrl: "cluster-01.qubership.org" # cloud passport: cluster-01 version: 1.5 +publicUrl: "cluster-01.qubership.org" # cloud passport: cluster-01 version: 1.5 +dashboardUrl: "https://dashboard.cluster-01.qubership.org" # cloud passport: cluster-01 version: 1.5 +labels: [] +defaultCredentialsId: "cloud-deploy-sa-token" # cloud passport: cluster-01 version: 1.5 +protocol: "https" # cloud passport: cluster-01 version: 1.5 +maasConfig: + credentialsId: "maas-cred" # cloud passport: cluster-01 version: 1.5 + enable: true # cloud passport: cluster-01 version: 1.5 + maasUrl: "http://maas.cluster-01.qubership.org" # cloud passport: cluster-01 version: 1.5 + maasInternalAddress: "http://maas.maas:8080" # cloud passport: cluster-01 version: 1.5 +vaultConfig: + credentialsId: "" + enable: false + url: "" +dbaasConfigs: + - credentialsId: "dbaas-cred" # cloud passport: cluster-01 version: 1.5 + enable: true # cloud passport: cluster-01 version: 1.5 + apiUrl: "http://dbaas.dbaas:8080" # cloud passport: cluster-01 version: 1.5 + aggregatorUrl: "https://dbaas.cluster-01.qubership.org" # cloud passport: cluster-01 version: 1.5 +consulConfig: + tokenSecret: "consul-cred" # cloud passport: cluster-01 version: 1.5 + enabled: true # cloud passport: cluster-01 version: 1.5 + publicUrl: "http://consul.consul:8080" # cloud passport: cluster-01 version: 1.5 + internalUrl: "http://consul.consul:8080" # cloud passport: cluster-01 version: 1.5 +deployParameters: + CLOUD_DASHBOARD_URL: "https://dashboard.cluster-01.qubership.org" # cloud passport: cluster-01 version: 1.5 + CMDB_URL: "https://cluster-01.qubership.org" # cloud passport: cluster-01 version: 1.5 + CONSUL_ENABLED: "true" # cloud passport: cluster-01 version: 1.5 + MAVEN_REPO_URL: "https://artifactory.qubership.org" # cloud passport: cluster-01 version: 1.5 + MONITORING_ENABLED: "true" # cloud passport: cluster-01 version: 1.5 + STORAGE_RWO_CLASS: "standard" # cloud passport: cluster-01 version: 1.5 + ZOOKEEPER_ADDRESS: "zookeeper.zookeeper:2181" # cloud passport: cluster-01 version: 1.5 +e2eParameters: + CLOUD_LEVEL_PARAM_1: "cloud-level-value-1" # paramset: cloud-level-params version: 25.1 source: instance +technicalConfigurationParameters: {} +deployParameterSets: [] +e2eParameterSets: [] +technicalConfigurationParameterSets: [] +``` + +[Cloud JSON schema](/schemas/cloud.schema.json) #### Namespace diff --git a/docs/features/calculator-cli.md b/docs/features/calculator-cli.md index e46c197a6..28aeac12c 100644 --- a/docs/features/calculator-cli.md +++ b/docs/features/calculator-cli.md @@ -681,19 +681,19 @@ The `` can be complex, such as a map or a list, whose elements can also b | `SERVER_HOSTNAME` | yes | string | **Deprecated**. Uses `CLOUD_PUBLIC_HOST` if set, otherwise falls back to `CLOUD_API_HOST` | None | N/A | | `CUSTOM_HOST` | yes | string | **Deprecated**. Uses `CLOUD_PRIVATE_HOST` if set, otherwise falls back to `SERVER_HOSTNAME` | None | N/A | | `OPENSHIFT_SERVER` | yes | string | **Deprecated**. Constructed as `CLOUD_PROTOCOL`://`CLOUD_PUBLIC_HOST`:`CLOUD_API_PORT` | None | N/A | -| `DBAAS_ENABLED` | yes | boolean | Feature toggle indicating whether DBaaS is used | `false` | `dbaasConfigs[0].enable` in the `Cloud` | -| `API_DBAAS_ADDRESS` | no | string | DBaaS API endpoint accessible within a cluster network. Provided if `DBAAS_ENABLED: true` only | None | `dbaasConfigs[0].apiUrl` in the `Cloud` | -| `DBAAS_AGGREGATOR_ADDRESS` | no | string | DBaaS API endpoint accessible outside the cluster network. Provided if `DBAAS_ENABLED: true` only | None | `dbaasConfigs[0].aggregatorUrl` in the `Cloud` | -| `MAAS_ENABLED` | yes | boolean | Feature toggle indicating whether MaaS is used | `false` | `maasConfig.enable` in the `Cloud` | -| `MAAS_INTERNAL_ADDRESS` | no | string | MaaS API endpoint accessible within a cluster network. Provided if `MAAS_ENABLED: true` only | None | `maasConfig.maasInternalAddress` in the `Cloud` | -| `MAAS_EXTERNAL_ROUTE` | no | string | Maas API endpoint accessible outside the cluster network. Provided if `MAAS_ENABLED: true` only | None | `maasConfig.maasUrl` in the `Cloud` | -| `MAAS_SERVICE_ADDRESS` | no | string | **Deprecated**. The same as `MAAS_EXTERNAL_ROUTE`. Provided if `MAAS_ENABLED: true` only | None | `maasConfig.maasUrl` in the `Cloud` | -| `VAULT_ENABLED` | yes | boolean | Feature toggle indicating whether Vault is used | `false` | `vaultConfig.enable` in the `Cloud` | -| `VAULT_ADDR` | no | string | Vault API endpoint accessible within a cluster network. Provided if `VAULT_ENABLED: true` only | None | `vaultConfig.enable` in the `Cloud` | -| `PUBLIC_VAULT_URL` | no | string | Vault API endpoint accessible outside the cluster network. Provided if `VAULT_ENABLED: true` only | None | `vaultConfig.url` in the `Cloud` | -| `CONSUL_ENABLED` | yes | boolean | Feature toggle indicating whether Consul is used | `false` | `consulConfig.enabled` in the `Cloud` | -| `CONSUL_URL` | no | string | Consul API endpoint accessible within a cluster network. Provided if `CONSUL_ENABLED: true` only | None | `consulConfig.internalUrl` in the `Cloud` | -| `CONSUL_PUBLIC_URL` | no | string | Consul API endpoint accessible within a cluster network. Provided if `CONSUL_ENABLED: true` only | None | `consulConfig.internalUrl` in the `Cloud` | +| `DBAAS_ENABLED` | no | boolean | Feature toggle indicating whether DBaaS is used | `false` | `dbaasConfigs[0].enable` in the `Cloud` | +| `API_DBAAS_ADDRESS` | no | string | DBaaS API endpoint accessible within a cluster network. Omitted if `dbaasConfigs[0].enable: false` | None | `dbaasConfigs[0].apiUrl` in the `Cloud` | +| `DBAAS_AGGREGATOR_ADDRESS` | no | string | DBaaS API endpoint accessible outside the cluster network. Omitted if `dbaasConfigs[0].enable: false` | None | `dbaasConfigs[0].aggregatorUrl` in the `Cloud` | +| `MAAS_ENABLED` | no | boolean | Feature toggle indicating whether MaaS is used | `false` | `maasConfig.enable` in the `Cloud` | +| `MAAS_INTERNAL_ADDRESS` | no | string | MaaS API endpoint accessible within a cluster network. Omitted if `maasConfig.enable: false` | None | `maasConfig.maasInternalAddress` in the `Cloud` | +| `MAAS_EXTERNAL_ROUTE` | no | string | Maas API endpoint accessible outside the cluster network. Omitted if `maasConfig.enable: false` | None | `maasConfig.maasUrl` in the `Cloud` | +| `MAAS_SERVICE_ADDRESS` | no | string | **Deprecated**. The same as `MAAS_EXTERNAL_ROUTE`. Omitted if `maasConfig.enable: false` | None | `maasConfig.maasUrl` in the `Cloud` | +| `VAULT_ENABLED` | no | boolean | Feature toggle indicating whether Vault is used | `false` | `vaultConfig.enable` in the `Cloud` | +| `VAULT_ADDR` | no | string | Vault API endpoint accessible within a cluster network. Omitted if `vaultConfig.enable: false` | None | `vaultConfig.enable` in the `Cloud` | +| `PUBLIC_VAULT_URL` | no | string | Vault API endpoint accessible outside the cluster network. Omitted if `vaultConfig.enable: false` | None | `vaultConfig.url` in the `Cloud` | +| `CONSUL_ENABLED` | no | boolean | Feature toggle indicating whether Consul is used | `false` | `consulConfig.enabled` in the `Cloud` | +| `CONSUL_URL` | no | string | Consul API endpoint accessible within a cluster network. Omitted if `consulConfig.enabled: false` | None | `consulConfig.internalUrl` in the `Cloud` | +| `CONSUL_PUBLIC_URL` | no | string | Consul API endpoint accessible within a cluster network. Omitted if `consulConfig.enabled: false` | None | `consulConfig.internalUrl` in the `Cloud` | | `PRODUCTION_MODE` | no | boolean | Defines the deployment environment (non-production/production) type for restricting Helm chart content | `false` | `deployParameters.PRODUCTION_MODE` in the `Cloud` | | `TENANTNAME` | yes | string | Tenant name | None | `name` in the `Tenant` | | `CLOUDNAME` | yes | string | Cloud name | None | `name` in the `Cloud` | @@ -761,14 +761,14 @@ global: &id001 | Attribute | Mandatory | Type | Description | Default | Source Environment Instance | |------------------------------------------|-----------|--------|-------------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `K8S_TOKEN` | yes | string | Cluster's API token | None | Taken from `data.secret` in the `Credential` set by `defaultCredentialsId` in the related `Namespace` or parent `Cloud`. If not set in `Namespace`, inherited from `Cloud`. `Namespace` has priority if both are set. | -| `DBAAS_AGGREGATOR_USERNAME` | no | string | DBaaS username | None | Taken from `data.username` property of the `Credential` specified via `dbaasConfigs[0].credentialsId.username` in the `Cloud` | -| `DBAAS_AGGREGATOR_PASSWORD` | no | string | DBaaS password | None | Taken from `data.password` property of the `Credential` specified via `dbaasConfigs[0].credentialsId.password` in the `Cloud` | -| `DBAAS_CLUSTER_DBA_CREDENTIALS_USERNAME` | no | string | same as `DBAAS_AGGREGATOR_USERNAME` | None | Taken from `data.username` property of the `Credential` specified via `dbaasConfigs[0].credentialsId.username` in the `Cloud` | -| `DBAAS_CLUSTER_DBA_CREDENTIALS_PASSWORD` | no | string | same as `DBAAS_AGGREGATOR_PASSWORD` | None | Taken from `data.password` property of the `Credential` specified via `dbaasConfigs[0].credentialsId.password` in the `Cloud` | -| `MAAS_CREDENTIALS_USERNAME` | no | string | MaaS username | None | Taken from `data.username` property of the `Credential` specified via `maasConfig.credentialsId.username` in the `Cloud` | -| `MAAS_CREDENTIALS_PASSWORD` | no | string | MaaS password | None | Taken from `data.password` property of the `Credential` specified via `maasConfig.credentialsId.password` in the `Cloud` | -| `VAULT_TOKEN` | no | string | Vault token | None | Taken from `data.secret` property of the `Credential` specified via `vaultConfig.credentialsId.secret` in the `Cloud` | -| `CONSUL_ADMIN_TOKEN` | no | string | Consul admin token | None | Taken from `data.secret` property of the `Credential` specified via `consulConfig.internalUrl` in the `Cloud` | +| `DBAAS_AGGREGATOR_USERNAME` | no | string | DBaaS username. Omitted if `dbaasConfigs[0].enable: false` or `dbaasConfigs[0].credentialsId: ""` | None | Taken from `data.username` property of the `Credential` specified via `dbaasConfigs[0].credentialsId.username` in the `Cloud` | +| `DBAAS_AGGREGATOR_PASSWORD` | no | string | DBaaS password. Omitted if `dbaasConfigs[0].enable: false` or `dbaasConfigs[0].credentialsId: ""` | None | Taken from `data.password` property of the `Credential` specified via `dbaasConfigs[0].credentialsId.password` in the `Cloud` | +| `DBAAS_CLUSTER_DBA_CREDENTIALS_USERNAME` | no | string | same as `DBAAS_AGGREGATOR_USERNAME`. Omitted if `dbaasConfigs[0].enable: false` or `dbaasConfigs[0].credentialsId: ""` | None | Taken from `data.username` property of the `Credential` specified via `dbaasConfigs[0].credentialsId.username` in the `Cloud` | +| `DBAAS_CLUSTER_DBA_CREDENTIALS_PASSWORD` | no | string | same as `DBAAS_AGGREGATOR_PASSWORD`. Omitted if `dbaasConfigs[0].enable: false` or `dbaasConfigs[0].credentialsId: ""` | None | Taken from `data.password` property of the `Credential` specified via `dbaasConfigs[0].credentialsId.password` in the `Cloud` | +| `MAAS_CREDENTIALS_USERNAME` | no | string | MaaS username. Omitted if `maasConfig.enable: false` or `maasConfig.credentialsId: ""` | None | Taken from `data.username` property of the `Credential` specified via `maasConfig.credentialsId.username` in the `Cloud` | +| `MAAS_CREDENTIALS_PASSWORD` | no | string | MaaS password. Omitted if `maasConfig.enable: false` or `maasConfig.credentialsId: ""` | None | Taken from `data.password` property of the `Credential` specified via `maasConfig.credentialsId.password` in the `Cloud` | +| `VAULT_TOKEN` | no | string | Vault token. Omitted if `vaultConfig.enable: false` or `vaultConfig.credentialsId: ""` | None | Taken from `data.secret` property of the `Credential` specified via `vaultConfig.credentialsId.secret` in the `Cloud` | +| `CONSUL_ADMIN_TOKEN` | no | string | Consul admin token. Omitted if `consulConfig.enabled: false` or `consulConfig.tokenSecret: ""` | None | Taken from `data.secret` property of the `Credential` specified via `consulConfig.tokenSecret` in the `Cloud` | | `SSL_SECRET_VALUE` | no | string | SSL Certificate bundle | None | The value is taken from the deployment parameter `DEFAULT_SSL_CERTIFICATES_BUNDLE`, which can be set at the `Tenant`, `Cloud`, `Namespace`, or `Application` | | `CA_BUNDLE_CERTIFICATE` | no | string | SSL Certificate bundle | None | The value is taken from the deployment parameter `DEFAULT_SSL_CERTIFICATES_BUNDLE`, which can be set at the `Tenant`, `Cloud`, `Namespace`, or `Application` | From 3e0583792732883bd3509d772672728c6322214b Mon Sep 17 00:00:00 2001 From: Siva Reddy Kunduru <35566000+sivareddyit@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:37:12 +0530 Subject: [PATCH 102/161] fix: added authorization for downloading the zip file from artifactory using registry credentials (#1167) --- python/artifact-searcher/artifact_searcher/artifact.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python/artifact-searcher/artifact_searcher/artifact.py b/python/artifact-searcher/artifact_searcher/artifact.py index c40b7cde6..3af0edf9b 100644 --- a/python/artifact-searcher/artifact_searcher/artifact.py +++ b/python/artifact-searcher/artifact_searcher/artifact.py @@ -420,9 +420,10 @@ def check_artifact(repo_url: str, group_id: str, artifact_id: str, version: str, folder = version_to_folder_name(version) filename = create_artifact_name(artifact_id, artifact_extension, version, classifier) full_url = urljoin(base, f"{group_id}/{artifact_id}/{folder}/{filename}") - + auth = HTTPBasicAuth(cred.username, cred.password) if cred else None + try: - response = requests.head(full_url, timeout=DEFAULT_REQUEST_TIMEOUT) + response = requests.head(full_url, auth=auth, timeout=DEFAULT_REQUEST_TIMEOUT) if response.status_code == 200: logger.info( f"[Repository: {repo_url}] [Artifact: {group_id}:{artifact_id}:{version}] - Artifact found: {full_url}" From 94221471187668d93986b8e0b50da89e04585ab0 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Tue, 24 Mar 2026 08:58:41 +0000 Subject: [PATCH 103/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 6edae1936..58d9a4ce3 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -69,9 +69,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.11" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.11" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.11" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.12" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.12" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.12" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index c06cd0795..9436b854d 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.11 +version: 1.31.12 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 1f8f82920..21768b45d 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.11 +version: 1.31.12 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 0bdb7485d..b8070bd01 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.11", + "envgene_version": "1.31.12", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From e3c3df2c752248d0f4be4e4f0c6f6a6549185852 Mon Sep 17 00:00:00 2001 From: popoveugene <42543333+popoveugene@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:26:46 +0300 Subject: [PATCH 104/161] docs: add template composition algorithm (#1176) --- docs/envgene-pipelines.md | 69 ++++++++++++++-------- docs/features/template-composition.md | 85 ++++++++++++++++++++++++++- 2 files changed, 127 insertions(+), 27 deletions(-) diff --git a/docs/envgene-pipelines.md b/docs/envgene-pipelines.md index 854338bff..05e66346a 100644 --- a/docs/envgene-pipelines.md +++ b/docs/envgene-pipelines.md @@ -4,7 +4,7 @@ This document describes the CI/CD pipelines and jobs in these pipelines used in Jobs are executed sequentially in the **listed order**. Depending on the condition, a job may or may not be executed. If any job in the sequence fails, the subsequent jobs in the flow are not executed. -The conditions for job execution, the sequence, and the Docker images are the same for both GitLab and GitHub CI/CD platforms. +The conditions for job execution, the sequence, and the Docker images are intended to match across GitLab and GitHub CI/CD platforms. > [!NOTE] > This is the sequence of the core EnvGene. EnvGene is extensible: in extensions, the sequence may be changed and new jobs may be added. @@ -17,30 +17,47 @@ This pipeline is triggered manually by the user via the GitLab/GitHub UI or by a If multiple [`ENV_NAMES`](/docs/instance-pipeline-parameters.md#env_names) are specified: -- For each cluster from `ENV_NAMES`, parallel and independent Cloud Passport discovery flows are started, consisting of (`trigger_passport_job`, `get_passport_job`, `process_decryption_mode_job`). -- For each Environment from `ENV_NAMES`, parallel flows of the remaining jobs are started. +- For each distinct cluster name in `ENV_NAMES`, at most one Cloud Passport flow runs: `trigger_passport_job`, `get_passport_job` (deduplicated by cluster). +- For each environment from `ENV_NAMES`, parallel flows of the remaining jobs are started. ### [Instance pipeline] Job sequence ```mermaid -flowchart LR - A[trigger_passport] --> B[get_passport] --> C[process_decryption_mode] --> D[env_inventory_generation] --> E[credential_rotation] --> F[app_reg_def_process] --> G[process_sd] --> H[env_build] --> I[generate_effective_set] --> J[git_commit] +flowchart TB + subgraph passport["Per-cluster jobs"] + A[trigger_passport] --> B[get_passport] + end + subgraph per_env["Per-environment jobs"] + C[bg_manage] --> D[env_inventory_generation] + D --> E[credential_rotation] + E --> F[app_reg_def_process] + F --> G[process_sd] + G --> H[env_build] + H --> I[generate_effective_set] + I --> J[git_commit] + J --> K[cmdb_import] + end + B --> C ``` -1. **bg_manage** - - **Condition**: Runs if [`BG_MANAGE: true`](/docs/instance-pipeline-parameters.md#bg_manage). - - **Docker image**: [`qubership-envgene`](https://github.com/Netcracker/qubership-envgene/pkgs/container/qubership-envgene) - -2. **trigger_passport**: +1. **trigger_passport**: - **Condition**: Runs if [`GET_PASSPORT: true`](/docs/instance-pipeline-parameters.md#get_passport) - **Docker image**: None. The Discovery repository is triggered from the pipeline -3. **get_passport**: +2. **get_passport**: - **Condition**: Runs if [`GET_PASSPORT: true`](/docs/instance-pipeline-parameters.md#get_passport) - **Docker image**: [`qubership-envgene`](https://github.com/Netcracker/qubership-envgene/pkgs/container/qubership-envgene) +3. **bg_manage** + - **Condition**: Runs if [`BG_MANAGE: true`](/docs/instance-pipeline-parameters.md#bg_manage). + - **Docker image**: [`qubership-envgene`](https://github.com/Netcracker/qubership-envgene/pkgs/container/qubership-envgene) + 4. **env_inventory_generation**: - - **Condition**: Runs if [`ENV_TEMPLATE_TEST: false`](/docs/envgene-repository-variables.md#env_template_test) AND ([`ENV_SPECIFIC_PARAMS`](/docs/instance-pipeline-parameters.md#env_specific_params) OR [`ENV_TEMPLATE_NAME`](/docs/instance-pipeline-parameters.md#env_template_name)) + - **Condition**: Runs if [`ENV_TEMPLATE_TEST: false`](/docs/envgene-repository-variables.md#env_template_test) AND any of the following holds: + - [`ENV_INVENTORY_CONTENT`](/docs/instance-pipeline-parameters.md#env_inventory_content) is set, or + - [`ENV_INVENTORY_INIT`](/docs/instance-pipeline-parameters.md#env_inventory_init) is `true`, or + - [`ENV_SPECIFIC_PARAMS`](/docs/instance-pipeline-parameters.md#env_specific_params) is set (non-empty), or + - [`ENV_TEMPLATE_NAME`](/docs/instance-pipeline-parameters.md#env_template_name) is set (non-empty) - **Docker image**: [`qubership-envgene`](https://github.com/Netcracker/qubership-envgene/pkgs/container/qubership-envgene) 5. **credential_rotation**: @@ -50,28 +67,26 @@ flowchart LR 6. **app_reg_def_process**: - **What happens in this job**: 1. Handles certificate updates from the configuration directory. - 2. Downloads the Environment Template artifact. - 3. Renders [Application Definitions](/docs/envgene-objects.md#application-definition) and [Registry Definitions](/docs/envgene-objects.md#registry-definition) from: + 2. Renders [Application Definitions](/docs/envgene-objects.md#application-definition) and [Registry Definitions](/docs/envgene-objects.md#registry-definition) from: 1. Templates, as described in [User Defined by Template](/docs/features/app-reg-defs.md#user-defined-by-template) 2. External Job, as described in [External Job](/docs/features/app-reg-defs.md#external-job) - 4. Runs [Application and Registry Definitions Transformation](/docs/features/app-reg-defs.md#application-and-registry-definitions-transformation) - - **Condition**: Runs if ( [`ENV_BUILD: true`](/docs/instance-pipeline-parameters.md#env_builder) ) + 3. Runs [Application and Registry Definitions Transformation](/docs/features/app-reg-defs.md#application-and-registry-definitions-transformation) + - **Condition**: Runs if [`ENV_BUILD: true`](/docs/instance-pipeline-parameters.md#env_builder) - **Docker image**: [`qubership-envgene`](https://github.com/Netcracker/qubership-envgene/pkgs/container/qubership-envgene) 7. **process_sd**: - - **Condition**: Runs if ( [`SOURCE_TYPE: json`](/docs/instance-pipeline-parameters.md#sd_source_type) AND [`SD_DATA`](/docs/instance-pipeline-parameters.md#sd_data) is provided ) OR ( [`SOURCE_TYPE: artifact`](/docs/instance-pipeline-parameters.md#sd_source_type) AND [`SD_VERSIONS`](/docs/instance-pipeline-parameters.md#sd_version) is provided ) + - **Condition**: Runs if ( [`SD_SOURCE_TYPE: json`](/docs/instance-pipeline-parameters.md#sd_source_type) AND [`SD_DATA`](/docs/instance-pipeline-parameters.md#sd_data) is provided ) OR ( [`SD_SOURCE_TYPE: artifact`](/docs/instance-pipeline-parameters.md#sd_source_type) AND [`SD_VERSION`](/docs/instance-pipeline-parameters.md#sd_version) is provided ) - **Docker image**: [`qubership-envgene`](https://github.com/Netcracker/qubership-envgene/pkgs/container/qubership-envgene) 8. **env_build**: - **What happens in this job**: 1. Handles certificate updates from the configuration directory. - 2. Downloads the Environment Template artifact from the `app_reg_def_process` job artifacts, **not from the registry** - 3. Updates the Environment Template version if [`ENV_TEMPLATE_VERSION`](/docs/instance-pipeline-parameters.md#env_template_version) is provided. - 4. Renders the environment using Jinja2 templates (renders Namespaces, Clouds, and other environment components, but not Application and Registry Definitions). - 5. Handles template overrides - 6. Handles template Parameter Set and Resource profiles. - 7. Handles environment-specific Parameter Set and Resource profiles. - 8. Creates Credentials including shared Credentials + 2. Updates the Environment Template version if [`ENV_TEMPLATE_VERSION`](/docs/instance-pipeline-parameters.md#env_template_version) is provided. + 3. Renders the environment using Jinja2 templates (renders Namespaces, Clouds, and other environment components, but not Application and Registry Definitions). + 4. Handles template overrides + 5. Handles template Parameter Set and Resource profiles. + 6. Handles environment-specific Parameter Set and Resource profiles. + 7. Creates Credentials including shared Credentials - **Condition**: Runs if [`ENV_BUILD: true`](/docs/instance-pipeline-parameters.md#env_builder). - **Docker image**: [`qubership-envgene`](https://github.com/Netcracker/qubership-envgene/pkgs/container/qubership-envgene) @@ -82,3 +97,9 @@ flowchart LR 10. **git_commit**: - **Condition**: Runs if there are jobs requiring changes to the repository AND [`ENV_TEMPLATE_TEST: false`](/docs/envgene-repository-variables.md#env_template_test) - **Docker image**: [`qubership-envgene`](https://github.com/Netcracker/qubership-envgene/pkgs/container/qubership-envgene) + +11. **cmdb_import**: + - **Condition**: Runs if [`CMDB_IMPORT: true`](/docs/instance-pipeline-parameters.md#cmdb_import) + + > [!NOTE] + > The `cmdb_import` job is **not** part of core EnvGene. It is an **extension point** diff --git a/docs/features/template-composition.md b/docs/features/template-composition.md index 6d40d09fb..68159c578 100644 --- a/docs/features/template-composition.md +++ b/docs/features/template-composition.md @@ -4,6 +4,7 @@ - [Problem Statement](#problem-statement) - [Proposed Approach](#proposed-approach) - [Key Capabilities](#key-capabilities) + - [Detailed Composition Algorithm](#detailed-composition-algorithm) - [Use Cases](#use-cases) - [Case 1](#case-1) - [Case 2](#case-2) @@ -77,6 +78,84 @@ This diagram shows parent and child templates with their components. The color o - Parent templates are regular EnvGene templates needing no special configuration - Supports multi-level composition chains +### Detailed Composition Algorithm + +The sequence below describes how composition is executed during template build. + +1. **Discover descriptors** + + Read all `*.yml|*.yaml` files from `templates/env_templates` (top-level only, non-recursive). + Each discovered file is processed as an independent child Template Descriptor. + +2. **Check whether composition is needed** + + If a descriptor does not contain `parent-templates`, composition is skipped for that descriptor. + +3. **Validate parent references** + + - `parent-templates` cannot be empty when declared. + - If multiple parents are declared, each inheriting namespace must explicitly set `parent`. + +4. **Resolve parent artifacts** + + - For each `app:version` in `parent-templates`, find matching Artifact Definition in `configuration/artifact_definitions`. + - Download and unpack the parent template artifact into temporary storage. + +5. **Prepare resource files (`templates/*`)** + + Copy resources in this order: + + - `templates/*` from parents referenced by `namespaces[].parent` + - `templates/*` from `tenant.parent` (if used) + - `templates/*` from `cloud.parent` (if used) + - child `templates/*` (always last) + + > [!IMPORTANT] + > The `templates/*` step is file-based and recursive. It includes all files and directories under `templates/`, including: + > + > - `env_templates` + > - `resource_profiles` + > - `parameters` + > - `appdefs` + > - `regdefs` + > + > If the same relative path exists in multiple sources, the later copy overwrites the earlier one. + +6. **Build resulting Template Descriptor** + + - Create the resulting descriptor as a copy of the child descriptor, then remove `parent-templates` from that resulting descriptor. + - If `tenant: { parent: }` is used, replace child `tenant` with the `tenant` value from the parent template descriptor referenced by ``. + - If `cloud: { parent: }` is used, replace child `cloud` with the `cloud` value from the parent template descriptor referenced by ``. + - If top-level `tenant`, `cloud` or `composite_structure` are missing in child, they are inherited from the first matching parent in namespace iteration order (`first parent wins`) and are not overwritten later. + +7. **Process namespaces** + + - For each namespace with `parent`, find the parent namespace by exact `name` and use it as the base namespace entry in the resulting descriptor. + - Then, if the child namespace defines `template_path`, overwrite the inherited `template_path` with the child value. + +8. **Apply `overrides-parent` (Cloud and Namespace only)** + + - Parameter maps (`deployParameters`, `e2eParameters`, `technicalConfigurationParameters`) are merged into `template_override`. + - Parameter set lists (`deployParameterSets`, `e2eParameterSets`, `technicalConfigurationParameterSets`) are appended into `template_override`. + - `profile` override has two modes: + - default mode (`merge-with-parent` is false or not set): use the override profile as the resulting profile reference; + - merge mode (`merge-with-parent: true`): merge override profile content into the selected parent profile and use the merged result. + - `tenant` does not support `overrides-parent` in template composition (inherit-only behavior). + +9. **Persist output** + + - Save composed descriptor to output `env_templates`. + - Save generated/merged resource profiles to output `resource_profiles`. + +10. **Resulting artifact** + + The output is a regular EnvGene template artifact and does not require special runtime handling. + +> [!IMPORTANT] +> File precedence is copy-order based. If the same file path exists in multiple sources, the last copied file wins. +> Effective precedence is: +> **namespace parents** - **tenant parent** - **cloud parent** - **child template files**. + ### Use Cases This feature can be used in scenarios where EnvGene manages configuration parameters for complex solutions consisting of multiple applications or application groups, with parameters developed and tested by different teams. @@ -109,10 +188,10 @@ parent-templates: default-bss: bss-product-template:2.0.0 basic-template: basic-product-template:10.1.3 # Optional -# If not set, the most recent Composite Structure found in the parent templates referenced by the `namespaces` attribute will be used +# If not set, inherit from the first matching parent encountered during namespace processing (`first parent wins`) composite_structure: "{{ templates_dir }}/env_templates/composite/composite_structure.yml.j2" # Optional -# If not set, the most recent Tenant found in the parent templates referenced by the `namespaces` attribute will be used +# If not set, inherit from the first matching parent encountered during namespace processing (`first parent wins`) # It can be string or dict, if string is provide that means that no composition is needed and the exact template will be used # example of string value tenant: "{{ templates_dir }}/env_templates/default/tenant.yml.j2" @@ -120,7 +199,7 @@ tenant: "{{ templates_dir }}/env_templates/default/tenant.yml.j2" tenant: parent: basic-template # Optional -# If not set, the most recent Cloud found in the parent templates referenced by the `namespaces` attribute will be used +# If not set, inherit from the first matching parent encountered during namespace processing (`first parent wins`) # It can be string or dict, if string is provide that means that no composition is needed and the exact template will be used # example of string value cloud: "{{ templates_dir }}/env_templates/default/cloud.yml.j2" From 53c558a2f7e6d5cd8ef80775879d866b1bf06406 Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Wed, 25 Mar 2026 08:13:09 +0300 Subject: [PATCH 105/161] feat: Updated config env in Github Envgene instance pipeline (#1177) --- .../instance-repo-pipeline/.github/configuration/config.env | 3 --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/configuration/config.env b/github_workflows/instance-repo-pipeline/.github/configuration/config.env index aa4329933..56b99fd69 100644 --- a/github_workflows/instance-repo-pipeline/.github/configuration/config.env +++ b/github_workflows/instance-repo-pipeline/.github/configuration/config.env @@ -1,6 +1,3 @@ -CI_PROJECT_DIR=/workspace -INSTANCES_DIR=/workspace/environments -COMMIT_ENV=true GITHUB_USER_EMAIL=envgene@qubership.org GITHUB_USER_NAME=envgene SECRET_POSTFIX=custom_secret diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 58d9a4ce3..371cf8cbe 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -58,6 +58,7 @@ on: env: CI_COMMIT_REF_NAME: ${{ github.ref_name }} CI_PROJECT_DIR: /workspace + INSTANCES_DIR: /workspace/environments SECRET_KEY: ${{ secrets.SECRET_KEY }} ENVGENE_AGE_PUBLIC_KEY: ${{ secrets.ENVGENE_AGE_PUBLIC_KEY }} ENVGENE_AGE_PRIVATE_KEY: ${{ secrets.ENVGENE_AGE_PRIVATE_KEY }} @@ -331,7 +332,7 @@ jobs: - name: ENV_BUILD if: needs.process_environment_variables.outputs.ENV_BUILDER == 'true' run: | - docker run --rm -v ${{ github.workspace }}:${{ env.CI_PROJECT_DIR }} -w ${{ env.CI_PROJECT_DIR }} --env-file .env.container --user root -e HOME=/root \ + docker run --rm -v ${{ github.workspace }}:${{ env.CI_PROJECT_DIR }} -w ${{ env.CI_PROJECT_DIR }} -e COMMIT_ENV=true --env-file .env.container --user root -e HOME=/root \ ${{ env.DOCKER_IMAGE_NAME_ENVGENE }}:${{ env.DOCKER_IMAGE_TAG_ENVGENE }} \ bash -c " source /module/venv/bin/activate From 0bc66467834d449b2dbd246d4293437b95550eb5 Mon Sep 17 00:00:00 2001 From: Tesma Jose <113982972+tesmarishy@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:25:22 +0530 Subject: [PATCH 106/161] fix: ensure availability of repo for get passport job. (#1175) --- build_pipegene/scripts/gitlab_ci.py | 32 +++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/build_pipegene/scripts/gitlab_ci.py b/build_pipegene/scripts/gitlab_ci.py index e33e2d1f3..4c3ea90aa 100644 --- a/build_pipegene/scripts/gitlab_ci.py +++ b/build_pipegene/scripts/gitlab_ci.py @@ -3,7 +3,7 @@ from envgenehelper import logger, get_cluster_name_from_full_name, get_environment_name_from_full_name from envgenehelper.plugin_engine import PluginEngine -from gcip import JobFilter, Pipeline +from gcip import JobFilter, Pipeline, TriggerJob import pipeline_helper from appregdef_render_job import prepare_appregdef_render_job @@ -81,9 +81,9 @@ def build_pipeline(params: dict) -> None: environment_name = get_environment_name_from_full_name(full_env_name) job_sequence = [ - "bg_manage_job", "trigger_passport_job", "get_passport_job", + "bg_manage_job", "env_inventory_generation_job", "credential_rotation_job", "appregdef_render_job", @@ -93,11 +93,6 @@ def build_pipeline(params: dict) -> None: "git_commit_job" ] - if not params.get('BG_MANAGE', None): - logger.info(f'Preparing of bg_manage job for environment {full_env_name} is skipped.') - else: - jobs_map['bg_manage_job'] = prepare_bg_manage_job(pipeline, full_env_name, tags) - # get passport job if it is not already added for cluster if params['GET_PASSPORT'] and cluster_name not in get_passport_jobs: jobs_map["trigger_passport_job"] = prepare_trigger_passport_job(pipeline, full_env_name) @@ -107,6 +102,11 @@ def build_pipeline(params: dict) -> None: else: logger.info(f"Generation of cloud passport for environment '{full_env_name}' is skipped") + if not params.get('BG_MANAGE', None): + logger.info(f'Preparing of bg_manage job for environment {full_env_name} is skipped.') + else: + jobs_map['bg_manage_job'] = prepare_bg_manage_job(pipeline, full_env_name, tags) + if is_inventory_generation_needed(params['IS_TEMPLATE_TEST'], params): jobs_map["env_inventory_generation_job"] = prepare_inventory_generation_job(pipeline, full_env_name, environment_name, cluster_name, @@ -205,8 +205,22 @@ def build_pipeline(params: dict) -> None: 'tmp/' ) - is_first_job = job.needs is None or len(job.needs) == 0 - if not is_first_job: + if not do_checkout(job): job.add_variables(GIT_STRATEGY="empty") sorted_pipeline.write_yaml() + +def is_trigger_job(job): + return isinstance(job, TriggerJob) + + +def do_checkout(job): + is_first_job = job.needs is None or len(job.needs) == 0 + if is_first_job or any(is_trigger_job(need) for need in job.needs): + logger.info( + f"Enabling checkout for {job.name} " + f"Stage: {job.stage}, Needs: {job.needs}" + ) + return True + + return False \ No newline at end of file From 7482105403739059d4af2fa97bb7f6c177473862 Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:00:17 +0300 Subject: [PATCH 107/161] feat: Add Google Artifact Registry to Github Instance Pipe (#1163) --- docs/envgene-repository-variables.md | 28 + docs/how-to/docker-registry-configuration.md | 168 ++++ .../.github/docs/README.md | 783 ++++++++++++++++-- .../docs}/assets/envgene-workflow-header.png | Bin .../.github/workflows/Envgene.yml | 11 + .../instance-repo-pipeline/README.md | 673 --------------- 6 files changed, 908 insertions(+), 755 deletions(-) create mode 100644 docs/how-to/docker-registry-configuration.md rename github_workflows/instance-repo-pipeline/{ => .github/docs}/assets/envgene-workflow-header.png (100%) delete mode 100644 github_workflows/instance-repo-pipeline/README.md diff --git a/docs/envgene-repository-variables.md b/docs/envgene-repository-variables.md index 2e3506e0c..f47da8214 100644 --- a/docs/envgene-repository-variables.md +++ b/docs/envgene-repository-variables.md @@ -15,6 +15,8 @@ - [`GH_RUNNER_SCRIPT_TIMEOUT`](#gh_runner_script_timeout) - [`CALCULATOR_CLI_JAVA_OPTIONS`](#calculator_cli_java_options) - [`DOCKER_REGISTRY` (in instance repository)](#docker_registry-in-instance-repository) + - [`DOCKER_CLOUD_REGISTRY_PROVIDER`](#docker_cloud_registry_provider) + - [`GCP_SA_KEY`](#gcp_sa_key) - [Template EnvGene Repository](#template-envgene-repository) - [`ENV_TEMPLATE_TEST`](#env_template_test) - [`ENVGENE_LOG_LEVEL` (in template repository)](#envgene_log_level-in-template-repository) @@ -166,6 +168,32 @@ CALCULATOR_CLI_JAVA_OPTIONS="-Djava.util.concurrent.ForkJoinPool.common.parallel **Example**: `registry.example.com/docker` +### `DOCKER_CLOUD_REGISTRY_PROVIDER` + +**Description**: Cloud provider for Docker registry authentication when pulling EnvGene Docker images. Currently, the only supported value is `GCP`. When set to `GCP`, the GitHub workflow authenticates to Google Artifact Registry (GAR) before pulling EnvGene images. Used together with [`DOCKER_REGISTRY`](#docker_registry-in-instance-repository) and [`GCP_SA_KEY`](#gcp_sa_key). + +**Default Value**: None + +**Mandatory**: No + +**Allowed Values**: `GCP` (only) + +**Example**: `GCP` + +**Note**: This parameter is used only in the GitHub EnvGene pipeline. For GitLab, use runner-level configuration. See [Docker Registry Configuration](/docs/how-to/docker-registry-configuration.md) for details. + +### `GCP_SA_KEY` + +**Description**: Full JSON content of the GCP service account key. Used for authenticating to Google Artifact Registry (GAR) when pulling EnvGene Docker images. Required only when [`DOCKER_CLOUD_REGISTRY_PROVIDER`](#docker_cloud_registry_provider) is set to `GCP`. + +**Default Value**: None + +**Mandatory**: No (required only for GAR authentication) + +**Example**: `{"type":"service_account","project_id":"...",...}` + +**Note**: Store as a secret (GitHub Actions Secrets) or masked variable. Never commit to the repository. Use a service account with at least `Artifact Registry Reader` role. See [Docker Registry Configuration](/docs/how-to/docker-registry-configuration.md) for details. + ## Template EnvGene Repository ### `ENV_TEMPLATE_TEST` diff --git a/docs/how-to/docker-registry-configuration.md b/docs/how-to/docker-registry-configuration.md new file mode 100644 index 000000000..7628d7d1b --- /dev/null +++ b/docs/how-to/docker-registry-configuration.md @@ -0,0 +1,168 @@ +# Using Docker Registries in EnvGene GitHub Workflow + +- [Using Docker Registries in EnvGene GitHub Workflow](#using-docker-registries-in-envgene-github-workflow) + - [Description](#description) + - [Supported Registries](#supported-registries) + - [Prerequisites](#prerequisites) + - [How the Workflow Uses the Registry](#how-the-workflow-uses-the-registry) + - [Option 1: GitHub Container Registry (GHCR)](#option-1-github-container-registry-ghcr) + - [GHCR Configuration](#ghcr-configuration) + - [GHCR Authentication](#ghcr-authentication) + - [Option 2: Google Artifact Registry (GAR)](#option-2-google-artifact-registry-gar) + - [GAR Prerequisites](#gar-prerequisites) + - [Step 1: Configure GitHub Repository Variables](#step-1-configure-github-repository-variables) + - [Step 2: Add GCP\_SA\_KEY Secret](#step-2-add-gcp_sa_key-secret) + - [GAR Authentication Flow](#gar-authentication-flow) + - [Parameter Reference](#parameter-reference) + - [Switching Between Registries](#switching-between-registries) + - [Troubleshooting](#troubleshooting) + +## Description + +This guide explains how to configure the EnvGene GitHub workflow to pull Docker images from different registries. The workflow uses three EnvGene images: `qubership-envgene`, `qubership-pipegene`, and `qubership-effective-set-generator`. By default, these images are pulled from GitHub Container Registry (GHCR). You can switch to Google Artifact Registry (GAR) by configuring the appropriate variables and secrets. + +## Supported Registries + +The EnvGene GitHub workflow currently supports two Docker registries: + +- **GitHub Container Registry (GHCR)** - Default option, no additional configuration required +- **Google Artifact Registry (GAR)** - Requires GCP service account configuration + +> [!NOTE] +> For other registry types (AWS ECR, Azure ACR, custom registries), custom authentication steps need to be added to the workflow. + +## Prerequisites + +- Instance repository with the EnvGene GitHub workflow installed +- Access to **Settings → Secrets and variables → Actions** in your GitHub repository + +## How the Workflow Uses the Registry + +The workflow defines image names in the `env` section of `Envgene.yml`: + +```yaml +env: + DOCKER_IMAGE_NAME_ENVGENE: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-envgene" + DOCKER_IMAGE_NAME_PIPEGENE: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-pipegene" + DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" +``` + +The `DOCKER_REGISTRY` variable (from repository variables) determines the registry base. When `DOCKER_CLOUD_REGISTRY_PROVIDER` is set to `GCP`, the workflow runs an authentication step before pulling images from GAR. + +## Option 1: GitHub Container Registry (GHCR) + +GHCR is the default registry. No additional configuration is required if your images are hosted at `ghcr.io/netcracker`. + +### GHCR Configuration + +| Where to configure | Parameter | Value | +|--------------------------|-------------------|----------------------| +| **Settings → Variables** | `DOCKER_REGISTRY` | `ghcr.io/netcracker` | + +If you omit `DOCKER_REGISTRY`, the workflow uses `ghcr.io/netcracker` by default. + +### GHCR Authentication + +GitHub Actions automatically authenticates to `ghcr.io` using `GITHUB_TOKEN` when pulling images. No extra secrets are needed. Ensure your repository has access to the container images (e.g. via package permissions if the images are in a different organization). + +## Option 2: Google Artifact Registry (GAR) + +To use Google Artifact Registry, you must configure the registry URL, set the cloud provider, and provide a GCP service account key for authentication. + +### GAR Prerequisites + +- Registry URL (path to your GAR repository) +- GCP service account JSON key with access to the registry + +### Step 1: Configure GitHub Repository Variables + +1. Go to your repository on GitHub. +2. Open **Settings → Secrets and variables → Actions**. +3. Open the **Variables** tab. +4. Add or edit the following variables: + +| Variable | Value | +|----------------------------------|----------------------------------------------| +| `DOCKER_REGISTRY` | `REGION-docker.pkg.dev/PROJECT_ID/REPO_NAME` | +| `DOCKER_CLOUD_REGISTRY_PROVIDER` | `GCP` | + +**Example for `DOCKER_REGISTRY`:** + +```text +europe-west1-docker.pkg.dev/my-gcp-project/envgene-images +``` + +Replace: + +- `REGION` with your GAR region (e.g. `europe-west1`, `us-central1`) +- `PROJECT_ID` with your GCP project ID +- `REPO_NAME` with your Artifact Registry repository name + +### Step 2: Add GCP_SA_KEY Secret + +1. In **Settings → Secrets and variables → Actions**, open the **Secrets** tab. +2. Click **New repository secret**. +3. Name: `GCP_SA_KEY`. +4. Value: Paste the full JSON content of the GCP service account key (including `{` and `}`). +5. Click **Add secret**. + +> [!IMPORTANT] +> The secret must contain the full JSON. Do not truncate or modify it. The workflow uses it with `docker login -u _json_key --password-stdin`. + +### GAR Authentication Flow + +When `DOCKER_CLOUD_REGISTRY_PROVIDER` is set to `GCP`, the workflow runs this step in the `envgene_execution` job: + +```yaml +- name: Authenticate to GAR (Google Artifact Registry) + if: needs.process_environment_variables.outputs.DOCKER_CLOUD_REGISTRY_PROVIDER == 'GCP' + run: | + REGISTRY_HOST=$(echo "${{ vars.DOCKER_REGISTRY }}" | cut -d'/' -f1) + echo '${{ secrets.GCP_SA_KEY }}' | docker login -u _json_key --password-stdin "$REGISTRY_HOST" +``` + +The step extracts the registry host (e.g. `europe-west1-docker.pkg.dev`) from `DOCKER_REGISTRY` and authenticates before any Docker image pulls. + +## Parameter Reference + +| Parameter | Location | Required for GHCR | Required for GAR | +|----------------------------------|-----------|-----------------------|------------------------| +| `DOCKER_REGISTRY` | Variables | No (uses default) | Yes | +| `DOCKER_CLOUD_REGISTRY_PROVIDER` | Variables | No | Yes - set to `GCP` | +| `GCP_SA_KEY` | Secrets | No | Yes | + +> [!NOTE] +> For GHCR: If you omit `DOCKER_REGISTRY`, the workflow uses the default value `ghcr.io/netcracker`. Set `DOCKER_REGISTRY` only if your images are in a different GHCR organization or path. + +## Switching Between Registries + +To switch from GHCR to GAR: + +1. Add `DOCKER_REGISTRY` and `DOCKER_CLOUD_REGISTRY_PROVIDER` variables. +2. Add `GCP_SA_KEY` secret. +3. Trigger the workflow. The GAR authentication step will run automatically. + +To switch back to GHCR: + +1. Remove or clear `DOCKER_CLOUD_REGISTRY_PROVIDER` (or set it to empty). +2. Set `DOCKER_REGISTRY` to `ghcr.io/netcracker` (or remove it to use the default). +3. Optionally remove `GCP_SA_KEY` if no longer needed. + +## Troubleshooting + +**Authentication fails with "unauthorized" or "denied":** + +- Verify `GCP_SA_KEY` contains the full JSON key. +- Ensure the service account has `Artifact Registry Reader` role on the repository. +- Check that the key has not expired. + +**Image pull fails with "not found":** + +- Verify `DOCKER_REGISTRY` format: `REGION-docker.pkg.dev/PROJECT_ID/REPO_NAME`. +- Ensure the images exist in the repository with the expected names: `qubership-envgene`, `qubership-pipegene`, `qubership-effective-set-generator`. +- Check the image tags match those in the workflow (e.g. `1.31.9`). + +**GAR authentication step does not run:** + +- Confirm `DOCKER_CLOUD_REGISTRY_PROVIDER` is set to `GCP` (case-sensitive). +- Variables are passed via `process_environment_variables` job outputs. Ensure the variable is not overridden in `config.env` or `pipeline_vars.env` with an empty value. diff --git a/github_workflows/instance-repo-pipeline/.github/docs/README.md b/github_workflows/instance-repo-pipeline/.github/docs/README.md index 44ae3616c..2f45354d1 100644 --- a/github_workflows/instance-repo-pipeline/.github/docs/README.md +++ b/github_workflows/instance-repo-pipeline/.github/docs/README.md @@ -1,133 +1,752 @@ -# EnvGene GitHub Pipeline Usage Guide +# EnvGene GitHub Workflow -- [EnvGene GitHub Pipeline Usage Guide](#envgene-github-pipeline-usage-guide) +

+ +User Guide + +[![GitHub Actions](https://img.shields.io/badge/GitHub_Actions-workflow_dispatch-2088FF?style=flat-square&logo=github-actions&logoColor=white)](https://docs.github.com/en/actions) +[![Manual Trigger](https://img.shields.io/badge/trigger-manual-orange?style=flat-square)](#how-to-trigger-the-workflow) + +
+ +![EnvGene Workflow](assets/envgene-workflow-header.png) + +- [EnvGene GitHub Workflow](#envgene-github-workflow) - [Overview](#overview) - - [Available Parameters](#available-parameters) - - [How to Trigger the Pipeline](#how-to-trigger-the-pipeline) - - [Via GitHub Actions UI](#via-github-actions-ui) - - [Via GitHub API Call](#via-github-api-call) + - [Installation](#installation) + - [Prerequisites](#prerequisites) + - [Step 1: Copy the Pipeline](#step-1-copy-the-pipeline) + - [Step 2: Configure Required Secrets](#step-2-configure-required-secrets) + - [Step 3: Optional — Repository Variables](#step-3-optional--repository-variables) + - [Step 4: Optional — Customize Configuration](#step-4-optional--customize-configuration) + - [Verifying the Setup](#verifying-the-setup) + - [Quick Start](#quick-start) + - [Workflow Structure](#workflow-structure) + - [Pipeline Steps](#pipeline-steps) + - [Job: `process_environment_variables`](#job-process_environment_variables) + - [Job: `envgene_execution` (runs per environment in matrix)](#job-envgene_execution-runs-per-environment-in-matrix) + - [Workflow Dispatch Inputs](#workflow-dispatch-inputs) + - [Understanding the 10-Input Limit](#understanding-the-10-input-limit) + - [Input Reference](#input-reference) + - [GH\_ADDITIONAL\_PARAMS — Passing Extra Parameters](#gh_additional_params--passing-extra-parameters) + - [What Is GH\_ADDITIONAL\_PARAMS?](#what-is-gh_additional_params) + - [Format and Syntax](#format-and-syntax) + - [Examples](#examples) + - [JSON Values and Escaping](#json-values-and-escaping) + - [When to Use pipeline\_vars.env Instead](#when-to-use-pipeline_varsenv-instead) + - [Adding New Parameters](#adding-new-parameters) + - [Option A: Add as Workflow Input (If Under the Limit)](#option-a-add-as-workflow-input-if-under-the-limit) + - [Option B: Use GH\_ADDITIONAL\_PARAMS](#option-b-use-gh_additional_params) + - [Option C: Use pipeline\_vars.env](#option-c-use-pipeline_varsenv) + - [Adding New Jobs and Conditional Execution](#adding-new-jobs-and-conditional-execution) + - [Step 1: Ensure the Variable Is Available](#step-1-ensure-the-variable-is-available) + - [Step 2: Expose the Variable as a Job Output](#step-2-expose-the-variable-as-a-job-output) + - [Step 3: Add the Job Step with an if Condition](#step-3-add-the-job-step-with-an-if-condition) + - [Complete Example: Adding a Custom Job](#complete-example-adding-a-custom-job) - [Parameter Priority](#parameter-priority) - - [`pipeline_vars.env`](#pipeline_varsenv) - - [Pipeline Customization](#pipeline-customization) - - [How to Get the Pipeline](#how-to-get-the-pipeline) + - [Repository Variables (vars)](#repository-variables-vars) + - [Variables Used by the Workflow](#variables-used-by-the-workflow) + - [How to Add Repository Variables](#how-to-add-repository-variables) + - [When Variables Are Empty or Missing](#when-variables-are-empty-or-missing) + - [Adding Custom Variables](#adding-custom-variables) + - [Using Different Docker Registries](#using-different-docker-registries) + - [GitHub Container Registry (GHCR)](#github-container-registry-ghcr) + - [Google Artifact Registry (GAR)](#google-artifact-registry-gar) + - [How to Trigger the Workflow](#how-to-trigger-the-workflow) + - [Via GitHub Actions UI](#via-github-actions-ui) + - [Via GitHub API](#via-github-api) + - [Directory Structure](#directory-structure) + - [Use Case Scenarios](#use-case-scenarios) + - [Scenario 1: Full Deployment (Environment Build + Effective Set)](#scenario-1-full-deployment-environment-build--effective-set) + - [Scenario 2: Environment Build Only (No Effective Set)](#scenario-2-environment-build-only-no-effective-set) + - [Scenario 3: Update Template Version and Rebuild](#scenario-3-update-template-version-and-rebuild) + - [Scenario 4: Blue-Green Operation](#scenario-4-blue-green-operation) + - [Scenario 5: Credential Rotation](#scenario-5-credential-rotation) + - [Scenario 6: Process Solution Descriptor from Artifact](#scenario-6-process-solution-descriptor-from-artifact) + - [Scenario 7: Generate New Environment Inventory](#scenario-7-generate-new-environment-inventory) + - [Scenario 8: Multiple Environments in One Run](#scenario-8-multiple-environments-in-one-run) + - [Further Reading](#further-reading) ## Overview -The EnvGene pipeline (`Envgene.yaml`) is a GitHub Actions workflow that supports both manual UI triggers and API calls. It provides the same full EnvGene functionality as the GitLab pipeline. +The **EnvGene** workflow (`Envgene.yml`) is a GitHub Actions pipeline that automates environment generation, configuration, and deployment for the EnvGene platform. It provides the same functionality as the GitLab-based instance pipeline, adapted for GitHub Actions. + +> [!NOTE] +> The workflow is **manually triggered only** (`workflow_dispatch`). There is no automatic trigger on push or pull request. + +The workflow supports: + +- Environment inventory generation +- Application and registry definition processing +- Solution Descriptor (SD) processing +- Environment build +- Effective Set generation +- Blue-Green management +- Credential rotation +- Git commit of generated artifacts + +--- + +## Installation + +This section describes what you need to set up the EnvGene workflow in your instance repository. + +### Prerequisites + +- A GitHub repository (instance repository) with the [EnvGene instance structure](/docs/samples/instance-repository/) +- GitHub Actions enabled for the repository +- GitHub-hosted runners (or self-hosted runners with Docker available) + +### Step 1: Copy the Pipeline + +Copy the `.github` directory from this folder to the root of your instance repository: + +```bash +cp -r github_workflows/instance-repo-pipeline/.github /path/to/your/instance-repo/ +``` + +The copied structure includes the workflow, scripts, configuration files, and the `load-env-files` action. + +### Step 2: Configure Required Secrets + +Go to **Settings** → **Secrets and variables** → **Actions** → **Secrets**, and add: + +| Secret | Required | Description | +|---------------------------|-------------------|---------------------------------------------------------------------| +| `SECRET_KEY` | When using Fernet | Fernet key for credential encryption | +| `ENVGENE_AGE_PUBLIC_KEY` | When using SOPS | Public key from EnvGene AGE key pair (SOPS encryption) | +| `ENVGENE_AGE_PRIVATE_KEY` | When using SOPS | Private key from EnvGene AGE key pair (SOPS decryption) | +| `GH_ACCESS_TOKEN` | Yes | GitHub token with `contents: write` to commit changes to repository | +| `GCP_SA_KEY` | When using GAR | Full JSON key of GCP service account for Artifact Registry access | + +> [!NOTE] +> At least one encryption method (Fernet or SOPS) must be configured if your repository uses encrypted credentials. See [Credential Encryption](/docs/how-to/credential-encryption.md) for details. + +### Step 3: Optional — Repository Variables + +Configure variables in **Settings** → **Secrets and variables** → **Actions** → **Variables** to override defaults: + +| Variable | Default | Purpose | +|----------------------------|----------------------|-----------------------------------------------| +| `DOCKER_REGISTRY` | `ghcr.io/netcracker` | Docker registry for EnvGene images | +| `GH_RUNNER_TAG_NAME` | `ubuntu-22.04` | Runner label for workflow jobs | +| `GH_RUNNER_SCRIPT_TIMEOUT` | `10` | Job timeout in minutes | + +See [Repository Variables (vars)](#repository-variables-vars) for details. For a step-by-step guide on GHCR and GAR, see [Using Different Docker Registries in Envgene.yml](/docs/how-to/docker-registry-configuration.md). + +### Step 4: Optional — Customize Configuration + +- **`.github/configuration/config.env`** — Base pipeline configuration (e.g. `CI_PROJECT_DIR`, `GITHUB_USER_*`). Edit if you need different defaults. +- **`.github/pipeline_vars.env`** — Override pipeline parameters for debugging or recurring runs. Leave empty or add variables as needed. + +### Verifying the Setup + +1. Ensure the workflow file is at `.github/workflows/Envgene.yml`. +1. Ensure required secrets are set. +1. Trigger the workflow manually (see [Quick Start](#quick-start)) with a valid `ENV_NAMES` value. + +For initializing a new instance repository from scratch, see [Environment Instance Repository Installation Guide](/docs/how-to/envgene-maitanance.md). + +## Quick Start + +> [!TIP] +> New to EnvGene? Start with [Installation](#installation), then come back here. + +1. Ensure the pipeline is installed (see [Installation](#installation)). +1. Go to **Actions** → **EnvGene Execution** → **Run workflow**. +1. Fill in **ENV_NAMES** (e.g. `cluster-01/env-01`) and any other parameters. +1. Click **Run workflow**. + +## Workflow Structure + +The workflow consists of two main jobs: + +| Job | Purpose | +|---------------------------------|-----------------------------------------------------------------| +| `process_environment_variables` | Parses inputs, loads config, builds matrix, exports variables | +| `envgene_execution` | Runs EnvGene steps per environment (matrix job) | + +The first job prepares all parameters and passes them to the second job via a shared `.env` artifact and job outputs. The second job runs once per environment in the matrix. + +### Pipeline Steps + +The following sections describe each step in the pipeline as defined in `Envgene.yml`. Steps marked as *conditional* run only when their condition is met. + +#### Job: `process_environment_variables` + +| Step | Description | +|---------------------------------|--------------------------------------------------------------| +| Repository Checkout | Checks out the repository (without persisting credentials) | +| Load environment variables | Loads `config.env` and `pipeline_vars.env` into `GITHUB_ENV` | +| Process Input Parameters | Exports workflow inputs to environment | +| Process additional variables | Parses `GH_ADDITIONAL_PARAMS` and adds to environment | +| Create env_generation_params | Builds `ENV_GENERATION_PARAMS` JSON from SD/ENV variables | +| Multiple Environment Processing | Generates environment matrix from `ENV_NAMES` | +| Create .env file | Dumps all environment variables to `.env` | +| Upload .env as artifact | Uploads `.env` for use by `envgene_execution` job | + +#### Job: `envgene_execution` (runs per environment in matrix) + +| Step | Condition | Description | +|--------------------------------|---------------------------------------------------------------------------------|--------------------------------------------------------------| +| Repository Checkout | Always | Checks out repository with full history | +| Download environment-file | Always | Downloads `.env` artifact from previous job | +| Prepare environment | Always | Restores env vars, sets `PACKAGE_NAME`, extracts cluster/env | +| Create name for dynamic secret | Always | Sets `SECRET_NAME` for cluster-specific secrets | +| Create env file for container | Always | Exports env to `.env.container` for Docker steps | +| **BG_MANAGE** | `BG_MANAGE == 'true'` | Blue-Green operations: state management, validation | +| **ENV_INVENTORY_GENERATION** | One of: `ENV_INVENTORY_CONTENT`, `ENV_SPECIFIC_PARAMS`, `ENV_TEMPLATE_NAME` set | Generates Environment Inventory | +| **CREDENTIAL_ROTATION** | `CRED_ROTATION_PAYLOAD` not empty | Rotates credentials per payload | +| **APP_REG_DEF_PROCESS** | `ENV_BUILDER == 'true'` | Sets template version, renders App/Reg definitions | +| **PROCESS_SD** | `SD_SOURCE_TYPE` + `SD_DATA` or `SD_VERSION` | Processes Solution Descriptor | +| **ENV_BUILD** | `ENV_BUILDER == 'true'` | Generates Environment Instance from templates | +| **GENERATE_EFFECTIVE_SET** | `GENERATE_EFFECTIVE_SET == 'true'` | Generates Effective Set (SBOMs, validation, artifacts) | +| **GIT_COMMIT** | Always | Commits changes to repository | + +Each conditional step (in **bold**) also uploads its output as an artifact. The `GIT_COMMIT` step always runs at the end of the pipeline. + +## Workflow Dispatch Inputs + +### Understanding the 10-Input Limit + +> [!IMPORTANT] +> GitHub Actions limits `workflow_dispatch` to **10 input parameters**. The EnvGene pipeline uses 9 of them for the most common parameters. The 10th slot is reserved for `GH_ADDITIONAL_PARAMS`, which acts as a container for all other parameters. + +This design lets you pass any number of additional parameters without hitting the limit. + +### Input Reference + +| Input | Required | Default | Type | Description | +|--------------------------|----------|-----------|--------|--------------------------------------------------------| +| `ENV_NAMES` | Yes | — | string | Environment(s) to process. Format: `cluster/env` | +| `DEPLOYMENT_TICKET_ID` | No | `""` | string | Ticket ID used as commit message prefix | +| `ENV_TEMPLATE_VERSION` | No | `""` | string | Template version to apply (e.g. `env-template:v1.2.3`) | +| `ENV_BUILDER` | No | `"true"` | choice | Enable environment build | +| `GENERATE_EFFECTIVE_SET` | No | `"false"` | choice | Enable Effective Set generation | +| `GET_PASSPORT` | No | `"false"` | choice | Enable Cloud Passport discovery | +| `CMDB_IMPORT` | No | `"false"` | choice | Enable CMDB export | +| `GH_ADDITIONAL_PARAMS` | No | `""` | string | Comma-separated key-value pairs for other parameters | + +For full parameter semantics, see [Instance Pipeline Parameters](/docs/instance-pipeline-parameters.md). + +## GH_ADDITIONAL_PARAMS — Passing Extra Parameters + +### What Is GH_ADDITIONAL_PARAMS? + +`GH_ADDITIONAL_PARAMS` is a single string input that carries all pipeline parameters that are not exposed as separate workflow inputs. It is parsed by `.github/scripts/process_additional_variables.sh`, which adds each `KEY=VALUE` pair to the workflow environment. + +Use it for parameters such as: + +- `BG_MANAGE`, `BG_STATE` — Blue-Green operations +- `SD_SOURCE_TYPE`, `SD_VERSION`, `SD_DATA` — Solution Descriptor +- `ENV_SPECIFIC_PARAMS`, `ENV_TEMPLATE_NAME` — Environment configuration +- `EFFECTIVE_SET_CONFIG` — Effective Set options +- `CRED_ROTATION_PAYLOAD` — Credential rotation +- Any other parameter from [Instance Pipeline Parameters](/docs/instance-pipeline-parameters.md) -## Available Parameters +### Format and Syntax -GitHub's UI limits manual inputs to 10 parameters. To handle this limitation while maintaining full functionality, we expose the most frequently used parameters directly in the UI and group the remaining parameters within the [GH_ADDITIONAL_PARAMS](/docs/instance-pipeline-parameters.md#gh_additional_params) parameter. +**Format:** `KEY1=VALUE1,KEY2=VALUE2,KEY3=VALUE3` -Only a limited number of core parameters are available in the GitHub version of the pipeline: +**Rules:** -- [ENV_NAMES](/docs/instance-pipeline-parameters.md#env_names) -- [DEPLOYMENT_TICKET_ID](/docs/instance-pipeline-parameters.md#deployment_ticket_id) -- [ENV_TEMPLATE_VERSION](/docs/instance-pipeline-parameters.md#env_template_version) -- [ENV_BUILDER](/docs/instance-pipeline-parameters.md#env_builder) -- [GENERATE_EFFECTIVE_SET](/docs/instance-pipeline-parameters.md#generate_effective_set) -- [GET_PASSPORT](/docs/instance-pipeline-parameters.md#get_passport) -- [CMDB_IMPORT](/docs/instance-pipeline-parameters.md#cmdb_import) -- [GH_ADDITIONAL_PARAMS](/docs/instance-pipeline-parameters.md#gh_additional_params) +- Pairs are separated by commas. +- Each pair is `KEY=VALUE` (no spaces around `=`). +- Keys and values are trimmed of leading/trailing whitespace. +- Empty pairs are ignored. + +### Examples + +**Simple values:** + +```text +BG_MANAGE=true,SD_SOURCE_TYPE=artifact,SD_VERSION=my-app:v1.0 +``` + +**With JSON (escape double quotes):** + +```text +EFFECTIVE_SET_CONFIG={\"version\": \"v2.0\", \"app_chart_validation\": \"false\"} +``` + +**Multiple parameters:** + +```text +SD_SOURCE_TYPE=json,SD_DATA=[{\"version\":2.1,\"type\":\"solutionDeploy\"}],ENV_SPECIFIC_PARAMS={\"tenantName\":\"my-tenant\"} +``` + +**Blue-Green state:** + +```text +BG_MANAGE=true,BG_STATE={\"controllerNamespace\":\"bss-controller\",\"originNamespace\":{\"name\":\"bss-origin\",\"state\":\"active\"}} +``` + +### JSON Values and Escaping + +For JSON values: + +1. Escape internal double quotes: `\"` instead of `"`. +1. Be aware that commas inside JSON are used as pair separators. If your JSON contains commas, the parser may split it incorrectly. + +> [!CAUTION] +> **Workaround for complex JSON:** Use `pipeline_vars.env` (see below) or pass the parameter via the GitHub API with proper escaping. Commas inside JSON values may cause incorrect parsing. + +### When to Use pipeline_vars.env Instead + +Use `.github/pipeline_vars.env` when: + +- You have complex JSON with many commas. +- You want to keep sensitive or long values out of the UI. +- You need the same values across many runs (e.g. for debugging). + +Variables in `pipeline_vars.env` must be in standard `KEY=VALUE` format. Do **not** wrap them in `GH_ADDITIONAL_PARAMS`. + +## Adding New Parameters + +### Option A: Add as Workflow Input (If Under the Limit) + +If you have fewer than 10 inputs and want a dedicated UI field: + +1. Add the input under `on.workflow_dispatch.inputs` in `Envgene.yml`: + +```yaml +on: + workflow_dispatch: + inputs: + # ... existing inputs ... + MY_NEW_PARAM: + required: false + default: "" + type: string + description: "Description of the parameter" +``` + +1. Add a line in the "Process Input Parameters" step to export it: + +```yaml +echo "MY_NEW_PARAM=${{ github.event.inputs.MY_NEW_PARAM }}" >> $GITHUB_ENV +``` + +1. If the parameter controls job execution, add it to `process_environment_variables.outputs` (see [Adding New Jobs](#adding-new-jobs-and-conditional-execution)). + +### Option B: Use GH_ADDITIONAL_PARAMS + +1. Pass the parameter in `GH_ADDITIONAL_PARAMS`, e.g. `MY_NEW_PARAM=value`. +1. It will be parsed and added to `GITHUB_ENV` automatically. +1. If you need it for conditional steps, add it to the job outputs (see below). + +### Option C: Use pipeline_vars.env + +1. Add the variable to `.github/pipeline_vars.env`: + +```text +MY_NEW_PARAM=my_value +``` + +1. It will be loaded by the `load-env-files` action. +1. If you need it for conditional steps, add it to the job outputs. + +## Adding New Jobs and Conditional Execution + +To add a new step that runs only when a parameter is set, follow these steps. + +### Step 1: Ensure the Variable Is Available + +The variable must be present in `GITHUB_ENV` after the `process_environment_variables` job. It can come from: + +- A workflow input (and the "Process Input Parameters" step) +- `GH_ADDITIONAL_PARAMS` (parsed by `process_additional_variables.sh`) +- `pipeline_vars.env` or `config.env` (loaded by `load-env-files`) + +### Step 2: Expose the Variable as a Job Output + +Add the variable to the `outputs` of `process_environment_variables` in `Envgene.yml`: + +```yaml +jobs: + process_environment_variables: + outputs: + env_matrix: ${{ steps.matrix-generator.outputs.env_matrix }} + # ... existing outputs ... + MY_NEW_FEATURE: ${{ env.MY_NEW_FEATURE }} +``` -The [GH_ADDITIONAL_PARAMS](/docs/instance-pipeline-parameters.md#gh_additional_params) parameter serves as a wrapper for all parameters except those listed above. This approach enables the transmission of all [Instance Pipeline parameters](/docs/instance-pipeline-parameters.md). +Without this, the next job cannot use it in `if` conditions. -## How to Trigger the Pipeline +### Step 3: Add the Job Step with an if Condition -The pipeline can be triggered in two ways: +Add your step inside the `envgene_execution` job with an `if`: + +```yaml +- name: MY_NEW_JOB + if: needs.process_environment_variables.outputs.MY_NEW_FEATURE == 'true' + run: | + # Your commands here +``` + +**Common condition patterns:** + +| Condition type | Example | +|---------------------|-----------------------------------------------------------------| +| Equals string | `needs.process_environment_variables.outputs.MY_VAR == 'true'` | +| Not empty | `needs.process_environment_variables.outputs.MY_VAR != ''` | +| Logical OR | `(condition1) \|\| (condition2)` | +| Logical AND | `(condition1) && (condition2)` | +| Multiple conditions | `outputs.ENV_BUILDER == 'true' && outputs.SD_VERSION != ''` | + +### Complete Example: Adding a Custom Job + +Assume you want a step that runs only when `RUN_CUSTOM_VALIDATION=true`. + +**1. Pass the parameter** via `GH_ADDITIONAL_PARAMS`: + +```text +RUN_CUSTOM_VALIDATION=true +``` + +**2. Add the output** in `Envgene.yml`: + +```yaml +process_environment_variables: + outputs: + # ... existing ... + RUN_CUSTOM_VALIDATION: ${{ env.RUN_CUSTOM_VALIDATION }} +``` + +**3. Add the step** in `envgene_execution`: + +```yaml +- name: CUSTOM_VALIDATION + if: needs.process_environment_variables.outputs.RUN_CUSTOM_VALIDATION == 'true' + run: | + echo "Running custom validation..." + # Your validation logic +``` + +## Parameter Priority + +When the same parameter is set in multiple places, the effective value is chosen by this order (highest first): + +1. Workflow input parameters (UI or API) +1. `pipeline_vars.env` +1. Repository variables (`vars`) +1. Organization variables + +## Repository Variables (vars) + +Repository variables are configured in **Settings → Secrets and variables → Actions → Variables** (repository-level) or at the organization level. They are referenced in the workflow as `vars.VARIABLE_NAME` and are available to all workflow runs. + +### Variables Used by the Workflow + +| Variable | Purpose | Default when empty | +|----------------------------------|------------------------------------------------|----------------------| +| `DOCKER_REGISTRY` | Docker registry base for EnvGene images | `ghcr.io/netcracker` | +| `DOCKER_CLOUD_REGISTRY_PROVIDER` | Cloud provider for registry auth (GCP for GAR) | (empty) | +| `GH_RUNNER_TAG_NAME` | Runner label for jobs (e.g. ubuntu-22.04) | `ubuntu-22.04` | +| `GH_RUNNER_SCRIPT_TIMEOUT` | Job timeout in minutes | `10` | + +### How to Add Repository Variables + +1. Go to your repository on GitHub. +1. Open **Settings** → **Secrets and variables** → **Actions**. +1. Open the **Variables** tab. +1. Click **New repository variable**. +1. Enter the name (e.g. `DOCKER_REGISTRY`) and value. +1. Click **Add variable**. + +### When Variables Are Empty or Missing + +The workflow uses fallback values when a variable is not set or is empty. For example: + +```yaml +runs-on: ${{ vars.GH_RUNNER_TAG_NAME || 'ubuntu-22.04' }} +DOCKER_IMAGE_NAME_ENVGENE: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-envgene" +timeout-minutes: ${{ fromJSON(vars.GH_RUNNER_SCRIPT_TIMEOUT || '10') }} +``` + +- If `vars.GH_RUNNER_TAG_NAME` is empty or missing → `ubuntu-22.04` is used. +- If `vars.DOCKER_REGISTRY` is empty or missing → `ghcr.io/netcracker` is used. +- If `vars.GH_RUNNER_SCRIPT_TIMEOUT` is empty or missing → `10` is used. + +> [!TIP] +> You do not need to define these variables for the workflow to run; defaults are applied automatically. + +### Adding Custom Variables + +To use your own variables in the workflow: + +1. Add the variable in **Settings → Secrets and variables → Actions → Variables**. +1. Reference it in `Envgene.yml` as `${{ vars.MY_CUSTOM_VAR }}`. +1. For optional variables with a default, use: `${{ vars.MY_CUSTOM_VAR || 'default_value' }}`. + +For a full list of supported repository variables, see [EnvGene Repository Variables](/docs/envgene-repository-variables.md). + +## Using Different Docker Registries + +The workflow pulls EnvGene Docker images (envgene, pipegene, effective-set-generator) from a registry. By default, images are pulled from GitHub Container Registry (GHCR). You can switch to another registry such as Google Artifact Registry (GAR) by configuring the appropriate variables and secrets. + +### GitHub Container Registry (GHCR) + +GHCR is the default registry. No additional configuration is required. + +| Where to configure | Parameter | Value | +|--------------------------|-------------------|--------------------------------| +| **Settings → Variables** | `DOCKER_REGISTRY` | `ghcr.io/netcracker` (default) | + +**Authentication:** GitHub Actions automatically authenticates to `ghcr.io` using `GITHUB_TOKEN` when pulling images. No extra secrets are needed. + +**Image names:** The workflow builds image paths as `$DOCKER_REGISTRY/qubership-envgene`, `$DOCKER_REGISTRY/qubership-pipegene`, etc. For GHCR, the full path is `ghcr.io/netcracker/qubership-envgene:1.31.9`. + +### Google Artifact Registry (GAR) + +To use Google Artifact Registry, configure the registry URL and GCP authentication. + +| Where to configure | Parameter | Value | +|--------------------------|----------------------------------|----------------------------------------------| +| **Settings → Variables** | `DOCKER_REGISTRY` | `REGION-docker.pkg.dev/PROJECT_ID/REPO_NAME` | +| **Settings → Variables** | `DOCKER_CLOUD_REGISTRY_PROVIDER` | `GCP` | +| **Settings → Secrets** | `GCP_SA_KEY` | Full JSON key of the GCP service account | + +**Example `DOCKER_REGISTRY` for GAR:** + +```text +europe-west1-docker.pkg.dev/my-gcp-project/envgene-images +``` + +**Authentication:** When `DOCKER_CLOUD_REGISTRY_PROVIDER` is set to `GCP`, the workflow runs a step that authenticates to GAR using `docker login` with the `_json_key` method. The `GCP_SA_KEY` secret must contain the full JSON key of a GCP service account that has `Artifact Registry Reader` (or equivalent) permissions. + +**How to set up GCP_SA_KEY:** + +1. Create a GCP service account with access to your Artifact Registry repository. +1. Create a JSON key for the service account (IAM → Service Accounts → Keys → Add Key). +1. Copy the entire JSON content. +1. In GitHub: **Settings → Secrets and variables → Actions → Secrets** → **New repository secret**. +1. Name: `GCP_SA_KEY`, Value: paste the full JSON. + +> [!IMPORTANT] +> The service account must have at least `Artifact Registry Reader` role on the repository. For private images, ensure the key is not expired and has the correct permissions. + +**Summary:** + +| Parameter | Location | Required for GAR | +|----------------------------------|-----------|---------------------------| +| `DOCKER_REGISTRY` | Variables | Yes - full GAR path | +| `DOCKER_CLOUD_REGISTRY_PROVIDER` | Variables | Yes - set to `GCP` | +| `GCP_SA_KEY` | Secrets | Yes - JSON key content | + +## How to Trigger the Workflow ### Via GitHub Actions UI -1. Go to your GitHub repository -2. Navigate to the **Actions** tab -3. Select the **EnvGene Execution** workflow -4. Click **Run workflow** -5. Fill in the required parameters -6. Click **Run workflow** +1. Open your repository on GitHub. +1. Go to **Actions**. +1. Select **EnvGene Execution**. +1. Click **Run workflow**. +1. Choose the branch, fill in parameters, and run. -### Via GitHub API Call +### Via GitHub API -Use the GitHub API to trigger the workflow: +
+Click to expand API example ```bash curl -X POST \ - -H "Authorization: token " \ + -H "Authorization: token " \ -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/repos///actions/workflows/Envgene.yaml/dispatches \ + https://api.github.com/repos///actions/workflows/Envgene.yml/dispatches \ -d '{ - "ref": "", + "ref": "main", "inputs": { - "": "", - "GH_ADDITIONAL_PARAMS": "KEY1=VALUE1,KEY2=VALUE2" + "ENV_NAMES": "cluster-01/env-01", + "ENV_BUILDER": "true", + "GENERATE_EFFECTIVE_SET": "true", + "DEPLOYMENT_TICKET_ID": "QBSHP-0001", + "GH_ADDITIONAL_PARAMS": "EFFECTIVE_SET_CONFIG={\"version\": \"v2.0\", \"app_chart_validation\": \"false\"}" } }' ``` -**Example:** +Replace ``, ``, ``, and `main` as needed. -```bash -curl -X POST \ - -H "Authorization: token token-placeholder-123" \ - -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/repos/qubership/instance-repo/actions/workflows/Envgene.yaml/dispatches \ - -d '{ - "ref": "main", - "inputs": { - "ENV_NAMES": "test-cluster/e01", - "ENV_BUILDER": "true", - "GENERATE_EFFECTIVE_SET": "true", - "DEPLOYMENT_TICKET_ID": "QBSHP-0001", - "GH_ADDITIONAL_PARAMS": "EFFECTIVE_SET_CONFIG={\"version\": \"v2.0\", \"app_chart_validation\": \"false\"}" - } - }' +
+ +## Directory Structure + +```text +instance-repo-pipeline/ +├── README.md # This file +└── .github/ + ├── actions/ + │ └── load-env-files/ # Loads .env files into GITHUB_ENV + ├── configuration/ + │ └── config.env # Base pipeline configuration + ├── docs/ + │ └── README.md # Additional usage notes + ├── scripts/ + │ ├── generate_env_matrix.sh # Builds environment matrix from ENV_NAMES + │ ├── process_additional_variables.sh # Parses GH_ADDITIONAL_PARAMS + │ ├── process_matrix_iteration.sh # Extracts cluster/env from matrix + │ └── create_env_generation_params.sh # Builds ENV_GENERATION_PARAMS JSON + ├── workflows/ + │ └── Envgene.yml # Main workflow definition + └── pipeline_vars.env # Optional overrides (template, often empty) ``` -## Parameter Priority +--- + +## Use Case Scenarios + +This section shows typical scenarios with example parameters and what happens when you run the workflow. + +### Scenario 1: Full Deployment (Environment Build + Effective Set) + +**Goal:** Build the environment and generate the Effective Set for deployment. + +| Parameter | Value | +|--------------------------|--------------------------| +| `ENV_NAMES` | `prod-cluster/prod-01` | +| `ENV_BUILDER` | `true` | +| `GENERATE_EFFECTIVE_SET` | `true` | +| `DEPLOYMENT_TICKET_ID` | `QBSHP-1234` | -The pipeline allows parameters to be defined in multiple locations, listed in descending order of priority: +**Steps that run:** APP_REG_DEF_PROCESS → ENV_BUILD → GENERATE_EFFECTIVE_SET → GIT_COMMIT -1. Pipeline execution input parameters -2. `pipeline_vars.env` file -3. Repository or repository group CI/CD variables +**Result:** Environment Instance is generated, Effective Set is created in `environments/prod-cluster/prod-01/effective-set/`, changes are committed to the repository. -Together, these form the complete context used by EnvGene. -When parameter keys from different sources overlap, their values are replaced according to the priority order specified above. +--- -Best practices for setting variables are: +### Scenario 2: Environment Build Only (No Effective Set) -- Define in CI/CD variables [EnvGene repository variables](https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-repository-variables.md) -- Pass through GitHub Actions UI or GitHub API Call [Instance Pipeline parameters](/docs/instance-pipeline-parameters.md) -- Use `pipeline_vars.env` for debug purposes +**Goal:** Regenerate the Environment Instance without generating the Effective Set (e.g. for validation or template updates). -## `pipeline_vars.env` +| Parameter | Value | +|----------------|------------------------| +| `ENV_NAMES` | `dev-cluster/dev-01` | +| `ENV_BUILDER` | `true` | -Variables in this file must be described in Dotenv format and follow POSIX shell variable syntax. +**Steps that run:** APP_REG_DEF_PROCESS → ENV_BUILD → GIT_COMMIT -For example: +**Result:** Environment Instance is regenerated and committed. GENERATE_EFFECTIVE_SET is skipped. + +--- + +### Scenario 3: Update Template Version and Rebuild + +**Goal:** Switch to a new template version and rebuild the environment. + +| Parameter | Value | +|------------------------|------------------------| +| `ENV_NAMES` | `prod-cluster/prod-01` | +| `ENV_BUILDER` | `true` | +| `ENV_TEMPLATE_VERSION` | `env-template:v2.1.0` | + +**Steps that run:** APP_REG_DEF_PROCESS (updates template version) → ENV_BUILD → GIT_COMMIT + +**Result:** `env_definition.yml` is updated with the new template version, environment is rebuilt with the new template, changes are committed. + +--- + +### Scenario 4: Blue-Green Operation + +**Goal:** Perform a Blue-Green operation (e.g. warmup, state change). + +| Parameter | Value | +|------------------------|-------------------------------------| +| `ENV_NAMES` | `prod-cluster/prod-01` | +| `GH_ADDITIONAL_PARAMS` | `BG_MANAGE=true,BG_STATE={...}` | + +**Example `GH_ADDITIONAL_PARAMS` value:** ```text -ENV_SPECIFIC_PARAMS={"clusterParams":{"clusterEndpoint":"","clusterToken":""},"additionalTemplateVariables":{"":""},"cloudName":"","envSpecificParamsets":{"":["paramsetA"],"cloud":["paramsetB"]},"paramsets":{"paramsetA":{"version":"","name":"","parameters":{"":""},"applications":[{"appName":"","parameters":{"":""}}]},"paramsetB":{"version":"","name":"","parameters":{"":""},"applications":[]}},"credentials":{"credX":{"type":"","data":{"username":"","password":""}},"credY":{"type":"","data":{"secret":""}}}} -EFFECTIVE_SET_CONFIG={\"version\": \"v2.0\", \"app_chart_validation\": \"false\"} +BG_MANAGE=true,BG_STATE={\"controllerNamespace\":\"bss-ctrl\",\"originNamespace\":{\"name\":\"bss-origin\",\"state\":\"ACTIVE\",\"version\":\"v1.0\"},\"peerNamespace\":{\"name\":\"bss-peer\",\"state\":\"CANDIDATE\",\"version\":\"v1.1\"},\"updateTime\":\"2024-01-15T10:00:00Z\"} ``` -Variables set in this file must NOT be wrapped with [GH_ADDITIONAL_PARAMS](/docs/instance-pipeline-parameters.md#gh_additional_params) +**Steps that run:** BG_MANAGE → GIT_COMMIT -## Pipeline Customization +**Result:** BG state is validated, state files are updated in the repository, namespace objects are copied if warmup. No ENV_BUILD or Effective Set. -Pipeline customization is only possible in a limited number of cases: +--- -- **Reducing** the number of input parameters in the UI -- Changing default values of input parameters in the UI -- Changing the mandatory status of input parameters in the UI +### Scenario 5: Credential Rotation -All these changes are made and described in the `Envgene.yaml` section: +**Goal:** Rotate credentials for an environment without rebuilding. -```yaml -on: - workflow_dispatch: - inputs: +| Parameter | Value | +|------------------------|-----------------------------------------| +| `ENV_NAMES` | `prod-cluster/prod-01` | +| `GH_ADDITIONAL_PARAMS` | `CRED_ROTATION_PAYLOAD={...}` | + +**Example `GH_ADDITIONAL_PARAMS` value:** + +```text +CRED_ROTATION_PAYLOAD={\"credentials\":[{\"name\":\"db-password\",\"newValue\":\"\"}]} ``` -## How to Get the Pipeline +**Steps that run:** CREDENTIAL_ROTATION → GIT_COMMIT + +**Result:** Credentials are updated per payload, changes are committed. See [Credential Rotation](/docs/features/cred-rotation.md) for full payload format. + +--- + +### Scenario 6: Process Solution Descriptor from Artifact + +**Goal:** Fetch SD from an artifact and merge it into the repository. + +| Parameter | Value | +|------------------------|-----------------------------------------------------------------| +| `ENV_NAMES` | `prod-cluster/prod-01` | +| `GH_ADDITIONAL_PARAMS` | `SD_SOURCE_TYPE=artifact,SD_VERSION=my-solution:v1.2.3,...` | + +**Steps that run:** PROCESS_SD → GIT_COMMIT + +**Result:** SD is downloaded from the artifact registry, merged (or replaced) into `environments/prod-cluster/prod-01/Inventory/solution-descriptor/sd.yaml`, committed. + +--- + +### Scenario 7: Generate New Environment Inventory + +**Goal:** Create a new Environment Inventory (`env_definition.yml`) for a new environment. + +| Parameter | Value | +|------------------------|-----------------------------------------------------------------| +| `ENV_NAMES` | `new-cluster/new-env` | +| `GH_ADDITIONAL_PARAMS` | `ENV_INVENTORY_INIT=true,ENV_TEMPLATE_NAME=my-env-template` | + +**Steps that run:** ENV_INVENTORY_GENERATION → GIT_COMMIT + +**Result:** New `env_definition.yml` is created at `environments/new-cluster/new-env/Inventory/`, committed. See [Environment Inventory Generation](/docs/features/env-inventory-generation.md). + +--- + +### Scenario 8: Multiple Environments in One Run + +**Goal:** Process several environments with the same parameters. + +| Parameter | Value | +|---------------|--------------------------------------------| +| `ENV_NAMES` | `cluster-01/env-01,cluster-01/env-02,...` | +| `ENV_BUILDER` | `true` | + +**Steps that run:** For each environment in the matrix: APP_REG_DEF_PROCESS → ENV_BUILD → GIT_COMMIT (parallel jobs) + +**Result:** Three separate `envgene_execution` jobs run in parallel, each processes one environment. All changes are committed in a single workflow run. + +--- + +## Further Reading + +| Document | Description | +|---------------------------------------------------------------------------------------|--------------------------------| +| [Instance Pipeline Parameters](/docs/instance-pipeline-parameters.md) | Full parameter reference | +| [EnvGene Pipelines](/docs/envgene-pipelines.md) | Pipeline flow and descriptions | +| [Using Different Docker Registries](/docs/how-to/docker-registry-configuration.md) | GHCR and GAR configuration | +| [Blue-Green Deployment](/docs/features/blue-green-deployment.md) | BG-related parameters | +| [SD Processing](/docs/use-cases/sd-processing.md) | Solution Descriptor use cases | + +--- + +
+ +EnvGene GitHub Workflow — Part of the Qubership EnvGene platform -To get the pipeline, you need to copy the [.github](/github_workflows/instance-repo-pipeline/.github) directory to the root of your GitHub repository +
diff --git a/github_workflows/instance-repo-pipeline/assets/envgene-workflow-header.png b/github_workflows/instance-repo-pipeline/.github/docs/assets/envgene-workflow-header.png similarity index 100% rename from github_workflows/instance-repo-pipeline/assets/envgene-workflow-header.png rename to github_workflows/instance-repo-pipeline/.github/docs/assets/envgene-workflow-header.png diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 371cf8cbe..de00a924e 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -63,6 +63,7 @@ env: ENVGENE_AGE_PUBLIC_KEY: ${{ secrets.ENVGENE_AGE_PUBLIC_KEY }} ENVGENE_AGE_PRIVATE_KEY: ${{ secrets.ENVGENE_AGE_PRIVATE_KEY }} GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }} + DOCKER_CLOUD_REGISTRY_PROVIDER: ${{ vars.DOCKER_CLOUD_REGISTRY_PROVIDER || '' }} #DOCKER_IMAGE_NAMES DOCKER_IMAGE_NAME_PIPEGENE: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-pipegene" @@ -92,6 +93,7 @@ jobs: SD_SOURCE_TYPE: ${{ env.SD_SOURCE_TYPE }} SD_DATA: ${{ env.SD_DATA }} SD_VERSION: ${{ env.SD_VERSION }} + DOCKER_CLOUD_REGISTRY_PROVIDER: ${{ env.DOCKER_CLOUD_REGISTRY_PROVIDER }} steps: - name: Repository Checkout uses: actions/checkout@v4.1.0 @@ -160,6 +162,7 @@ jobs: with: fetch-depth: 0 + ### ENV VARS PROCESSING - START ### - name: Download environment-file uses: actions/download-artifact@v4 with: @@ -188,6 +191,14 @@ jobs: - name: Create env file for container run: env > .env.container + - name: Authenticate to GAR (Google Artifact Registry) + if: needs.process_environment_variables.outputs.DOCKER_CLOUD_REGISTRY_PROVIDER == 'GCP' + run: | + REGISTRY_HOST=$(echo "${{ vars.DOCKER_REGISTRY }}" | cut -d'/' -f1) + echo '${{ secrets.GCP_SA_KEY }}' | docker login -u _json_key --password-stdin "$REGISTRY_HOST" + ### ENV VARS PROCESSING - END ### + + ### BG MANAGE ### - name: BG_MANAGE if: needs.process_environment_variables.outputs.BG_MANAGE == 'true' diff --git a/github_workflows/instance-repo-pipeline/README.md b/github_workflows/instance-repo-pipeline/README.md deleted file mode 100644 index 6c8a94158..000000000 --- a/github_workflows/instance-repo-pipeline/README.md +++ /dev/null @@ -1,673 +0,0 @@ -# EnvGene GitHub Workflow - -
- -User Guide - -[![GitHub Actions](https://img.shields.io/badge/GitHub_Actions-workflow_dispatch-2088FF?style=flat-square&logo=github-actions&logoColor=white)](https://docs.github.com/en/actions) -[![Manual Trigger](https://img.shields.io/badge/trigger-manual-orange?style=flat-square)](#how-to-trigger-the-workflow) - -
- -![EnvGene Workflow](assets/envgene-workflow-header.png) - -- [EnvGene GitHub Workflow — User Guide](#envgene-github-workflow) - - [Overview](#overview) - - [Installation](#installation) - - [Quick Start](#quick-start) - - [Workflow Structure](#workflow-structure) - - [Pipeline Steps](#pipeline-steps) - - [Workflow Dispatch Inputs](#workflow-dispatch-inputs) - - [Understanding the 10-Input Limit](#understanding-the-10-input-limit) - - [Input Reference](#input-reference) - - [GH_ADDITIONAL_PARAMS — Passing Extra Parameters](#gh_additional_params--passing-extra-parameters) - - [What Is GH_ADDITIONAL_PARAMS?](#what-is-gh_additional_params) - - [Format and Syntax](#format-and-syntax) - - [Examples](#examples) - - [JSON Values and Escaping](#json-values-and-escaping) - - [When to Use pipeline_vars.env Instead](#when-to-use-pipeline_varsenv-instead) - - [Adding New Parameters](#adding-new-parameters) - - [Option A: Add as Workflow Input (If Under the Limit)](#option-a-add-as-workflow-input-if-under-the-limit) - - [Option B: Use GH_ADDITIONAL_PARAMS](#option-b-use-gh_additional_params) - - [Option C: Use pipeline_vars.env](#option-c-use-pipeline_varsenv) - - [Adding New Jobs and Conditional Execution](#adding-new-jobs-and-conditional-execution) - - [Step 1: Ensure the Variable Is Available](#step-1-ensure-the-variable-is-available) - - [Step 2: Expose the Variable as a Job Output](#step-2-expose-the-variable-as-a-job-output) - - [Step 3: Add the Job Step with an if Condition](#step-3-add-the-job-step-with-an-if-condition) - - [Complete Example: Adding a Custom Job](#complete-example-adding-a-custom-job) - - [Parameter Priority](#parameter-priority) - - [Repository Variables (vars)](#repository-variables-vars) - - [How to Trigger the Workflow](#how-to-trigger-the-workflow) - - [Via GitHub Actions UI](#via-github-actions-ui) - - [Via GitHub API](#via-github-api) - - [Directory Structure](#directory-structure) - - [Use Case Scenarios](#use-case-scenarios) - - [Further Reading](#further-reading) - -## Overview - -The **EnvGene** workflow (`Envgene.yml`) is a GitHub Actions pipeline that automates environment generation, configuration, and deployment for the EnvGene platform. It provides the same functionality as the GitLab-based instance pipeline, adapted for GitHub Actions. - -> [!NOTE] -> The workflow is **manually triggered only** (`workflow_dispatch`). There is no automatic trigger on push or pull request. - -The workflow supports: - -- Environment inventory generation -- Application and registry definition processing -- Solution Descriptor (SD) processing -- Environment build -- Effective Set generation -- Blue-Green management -- Credential rotation -- Git commit of generated artifacts - ---- - -## Installation - -This section describes what you need to set up the EnvGene workflow in your instance repository. - -### Prerequisites - -- A GitHub repository (instance repository) with the [EnvGene instance structure](/docs/samples/instance-repository/) -- GitHub Actions enabled for the repository -- GitHub-hosted runners (or self-hosted runners with Docker available) - -### Step 1: Copy the Pipeline - -Copy the `.github` directory from this folder to the root of your instance repository: - -```bash -cp -r github_workflows/instance-repo-pipeline/.github /path/to/your/instance-repo/ -``` - -The copied structure includes the workflow, scripts, configuration files, and the `load-env-files` action. - -### Step 2: Configure Required Secrets - -Go to **Settings** → **Secrets and variables** → **Actions** → **Secrets**, and add: - -| Secret | Required | Description | -|---------------------------|-------------------|-----------------------------------------------------------------------| -| `SECRET_KEY` | When using Fernet | Fernet key for credential encryption | -| `ENVGENE_AGE_PUBLIC_KEY` | When using SOPS | Public key from EnvGene AGE key pair (SOPS encryption) | -| `ENVGENE_AGE_PRIVATE_KEY` | When using SOPS | Private key from EnvGene AGE key pair (SOPS decryption) | -| `GH_ACCESS_TOKEN` | Yes | GitHub token with `contents: write` to commit changes to repository | - -> [!NOTE] -> At least one encryption method (Fernet or SOPS) must be configured if your repository uses encrypted credentials. See [Credential Encryption](/docs/how-to/credential-encryption.md) for details. - -### Step 3: Optional — Repository Variables - -Configure variables in **Settings** → **Secrets and variables** → **Actions** → **Variables** to override defaults: - -| Variable | Default | Purpose | -|----------------------------|----------------------|---------------------------------------------------| -| `DOCKER_REGISTRY` | `ghcr.io/netcracker` | Docker registry for EnvGene images | -| `GH_RUNNER_TAG_NAME` | `ubuntu-22.04` | Runner label for workflow jobs | -| `GH_RUNNER_SCRIPT_TIMEOUT` | `10` | Job timeout in minutes | - -See [Repository Variables (vars)](#repository-variables-vars) for details. - -### Step 4: Optional — Customize Configuration - -- **`.github/configuration/config.env`** — Base pipeline configuration (e.g. `CI_PROJECT_DIR`, `GITHUB_USER_*`). Edit if you need different defaults. -- **`.github/pipeline_vars.env`** — Override pipeline parameters for debugging or recurring runs. Leave empty or add variables as needed. - -### Verifying the Setup - -1. Ensure the workflow file is at `.github/workflows/Envgene.yml`. -1. Ensure required secrets are set. -1. Trigger the workflow manually (see [Quick Start](#quick-start)) with a valid `ENV_NAMES` value. - -For initializing a new instance repository from scratch, see [Environment Instance Repository Installation Guide](/docs/how-to/envgene-maitanance.md). - -## Quick Start - -> [!TIP] -> New to EnvGene? Start with [Installation](#installation), then come back here. - -1. Ensure the pipeline is installed (see [Installation](#installation)). -1. Go to **Actions** → **EnvGene Execution** → **Run workflow**. -1. Fill in **ENV_NAMES** (e.g. `cluster-01/env-01`) and any other parameters. -1. Click **Run workflow**. - -## Workflow Structure - -The workflow consists of two main jobs: - -| Job | Purpose | -|----------------------------------|-------------------------------------------------------------------------| -| `process_environment_variables` | Parses inputs, loads config, builds matrix, exports variables | -| `envgene_execution` | Runs EnvGene steps per environment (matrix job) | - -The first job prepares all parameters and passes them to the second job via a shared `.env` artifact and job outputs. The second job runs once per environment in the matrix. - -### Pipeline Steps - -The following sections describe each step in the pipeline as defined in `Envgene.yml`. Steps marked as *conditional* run only when their condition is met. - -#### Job: `process_environment_variables` - -| Step | Description | -|------------------------------|-----------------------------------------------------------------------------| -| Repository Checkout | Checks out the repository (without persisting credentials) | -| Load environment variables | Loads `config.env` and `pipeline_vars.env` into `GITHUB_ENV` | -| Process Input Parameters | Exports workflow inputs (ENV_NAMES, ENV_BUILDER, etc.) to environment | -| Process additional variables | Parses `GH_ADDITIONAL_PARAMS` and adds each `KEY=VALUE` to environment | -| Create env_generation_params | Builds `ENV_GENERATION_PARAMS` JSON from SD/ENV_SPECIFIC_PARAMS variables | -| Multiple Environment Processing | Generates environment matrix from `ENV_NAMES` (comma/semicolon/space) | -| Create .env file | Dumps all environment variables to `.env` | -| Upload .env as artifact | Uploads `.env` for use by `envgene_execution` job | - -#### Job: `envgene_execution` (runs per environment in matrix) - -| Step | Condition | Description | -|-------------------------|------------------------------------------------|-----------------------------------------------------------------------------| -| Repository Checkout | Always | Checks out repository with full history | -| Download environment-file| Always | Downloads `.env` artifact from previous job | -| Prepare environment | Always | Restores env vars, sets `PACKAGE_NAME`, extracts `CLUSTER_NAME`/`ENV_NAME` | -| Create name for dynamic secret | Always | Sets `SECRET_NAME` for cluster-specific secrets | -| Create env file for container | Always | Exports env to `.env.container` for Docker steps | -| **BG_MANAGE** | `BG_MANAGE == 'true'` | Blue-Green operations: state management, Origin/Peer config, validation | -| **ENV_INVENTORY_GENERATION** | `ENV_INVENTORY_CONTENT` / `ENV_SPECIFIC_PARAMS` / `ENV_TEMPLATE_NAME` set | Generates Environment Inventory at `env_definition.yml` | -| **CREDENTIAL_ROTATION** | `CRED_ROTATION_PAYLOAD` not empty | Rotates credentials per payload | -| **APP_REG_DEF_PROCESS** | `ENV_BUILDER == 'true'` | Sets template version, renders App/Reg definitions, handles certs | -| **PROCESS_SD** | `SD_SOURCE_TYPE=json` + `SD_DATA` or `artifact` + `SD_VERSION` | Processes Solution Descriptor (JSON or artifact) | -| **ENV_BUILD** | `ENV_BUILDER == 'true'` | Main environment build: generates Environment Instance from templates | -| **GENERATE_EFFECTIVE_SET** | `GENERATE_EFFECTIVE_SET == 'true'` | Generates Effective Set (SBOMs, validation, deployment artifacts) | -| **GIT_COMMIT** | Always | Commits changes to repository, prepares artifacts for downstream use | - -Each conditional step (in **bold**) also uploads its output as an artifact. The `GIT_COMMIT` step always runs at the end of the pipeline. - -## Workflow Dispatch Inputs - -### Understanding the 10-Input Limit - -> [!IMPORTANT] -> GitHub Actions limits `workflow_dispatch` to **10 input parameters**. The EnvGene pipeline uses 9 of them for the most common parameters. The 10th slot is reserved for `GH_ADDITIONAL_PARAMS`, which acts as a container for all other parameters. - -This design lets you pass any number of additional parameters without hitting the limit. - -### Input Reference - -| Input | Required | Default | Type | Description | -|-------------------------|----------|-----------|--------|-------------------------------------------------------------------------| -| `ENV_NAMES` | Yes | — | string | Environment(s) to process. Format: `cluster/env` or comma-separated | -| `DEPLOYMENT_TICKET_ID` | No | `""` | string | Ticket ID used as commit message prefix | -| `ENV_TEMPLATE_VERSION` | No | `""` | string | Template version to apply (e.g. `env-template:v1.2.3`) | -| `ENV_BUILDER` | No | `"true"` | choice | Enable environment build. Options: `true`, `false` | -| `GENERATE_EFFECTIVE_SET`| No | `"false"` | choice | Enable Effective Set generation. Options: `true`, `false` | -| `GET_PASSPORT` | No | `"false"` | choice | Enable Cloud Passport discovery. Options: `true`, `false` | -| `CMDB_IMPORT` | No | `"false"` | choice | Enable CMDB export. Options: `true`, `false` | -| `GH_ADDITIONAL_PARAMS` | No | `""` | string | Comma-separated key-value pairs for all other parameters | - -For full parameter semantics, see [Instance Pipeline Parameters](/docs/instance-pipeline-parameters.md). - -## GH_ADDITIONAL_PARAMS — Passing Extra Parameters - -### What Is GH_ADDITIONAL_PARAMS? - -`GH_ADDITIONAL_PARAMS` is a single string input that carries all pipeline parameters that are not exposed as separate workflow inputs. It is parsed by `.github/scripts/process_additional_variables.sh`, which adds each `KEY=VALUE` pair to the workflow environment. - -Use it for parameters such as: - -- `BG_MANAGE`, `BG_STATE` — Blue-Green operations -- `SD_SOURCE_TYPE`, `SD_VERSION`, `SD_DATA` — Solution Descriptor -- `ENV_SPECIFIC_PARAMS`, `ENV_TEMPLATE_NAME` — Environment configuration -- `EFFECTIVE_SET_CONFIG` — Effective Set options -- `CRED_ROTATION_PAYLOAD` — Credential rotation -- Any other parameter from [Instance Pipeline Parameters](/docs/instance-pipeline-parameters.md) - -### Format and Syntax - -**Format:** `KEY1=VALUE1,KEY2=VALUE2,KEY3=VALUE3` - -**Rules:** - -- Pairs are separated by commas. -- Each pair is `KEY=VALUE` (no spaces around `=`). -- Keys and values are trimmed of leading/trailing whitespace. -- Empty pairs are ignored. - -### Examples - -**Simple values:** - -```text -BG_MANAGE=true,SD_SOURCE_TYPE=artifact,SD_VERSION=my-app:v1.0 -``` - -**With JSON (escape double quotes):** - -```text -EFFECTIVE_SET_CONFIG={\"version\": \"v2.0\", \"app_chart_validation\": \"false\"} -``` - -**Multiple parameters:** - -```text -SD_SOURCE_TYPE=json,SD_DATA=[{\"version\":2.1,\"type\":\"solutionDeploy\"}],ENV_SPECIFIC_PARAMS={\"tenantName\":\"my-tenant\"} -``` - -**Blue-Green state:** - -```text -BG_MANAGE=true,BG_STATE={\"controllerNamespace\":\"bss-controller\",\"originNamespace\":{\"name\":\"bss-origin\",\"state\":\"active\"}} -``` - -### JSON Values and Escaping - -For JSON values: - -1. Escape internal double quotes: `\"` instead of `"`. -1. Be aware that commas inside JSON are used as pair separators. If your JSON contains commas, the parser may split it incorrectly. - -> [!CAUTION] -> **Workaround for complex JSON:** Use `pipeline_vars.env` (see below) or pass the parameter via the GitHub API with proper escaping. Commas inside JSON values may cause incorrect parsing. - -### When to Use pipeline_vars.env Instead - -Use `.github/pipeline_vars.env` when: - -- You have complex JSON with many commas. -- You want to keep sensitive or long values out of the UI. -- You need the same values across many runs (e.g. for debugging). - -Variables in `pipeline_vars.env` must be in standard `KEY=VALUE` format. Do **not** wrap them in `GH_ADDITIONAL_PARAMS`. - -## Adding New Parameters - -### Option A: Add as Workflow Input (If Under the Limit) - -If you have fewer than 10 inputs and want a dedicated UI field: - -1. Add the input under `on.workflow_dispatch.inputs` in `Envgene.yml`: - -```yaml -on: - workflow_dispatch: - inputs: - # ... existing inputs ... - MY_NEW_PARAM: - required: false - default: "" - type: string - description: "Description of the parameter" -``` - -1. Add a line in the "Process Input Parameters" step to export it: - -```yaml -echo "MY_NEW_PARAM=${{ github.event.inputs.MY_NEW_PARAM }}" >> $GITHUB_ENV -``` - -1. If the parameter controls job execution, add it to `process_environment_variables.outputs` (see [Adding New Jobs](#adding-new-jobs-and-conditional-execution)). - -### Option B: Use GH_ADDITIONAL_PARAMS - -1. Pass the parameter in `GH_ADDITIONAL_PARAMS`, e.g. `MY_NEW_PARAM=value`. -1. It will be parsed and added to `GITHUB_ENV` automatically. -1. If you need it for conditional steps, add it to the job outputs (see below). - -### Option C: Use pipeline_vars.env - -1. Add the variable to `.github/pipeline_vars.env`: - -```text -MY_NEW_PARAM=my_value -``` - -1. It will be loaded by the `load-env-files` action. -1. If you need it for conditional steps, add it to the job outputs. - -## Adding New Jobs and Conditional Execution - -To add a new step that runs only when a parameter is set, follow these steps. - -### Step 1: Ensure the Variable Is Available - -The variable must be present in `GITHUB_ENV` after the `process_environment_variables` job. It can come from: - -- A workflow input (and the "Process Input Parameters" step) -- `GH_ADDITIONAL_PARAMS` (parsed by `process_additional_variables.sh`) -- `pipeline_vars.env` or `config.env` (loaded by `load-env-files`) - -### Step 2: Expose the Variable as a Job Output - -Add the variable to the `outputs` of `process_environment_variables` in `Envgene.yml`: - -```yaml -jobs: - process_environment_variables: - outputs: - env_matrix: ${{ steps.matrix-generator.outputs.env_matrix }} - # ... existing outputs ... - MY_NEW_FEATURE: ${{ env.MY_NEW_FEATURE }} -``` - -Without this, the next job cannot use it in `if` conditions. - -### Step 3: Add the Job Step with an if Condition - -Add your step inside the `envgene_execution` job with an `if`: - -```yaml -- name: MY_NEW_JOB - if: needs.process_environment_variables.outputs.MY_NEW_FEATURE == 'true' - run: | - # Your commands here -``` - -**Common condition patterns:** - -| Condition type | Example | -|---------------------|-------------------------------------------------------------------------| -| Equals string | `needs.process_environment_variables.outputs.MY_VAR == 'true'` | -| Not empty | `needs.process_environment_variables.outputs.MY_VAR != ''` | -| Logical OR | `(condition1) \|\| (condition2)` | -| Logical AND | `(condition1) && (condition2)` | -| Multiple conditions | `outputs.ENV_BUILDER == 'true' && outputs.SD_VERSION != ''` | - -### Complete Example: Adding a Custom Job - -Assume you want a step that runs only when `RUN_CUSTOM_VALIDATION=true`. - -**1. Pass the parameter** via `GH_ADDITIONAL_PARAMS`: - -```text -RUN_CUSTOM_VALIDATION=true -``` - -**2. Add the output** in `Envgene.yml`: - -```yaml -process_environment_variables: - outputs: - # ... existing ... - RUN_CUSTOM_VALIDATION: ${{ env.RUN_CUSTOM_VALIDATION }} -``` - -**3. Add the step** in `envgene_execution`: - -```yaml -- name: CUSTOM_VALIDATION - if: needs.process_environment_variables.outputs.RUN_CUSTOM_VALIDATION == 'true' - run: | - echo "Running custom validation..." - # Your validation logic -``` - -## Parameter Priority - -When the same parameter is set in multiple places, the effective value is chosen by this order (highest first): - -1. Workflow input parameters (UI or API) -1. `pipeline_vars.env` -1. Repository variables (`vars`) -1. Organization variables - -## Repository Variables (vars) - -Repository variables are configured in **Settings → Secrets and variables → Actions → Variables** (repository-level) or at the organization level. They are referenced in the workflow as `vars.VARIABLE_NAME` and are available to all workflow runs. - -### Variables Used by the Workflow - -| Variable | Purpose | Default when empty | -|----------------------------|------------------------------------------|----------------------| -| `DOCKER_REGISTRY` | Docker registry base for EnvGene images | `ghcr.io/netcracker` | -| `GH_RUNNER_TAG_NAME` | Runner label for jobs (e.g. ubuntu-22.04)| `ubuntu-22.04` | -| `GH_RUNNER_SCRIPT_TIMEOUT` | Job timeout in minutes | `10` | - -### How to Add Repository Variables - -1. Go to your repository on GitHub. -1. Open **Settings** → **Secrets and variables** → **Actions**. -1. Open the **Variables** tab. -1. Click **New repository variable**. -1. Enter the name (e.g. `DOCKER_REGISTRY`) and value. -1. Click **Add variable**. - -### When Variables Are Empty or Missing - -The workflow uses fallback values when a variable is not set or is empty. For example: - -```yaml -runs-on: ${{ vars.GH_RUNNER_TAG_NAME || 'ubuntu-22.04' }} -DOCKER_IMAGE_NAME_ENVGENE: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-envgene" -timeout-minutes: ${{ fromJSON(vars.GH_RUNNER_SCRIPT_TIMEOUT || '10') }} -``` - -- If `vars.GH_RUNNER_TAG_NAME` is empty or missing → `ubuntu-22.04` is used. -- If `vars.DOCKER_REGISTRY` is empty or missing → `ghcr.io/netcracker` is used. -- If `vars.GH_RUNNER_SCRIPT_TIMEOUT` is empty or missing → `10` is used. - -> [!TIP] -> You do not need to define these variables for the workflow to run; defaults are applied automatically. - -### Adding Custom Variables - -To use your own variables in the workflow: - -1. Add the variable in **Settings → Secrets and variables → Actions → Variables**. -1. Reference it in `Envgene.yml` as `${{ vars.MY_CUSTOM_VAR }}`. -1. For optional variables with a default, use: `${{ vars.MY_CUSTOM_VAR || 'default_value' }}`. - -For a full list of supported repository variables, see [EnvGene Repository Variables](/docs/envgene-repository-variables.md). - -## How to Trigger the Workflow - -### Via GitHub Actions UI - -1. Open your repository on GitHub. -1. Go to **Actions**. -1. Select **EnvGene Execution**. -1. Click **Run workflow**. -1. Choose the branch, fill in parameters, and run. - -### Via GitHub API - -
-Click to expand API example - -```bash -curl -X POST \ - -H "Authorization: token " \ - -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/repos///actions/workflows/Envgene.yml/dispatches \ - -d '{ - "ref": "main", - "inputs": { - "ENV_NAMES": "cluster-01/env-01", - "ENV_BUILDER": "true", - "GENERATE_EFFECTIVE_SET": "true", - "DEPLOYMENT_TICKET_ID": "QBSHP-0001", - "GH_ADDITIONAL_PARAMS": "EFFECTIVE_SET_CONFIG={\"version\": \"v2.0\", \"app_chart_validation\": \"false\"}" - } - }' -``` - -Replace ``, ``, ``, and `main` as needed. - -
- -## Directory Structure - -```text -instance-repo-pipeline/ -├── README.md # This file -└── .github/ - ├── actions/ - │ └── load-env-files/ # Loads .env files into GITHUB_ENV - ├── configuration/ - │ └── config.env # Base pipeline configuration - ├── docs/ - │ └── README.md # Additional usage notes - ├── scripts/ - │ ├── generate_env_matrix.sh # Builds environment matrix from ENV_NAMES - │ ├── process_additional_variables.sh # Parses GH_ADDITIONAL_PARAMS - │ ├── process_matrix_iteration.sh # Extracts cluster/env from matrix - │ └── create_env_generation_params.sh # Builds ENV_GENERATION_PARAMS JSON - ├── workflows/ - │ └── Envgene.yml # Main workflow definition - └── pipeline_vars.env # Optional overrides (template, often empty) -``` - ---- - -## Use Case Scenarios - -This section shows typical scenarios with example parameters and what happens when you run the workflow. - -### Scenario 1: Full Deployment (Environment Build + Effective Set) - -**Goal:** Build the environment and generate the Effective Set for deployment. - -| Parameter | Value | -|-----------------------|--------------------------| -| `ENV_NAMES` | `prod-cluster/prod-01` | -| `ENV_BUILDER` | `true` | -| `GENERATE_EFFECTIVE_SET` | `true` | -| `DEPLOYMENT_TICKET_ID`| `QBSHP-1234` | - -**Steps that run:** APP_REG_DEF_PROCESS → ENV_BUILD → GENERATE_EFFECTIVE_SET → GIT_COMMIT - -**Result:** Environment Instance is generated, Effective Set is created in `environments/prod-cluster/prod-01/effective-set/`, changes are committed to the repository. - ---- - -### Scenario 2: Environment Build Only (No Effective Set) - -**Goal:** Regenerate the Environment Instance without generating the Effective Set (e.g. for validation or template updates). - -| Parameter | Value | -|-------------|------------------------| -| `ENV_NAMES` | `dev-cluster/dev-01` | -| `ENV_BUILDER` | `true` | - -**Steps that run:** APP_REG_DEF_PROCESS → ENV_BUILD → GIT_COMMIT - -**Result:** Environment Instance is regenerated and committed. GENERATE_EFFECTIVE_SET is skipped. - ---- - -### Scenario 3: Update Template Version and Rebuild - -**Goal:** Switch to a new template version and rebuild the environment. - -| Parameter | Value | -|-----------------------|-------------------------------| -| `ENV_NAMES` | `prod-cluster/prod-01` | -| `ENV_BUILDER` | `true` | -| `ENV_TEMPLATE_VERSION`| `env-template:v2.1.0` | - -**Steps that run:** APP_REG_DEF_PROCESS (updates template version) → ENV_BUILD → GIT_COMMIT - -**Result:** `env_definition.yml` is updated with the new template version, environment is rebuilt with the new template, changes are committed. - ---- - -### Scenario 4: Blue-Green Operation - -**Goal:** Perform a Blue-Green operation (e.g. warmup, state change). - -| Parameter | Value | -|-----------------------|------------------------| -| `ENV_NAMES` | `prod-cluster/prod-01` | -| `GH_ADDITIONAL_PARAMS` | `BG_MANAGE=true,BG_STATE={...}` (see below) | - -**Example `GH_ADDITIONAL_PARAMS` value:** - -```text -BG_MANAGE=true,BG_STATE={\"controllerNamespace\":\"bss-ctrl\",\"originNamespace\":{\"name\":\"bss-origin\",\"state\":\"ACTIVE\",\"version\":\"v1.0\"},\"peerNamespace\":{\"name\":\"bss-peer\",\"state\":\"CANDIDATE\",\"version\":\"v1.1\"},\"updateTime\":\"2024-01-15T10:00:00Z\"} -``` - -**Steps that run:** BG_MANAGE → GIT_COMMIT - -**Result:** BG state is validated, state files are updated in the repository, namespace objects are copied if warmup. No ENV_BUILD or Effective Set. - ---- - -### Scenario 5: Credential Rotation - -**Goal:** Rotate credentials for an environment without rebuilding. - -| Parameter | Value | -|------------------------|--------------------------------------------| -| `ENV_NAMES` | `prod-cluster/prod-01` | -| `GH_ADDITIONAL_PARAMS` | `CRED_ROTATION_PAYLOAD={...}` (see below) | - -**Example `GH_ADDITIONAL_PARAMS` value:** - -```text -CRED_ROTATION_PAYLOAD={\"credentials\":[{\"name\":\"db-password\",\"newValue\":\"\"}]} -``` - -**Steps that run:** CREDENTIAL_ROTATION → GIT_COMMIT - -**Result:** Credentials are updated per payload, changes are committed. See [Credential Rotation](/docs/features/cred-rotation.md) for full payload format. - ---- - -### Scenario 6: Process Solution Descriptor from Artifact - -**Goal:** Fetch SD from an artifact and merge it into the repository. - -| Parameter | Value | -|-----------------------|-----------------------------------------------------------------------| -| `ENV_NAMES` | `prod-cluster/prod-01` | -| `GH_ADDITIONAL_PARAMS` | `SD_SOURCE_TYPE=artifact,SD_VERSION=my-solution:v1.2.3,SD_REPO_MERGE_MODE=replace` | - -**Steps that run:** PROCESS_SD → GIT_COMMIT - -**Result:** SD is downloaded from the artifact registry, merged (or replaced) into `environments/prod-cluster/prod-01/Inventory/solution-descriptor/sd.yaml`, committed. - ---- - -### Scenario 7: Generate New Environment Inventory - -**Goal:** Create a new Environment Inventory (`env_definition.yml`) for a new environment. - -| Parameter | Value | -|-----------------------|-----------------------------------------------------------------------| -| `ENV_NAMES` | `new-cluster/new-env` | -| `GH_ADDITIONAL_PARAMS` | `ENV_INVENTORY_INIT=true,ENV_TEMPLATE_NAME=my-env-template` | - -**Steps that run:** ENV_INVENTORY_GENERATION → GIT_COMMIT - -**Result:** New `env_definition.yml` is created at `environments/new-cluster/new-env/Inventory/`, committed. See [Environment Inventory Generation](/docs/features/env-inventory-generation.md). - ---- - -### Scenario 8: Multiple Environments in One Run - -**Goal:** Process several environments with the same parameters. - -| Parameter | Value | -|-------------|--------------------------------------| -| `ENV_NAMES` | `cluster-01/env-01,cluster-01/env-02,cluster-02/env-01` | -| `ENV_BUILDER` | `true` | - -**Steps that run:** For each environment in the matrix: APP_REG_DEF_PROCESS → ENV_BUILD → GIT_COMMIT (parallel jobs) - -**Result:** Three separate `envgene_execution` jobs run in parallel, each processes one environment. All changes are committed in a single workflow run. - ---- - -## Further Reading - -| Document | Description | -|----------|--------------| -| [Instance Pipeline Parameters](/docs/instance-pipeline-parameters.md) | Full parameter reference | -| [EnvGene Pipelines](/docs/envgene-pipelines.md) | Pipeline flow and job descriptions | -| [Blue-Green Deployment](/docs/features/blue-green-deployment.md) | BG-related parameters | -| [SD Processing](/docs/use-cases/sd-processing.md) | Solution Descriptor use cases | - ---- - -
- -EnvGene GitHub Workflow — Part of the Qubership EnvGene platform - -
From 7d2276777dd128b2eacb8269a52ddd1b9663d495 Mon Sep 17 00:00:00 2001 From: dysmon <120464230+dysmon@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:25:09 +0500 Subject: [PATCH 108/161] feat: replace all common modules and dependencies from envgene dockerimages to initial base module image (#1147) --- .../build/Dockerfile | 62 ++------- build_envgene/build/Dockerfile | 124 +++++------------- build_pipegene/build/Dockerfile | 105 +++------------ 3 files changed, 62 insertions(+), 229 deletions(-) diff --git a/build_effective_set_generator/build/Dockerfile b/build_effective_set_generator/build/Dockerfile index 84b7c127d..68d89a8da 100644 --- a/build_effective_set_generator/build/Dockerfile +++ b/build_effective_set_generator/build/Dockerfile @@ -1,49 +1,34 @@ -######################################### +# ═══════════════════════════════════════════════════════════════════════════════════ # Stage 1: Build -# Multi-stage build to reduce final image size -FROM python:3.12-alpine3.19 AS build +# ═══════════════════════════════════════════════════════════════════════════════════ +FROM ghcr.io/netcracker/qubership-envgene-base-modules:1.0.0 AS build ARG RUN_JAVA_VERSION=1.3.8 -ARG SOPS_VERSION=3.9.0 # hadolint ignore=DL3018 RUN apk add --no-cache \ - gcc \ - musl-dev \ libffi-dev \ openssl-dev \ python3-dev \ cargo \ rust \ - build-base \ - curl \ - wget + build-base -COPY build_effective_set_generator/build/sources.list /etc/apk/repositories COPY build_effective_set_generator/build/pip.conf /etc/pip.conf COPY build_effective_set_generator/build/requirements.txt /build/requirements.txt COPY python /python -RUN python3 -m venv /module/venv && \ - /module/venv/bin/pip install --upgrade pip "setuptools<82" wheel - -RUN /module/venv/bin/pip install --no-cache-dir \ +RUN pip install --no-cache-dir \ --no-binary cffi \ --no-binary cryptography \ -r /build/requirements.txt - -RUN /module/venv/bin/pip install --no-cache-dir \ + +RUN pip install --no-cache-dir \ /python/integration \ /python/jschon-sort \ /python/envgene \ /python/artifact-searcher -# Download and install SOPS for secrets management -RUN curl -fsSL \ - https://github.com/mozilla/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.linux.amd64 \ - -o /usr/local/bin/sops && \ - chmod +x /usr/local/bin/sops - RUN mkdir -p /deployments && \ curl -sSL https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh \ -o /deployments/run-java.sh @@ -51,57 +36,36 @@ RUN mkdir -p /deployments && \ COPY build_effective_set_generator/effective-set-generator/target/*.jar /deployments/app.jar -######################################### +# ═══════════════════════════════════════════════════════════════════════════════════ # Stage 2: Runtime -# Lightweight runtime image with only essential dependencies -FROM python:3.12-alpine3.19 AS runtime +# ═══════════════════════════════════════════════════════════════════════════════════ +FROM ghcr.io/netcracker/qubership-envgene-base-modules:1.0.0 AS runtime ARG JAVA_PACKAGE=openjdk17-jre-headless ENV LANG='en_US.UTF-8' \ - LANGUAGE='en_US:en' \ - PATH=/module/venv/bin:$PATH - -COPY build_effective_set_generator/build/pip.conf /etc/pip.conf -COPY build_effective_set_generator/build/sources.list /etc/apk/repositories + LANGUAGE='en_US:en' # Install ONLY runtime dependencies (NO dev packages) # hadolint ignore=DL3018 RUN apk add --no-cache \ - libffi \ libgcc \ - openssl \ - bash \ - ca-certificates \ - tar \ - curl \ - jq \ - yq \ - gettext \ - sed \ - age \ ${JAVA_PACKAGE} COPY --from=build /python /python COPY --from=build /module/venv /module/venv -COPY --from=build /usr/local/bin/sops /usr/local/bin/sops COPY --from=build /deployments /deployments COPY scripts/build_env/ /build_env/scripts/build_env/ COPY build_effective_set_generator/scripts/ /module/scripts/ COPY scripts/utils/ /module/scripts/utils/ -# User creation + permissions -RUN addgroup ci && \ - adduser -D -h /module/ -s /bin/bash -G ci ci && \ - chown ci:ci -R /module /deployments && \ +# Permissions +RUN chown -R ci:ci /module /deployments && \ chmod +x /module/scripts/*.sh && \ - chmod +x /module/scripts/utils/entrypoint.sh && \ chmod 644 /module/scripts/*.py 2>/dev/null || true && \ - chmod +x /usr/local/bin/sops && \ chmod 540 /deployments/run-java.sh && \ chmod g+rwX /deployments USER ci:ci - WORKDIR /module diff --git a/build_envgene/build/Dockerfile b/build_envgene/build/Dockerfile index 88ca831c0..5214f577d 100644 --- a/build_envgene/build/Dockerfile +++ b/build_envgene/build/Dockerfile @@ -1,25 +1,11 @@ # checkov:skip=CKV_DOCKER_3:This build container requires root privileges for CI/CD operations - -FROM python:3.12-alpine3.19 AS build - -# Install build dependencies +# checkov:skip=CKV_DOCKER_8:Root required for workspace write (shutil.copy2 utime) in CI/CD # checkov:skip=CKV2_DOCKER_1:sudo is required for certain operations in scripts -# hadolint ignore=DL3018 -RUN apk add --no-cache \ - gcc \ - musl-dev \ - libffi-dev \ - openssl-dev \ - libxml2-dev \ - libxslt-dev \ - zlib-dev \ - git \ - curl \ - jq \ - openssh-client \ - sudo \ - zip \ - unzip + +# ═══════════════════════════════════════════════════════════════════════════════════ +# Stage 1: Build +# ═══════════════════════════════════════════════════════════════════════════════════ +FROM ghcr.io/netcracker/qubership-envgene-base-modules:1.0.0 AS build COPY build_envgene/build/pip.conf /etc/pip.conf COPY build_envgene/build/requirements.txt /build/requirements.txt @@ -30,97 +16,55 @@ COPY python /python COPY build_envgene/scripts /module/scripts COPY scripts/bg_manage /scripts/bg_manage COPY creds_rotation/scripts /module/creds_rotation_scripts -COPY build_* create_* produce_* sort* /build_env/ COPY scripts/build_env /build_env/scripts/build_env/ COPY scripts/build_template /build_env/scripts/build_template/ COPY scripts/cloud_passport/ /cloud_passport/scripts/ COPY schemas /build_env/schemas COPY scripts/utils /module/scripts/utils -RUN python3 -m venv /module/venv && \ - /module/venv/bin/pip install --upgrade pip "setuptools<82" wheel - -RUN /module/venv/bin/pip install --no-cache-dir --retries 10 --timeout 60 -r /build/requirements.txt - -RUN /module/venv/bin/pip install /python/jschon-sort && \ - /module/venv/bin/pip install /python/envgene && \ - /module/venv/bin/pip install /python/integration && \ - /module/venv/bin/pip install /python/artifact-searcher && \ - /module/venv/bin/pip install --no-cache-dir --no-deps -r /build/creds_rotation_requirements.txt - -RUN wget --quiet --tries=3 \ - https://github.com/mozilla/sops/releases/download/v3.9.0/sops-v3.9.0.linux.amd64 \ - -O /usr/local/bin/sops && \ - chmod +x /usr/local/bin/sops - -# Aggressive cleanup to reduce image size -RUN apk del gcc musl-dev libffi-dev openssl-dev libxml2-dev libxslt-dev zlib-dev && \ - rm -rf /var/cache/apk/* /tmp/* /var/tmp/* /root/.cache -# Remove unnecessary files from Python packages -RUN find /module/venv/lib/python3.12/site-packages -name '*.pyc' -delete - -RUN find /module/venv/lib/python3.12/site-packages -name '*.pyo' -delete -RUN find /module/venv/lib/python3.12/site-packages -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null || true -RUN rm -rf /module/venv/lib/python3.12/site-packages/pytest* \ - /module/venv/lib/python3.12/site-packages/_pytest* 2>/dev/null || true -RUN /module/venv/bin/pip cache purge +RUN pip install --no-cache-dir --retries 10 --timeout 60 \ + -r /build/requirements.txt \ + /python/jschon-sort \ + /python/envgene \ + /python/integration \ + /python/artifact-searcher && \ + pip install --no-cache-dir --no-deps \ + -r /build/creds_rotation_requirements.txt + +RUN rm -rf /var/cache/apk/* /tmp/* /var/tmp/* /root/.cache && \ + find /module/venv/lib/python3.12/site-packages -name '*.pyc' -delete && \ + find /module/venv/lib/python3.12/site-packages -name '*.pyo' -delete && \ + find /module/venv/lib/python3.12/site-packages -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null || true && \ + rm -rf /module/venv/lib/python3.12/site-packages/pytest* \ + /module/venv/lib/python3.12/site-packages/_pytest* 2>/dev/null || true && \ + /module/venv/bin/pip cache purge RUN chmod 754 /module/scripts/* && \ chmod 754 /module/creds_rotation_scripts/* - -FROM python:3.12-alpine3.19 AS runtime - -# Install only essential runtime dependencies -# checkov:skip=CKV2_DOCKER_1:sudo is required for certain operations in scripts -# hadolint ignore=DL3018 -RUN apk add --no-cache \ - bash \ - ca-certificates \ - curl \ - jq \ - yq \ - gettext \ - age \ - git \ - openssh-client \ - sudo \ - zip \ - unzip \ - tar +# ═══════════════════════════════════════════════════════════════════════════════════ +# Stage 2: Runtime +# ═══════════════════════════════════════════════════════════════════════════════════ +FROM ghcr.io/netcracker/qubership-envgene-base-modules:1.0.0 AS runtime COPY --from=build /module /module COPY --from=build /scripts/bg_manage /scripts/bg_manage -COPY --from=build /usr/local/bin/sops /usr/local/bin/sops COPY --from=build /build_env /build_env COPY --from=build /cloud_passport /cloud_passport COPY --from=build /python /python COPY --from=build /etc/pip.conf /etc/pip.conf +RUN rm -rf /var/cache/apk/* /tmp/* /var/tmp/* /root/.cache && \ + find /module/venv/lib/python3.12/site-packages -name '*.pyc' -delete && \ + /module/venv/bin/pip cache purge -# Create directories that might be needed for CI environments -# These directories are commonly used by GitHub Actions and GitLab CI -RUN mkdir -p /__w/_temp/_runner_file_commands && \ - mkdir -p /github/workspace && \ - mkdir -p /github/home && \ - mkdir -p /builds && \ - mkdir -p /cache && \ - chmod 777 /__w/_temp/_runner_file_commands && \ - chmod 777 /github/workspace && \ - chmod 777 /github/home && \ - chmod 777 /builds && \ - chmod 777 /cache - -# Final cleanup -RUN rm -rf /var/cache/apk/* /tmp/* /var/tmp/* /root/.cache -RUN find /module/venv/lib/python3.12/site-packages -name '*.pyc' -delete -RUN /module/venv/bin/pip cache purge - -ENV PATH=/module/venv/bin:$PATH \ - PYTHONUNBUFFERED=1 \ - PYTHONDONTWRITEBYTECODE=1 +RUN chown -R ci:ci /module /build_env /cloud_passport /scripts HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD python -c "import sys; sys.exit(0)" || exit 1 +# checkov:skip=CKV_DOCKER_8:Root required for workspace write in CI/CD +# hadolint ignore=DL3002 +USER root +WORKDIR /module/scripts CMD ["bash"] diff --git a/build_pipegene/build/Dockerfile b/build_pipegene/build/Dockerfile index 78047c63c..149edca62 100644 --- a/build_pipegene/build/Dockerfile +++ b/build_pipegene/build/Dockerfile @@ -1,34 +1,13 @@ -######################################### +# ═══════════════════════════════════════════════════════════════════════════════════ # Stage 1: Build -# Multi-stage build to reduce final image size -FROM python:3.12-alpine3.19 AS build -SHELL ["/bin/ash", "-eo", "pipefail", "-c"] +# ═══════════════════════════════════════════════════════════════════════════════════ +FROM ghcr.io/netcracker/qubership-envgene-base-modules:1.0.0 AS build -# Install build dependencies (pinned) -RUN set -eux; \ - apk update; \ - ver() { apk list --verbose --available "$1" | head -n1 | sed -E "s/^$1-//; s/ .*//"; }; \ - apk add --no-cache \ - gcc="$(ver gcc)" \ - musl-dev="$(ver musl-dev)" \ - libffi-dev="$(ver libffi-dev)" \ - openssl-dev="$(ver openssl-dev)" \ - git="$(ver git)" \ - curl="$(ver curl)" \ - jq="$(ver jq)" \ - yq="$(ver yq)" \ - gettext="$(ver gettext)" \ - sed="$(ver sed)" \ - age="$(ver age)"; \ - rm -rf /var/cache/apk/* - -# Copy configuration files (merged into build directory) -COPY build_pipegene/build/sources.list /etc/apk/repositories +# Copy configuration files and source code COPY build_pipegene/build/pip.conf /etc/pip.conf COPY build_pipegene/build/constraint.txt /build/constraint.txt COPY build_pipegene/build/requirements.txt /build/requirements.txt -# Copy source code COPY python /python COPY schemas /module/schemas COPY base_modules/scripts /module/scripts @@ -37,19 +16,13 @@ COPY scripts/utils /module/scripts/scripts/utils/ COPY build_pipegene/pipegene_plugins /module/scripts/pipegene_plugins # Create virtual environment and install Python packages -RUN python -m venv /module/venv && \ - /module/venv/bin/pip install --upgrade pip "setuptools<82" wheel && \ - /module/venv/bin/pip install --no-cache-dir --retries 10 --timeout 60 -r /build/requirements.txt && \ - /module/venv/bin/pip install /python/integration /python/jschon-sort /python/envgene && \ - /module/venv/bin/pip install PyYAML - -# Download and install SOPS for secrets management (use curl for BusyBox compatibility) -RUN curl --fail --show-error --location --retry 3 \ - https://github.com/mozilla/sops/releases/download/v3.9.0/sops-v3.9.0.linux.amd64 \ - -o /usr/local/bin/sops && \ - chmod +x /usr/local/bin/sops +RUN pip install --no-cache-dir --retries 10 --timeout 60 \ + -r /build/requirements.txt \ + /python/integration \ + /python/jschon-sort \ + /python/envgene -# Aggressive cleanup to reduce image size +# Cleanup to reduce image size RUN apk del gcc musl-dev libffi-dev openssl-dev && \ rm -rf /var/cache/apk/* /tmp/* /var/tmp/* /root/.cache && \ find /module/venv/lib/python3.12/site-packages -name '*.pyc' -delete && \ @@ -64,75 +37,27 @@ RUN apk del gcc musl-dev libffi-dev openssl-dev && \ ( /module/venv/bin/pip cache purge 2>/dev/null || true ) && \ rm -rf /module/venv/lib/python3.12/site-packages/pip* 2>/dev/null || true -# Set permissions RUN chmod 754 /module/scripts/* -######################################### +# ═══════════════════════════════════════════════════════════════════════════════════ # Stage 2: Runtime -# Lightweight runtime image with only essential dependencies -FROM python:3.12-alpine3.19 AS runtime -SHELL ["/bin/ash", "-eo", "pipefail", "-c"] - -# Install only essential runtime dependencies (pinned) -RUN set -eux; \ - apk update; \ - ver() { apk list --verbose --available "$1" | head -n1 | sed -E "s/^$1-//; s/ .*//"; }; \ - apk add --no-cache \ - bash="$(ver bash)" \ - ca-certificates="$(ver ca-certificates)" \ - curl="$(ver curl)" \ - jq="$(ver jq)" \ - yq="$(ver yq)" \ - gettext="$(ver gettext)" \ - sed="$(ver sed)" \ - age="$(ver age)" \ - git="$(ver git)"; \ - rm -rf /var/cache/apk/* +# ═══════════════════════════════════════════════════════════════════════════════════ +FROM ghcr.io/netcracker/qubership-envgene-base-modules:1.0.0 AS runtime -# Copy everything from build stage COPY --from=build /module /module -COPY --from=build /usr/local/bin/sops /usr/local/bin/sops COPY --from=build /python /python COPY --from=build /etc/pip.conf /etc/pip.conf -# Set permissions -RUN chmod +x /usr/local/bin/sops - -# Create directories that might be needed for CI environments -RUN mkdir -p /__w/_temp/_runner_file_commands && \ - mkdir -p /github/workspace && \ - mkdir -p /github/home && \ - mkdir -p /builds && \ - mkdir -p /cache && \ - chmod 777 /__w/_temp/_runner_file_commands && \ - chmod 777 /github/workspace && \ - chmod 777 /github/home && \ - chmod 777 /builds && \ - chmod 777 /cache - # Final cleanup RUN set -eux; \ rm -rf /var/cache/apk/* /tmp/* /var/tmp/* /root/.cache; \ find /module/venv/lib/python3.12/site-packages -name '*.pyc' -delete; \ - /module/venv/bin/pip cache purge 2>/dev/null || true; \ - # Keep pip for runtime compatibility, but remove setuptools and wheel - rm -rf /module/venv/lib/python3.12/site-packages/setuptools* /module/venv/lib/python3.12/site-packages/wheel* 2>/dev/null || true - -# Set environment -ENV PATH=/module/venv/bin:$PATH \ - PYTHONUNBUFFERED=1 \ - PYTHONDONTWRITEBYTECODE=1 + /module/venv/bin/pip cache purge 2>/dev/null || true; -# Create user for security -RUN addgroup ci && adduser -D -h /module/ -s /bin/bash -G ci ci && \ - chown ci:ci -R /module - -# Health check HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD python -c "import sys; sys.exit(0)" || exit 1 +RUN chown -R ci:ci /module USER ci:ci WORKDIR /module/scripts - -# Default command CMD ["bash"] From 0a0e26aff1d547d604ccfda6e082e6361fa419f3 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Thu, 26 Mar 2026 07:45:27 +0000 Subject: [PATCH 109/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index de00a924e..982b99124 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -71,9 +71,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.12" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.12" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.12" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.13" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.13" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.13" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 9436b854d..9f06a1d6c 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.12 +version: 1.31.13 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 21768b45d..f8d7f3dc4 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.12 +version: 1.31.13 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index b8070bd01..faa845636 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.12", + "envgene_version": "1.31.13", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 15ca8793994560b9001cd94f0f14014134a7e5f0 Mon Sep 17 00:00:00 2001 From: chethana-shastry-p Date: Thu, 26 Mar 2026 13:42:47 +0530 Subject: [PATCH 110/161] fix: correcting type for consul enabled (#1180) --- .../test/resources/environments/cluster-01/pl-01/cloud.yml | 2 +- .../effective-set/cleanup/monitoring-origin/parameters.yaml | 2 +- .../cluster-01/pl-01/effective-set/cleanup/pg/parameters.yaml | 2 +- .../MONITORING/values/deployment-parameters.yaml | 4 ++-- .../deployment/pg/postgres/values/deployment-parameters.yaml | 4 ++-- scripts/build_env/cloud_passport.py | 2 +- test_data/test_environments/cluster-01/env-01/cloud.yml | 2 +- test_data/test_environments/cluster-01/env-02/cloud.yml | 2 +- test_data/test_environments/cluster-01/env-03/cloud.yml | 2 +- test_data/test_environments/cluster-01/env-04/cloud.yml | 2 +- test_data/test_environments/cluster-02/env-01/cloud.yml | 2 +- 11 files changed, 13 insertions(+), 13 deletions(-) diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/cloud.yml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/cloud.yml index e151438dd..a8166f399 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/cloud.yml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/cloud.yml @@ -40,7 +40,7 @@ deployParameters: CDN_STORAGE_USERNAME: "${STORAGE_USERNAME}" # cloud passport: cluster-01 version: 1.5 CLOUD_DASHBOARD_URL: "https://dashboard.cluster-01.qubership.org" # cloud passport: cluster-01 version: 1.5 CMDB_URL: "https://cluster-01.qubership.org" # cloud passport: cluster-01 version: 1.5 - CONSUL_ENABLED: "true" # cloud passport: cluster-01 version: 1.5 + CONSUL_ENABLED: true # cloud passport: cluster-01 version: 1.5 DEFAULT_TENANT_ADMIN_LOGIN: "admin" # cloud passport: cluster-01 version: 1.5 DEFAULT_TENANT_ADMIN_PASSWORD: "password" # cloud passport: cluster-01 version: 1.5 DEFAULT_TENANT_NAME: "tenant" # cloud passport: cluster-01 version: 1.5 diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/monitoring-origin/parameters.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/monitoring-origin/parameters.yaml index 466dd98f9..ee9dbe1bb 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/monitoring-origin/parameters.yaml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/monitoring-origin/parameters.yaml @@ -12,7 +12,7 @@ CLOUD_PROTOCOL: https CLOUD_PUBLIC_HOST: cluster-01.qubership.org CMDB_URL: https://cluster-01.qubership.org CONSUL_ADMIN_TOKEN: token-placeholder-123 -CONSUL_ENABLED: 'true' +CONSUL_ENABLED: true CONSUL_PUBLIC_URL: http://consul.consul:8080 CONSUL_URL: http://consul.consul:8080 CONTROLLER_NAMESPACE: env-1-bg-controller diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/pg/parameters.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/pg/parameters.yaml index 38eb6ae59..f1613b417 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/pg/parameters.yaml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/pg/parameters.yaml @@ -12,7 +12,7 @@ CLOUD_PROTOCOL: https CLOUD_PUBLIC_HOST: cluster-01.qubership.org CMDB_URL: https://cluster-01.qubership.org CONSUL_ADMIN_TOKEN: token-placeholder-123 -CONSUL_ENABLED: 'true' +CONSUL_ENABLED: true CONSUL_PUBLIC_URL: http://consul.consul:8080 CONSUL_URL: http://consul.consul:8080 CUSTOM_HOST: cluster-01.qubership.org diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/monitoring-origin/MONITORING/values/deployment-parameters.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/monitoring-origin/MONITORING/values/deployment-parameters.yaml index d0aaf505b..e900f09b6 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/monitoring-origin/MONITORING/values/deployment-parameters.yaml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/monitoring-origin/MONITORING/values/deployment-parameters.yaml @@ -17,7 +17,7 @@ CLOUD_PRIVATE_HOST: cluster-01.qubership.org CLOUD_PROTOCOL: https CLOUD_PUBLIC_HOST: cluster-01.qubership.org CMDB_URL: https://cluster-01.qubership.org -CONSUL_ENABLED: 'true' +CONSUL_ENABLED: true CONSUL_PUBLIC_URL: http://consul.consul:8080 CONSUL_URL: http://consul.consul:8080 CONTROLLER_NAMESPACE: env-1-bg-controller @@ -100,7 +100,7 @@ global: &id002 CLOUD_PROTOCOL: https CLOUD_PUBLIC_HOST: cluster-01.qubership.org CMDB_URL: https://cluster-01.qubership.org - CONSUL_ENABLED: 'true' + CONSUL_ENABLED: true CONSUL_PUBLIC_URL: http://consul.consul:8080 CONSUL_URL: http://consul.consul:8080 CONTROLLER_NAMESPACE: env-1-bg-controller diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/pg/postgres/values/deployment-parameters.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/pg/postgres/values/deployment-parameters.yaml index c11ace9fc..006a49a9e 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/pg/postgres/values/deployment-parameters.yaml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/pg/postgres/values/deployment-parameters.yaml @@ -17,7 +17,7 @@ CLOUD_PRIVATE_HOST: cluster-01.qubership.org CLOUD_PROTOCOL: https CLOUD_PUBLIC_HOST: cluster-01.qubership.org CMDB_URL: https://cluster-01.qubership.org -CONSUL_ENABLED: 'true' +CONSUL_ENABLED: true CONSUL_PUBLIC_URL: http://consul.consul:8080 CONSUL_URL: http://consul.consul:8080 CUSTOM_HOST: cluster-01.qubership.org @@ -89,7 +89,7 @@ global: &id001 CLOUD_PROTOCOL: https CLOUD_PUBLIC_HOST: cluster-01.qubership.org CMDB_URL: https://cluster-01.qubership.org - CONSUL_ENABLED: 'true' + CONSUL_ENABLED: true CONSUL_PUBLIC_URL: http://consul.consul:8080 CONSUL_URL: http://consul.consul:8080 CUSTOM_HOST: cluster-01.qubership.org diff --git a/scripts/build_env/cloud_passport.py b/scripts/build_env/cloud_passport.py index f96185ce9..f4c11f8fa 100644 --- a/scripts/build_env/cloud_passport.py +++ b/scripts/build_env/cloud_passport.py @@ -83,7 +83,7 @@ def process_cloud_definition(cloudPassportYaml, env_dir, comment) : process_and_update_key("publicUrl", consulConfigYaml, "CONSUL_PUBLIC_URL", consulPassportYaml, comment) process_and_update_key("internalUrl", consulConfigYaml, "CONSUL_URL", consulPassportYaml, comment) # CONSUL_ENABLED variable should be both in consul section and in deploy parameters - store_value_to_yaml(cloudYaml["deployParameters"], "CONSUL_ENABLED", f"{consulConfigYaml['enabled']}".lower(), comment) + store_value_to_yaml(cloudYaml["deployParameters"], "CONSUL_ENABLED", consulConfigYaml['enabled'], comment) del cloudPassportYaml["consul"] else: store_value_to_yaml(cloudYaml["consulConfig"], "enabled", False) diff --git a/test_data/test_environments/cluster-01/env-01/cloud.yml b/test_data/test_environments/cluster-01/env-01/cloud.yml index 482a75bfd..4fa2aec6b 100644 --- a/test_data/test_environments/cluster-01/env-01/cloud.yml +++ b/test_data/test_environments/cluster-01/env-01/cloud.yml @@ -40,7 +40,7 @@ deployParameters: CDN_STORAGE_USERNAME: "${STORAGE_USERNAME}" # cloud passport: cluster-01 version: 1.5 CLOUD_DASHBOARD_URL: "https://dashboard.cluster-01.qubership.org" # cloud passport: cluster-01 version: 1.5 CMDB_URL: "https://cluster-01.qubership.org" # cloud passport: cluster-01 version: 1.5 - CONSUL_ENABLED: "true" # cloud passport: cluster-01 version: 1.5 + CONSUL_ENABLED: true # cloud passport: cluster-01 version: 1.5 DEFAULT_TENANT_ADMIN_LOGIN: "admin" # cloud passport: cluster-01 version: 1.5 DEFAULT_TENANT_ADMIN_PASSWORD: "password" # cloud passport: cluster-01 version: 1.5 DEFAULT_TENANT_NAME: "tenant" # cloud passport: cluster-01 version: 1.5 diff --git a/test_data/test_environments/cluster-01/env-02/cloud.yml b/test_data/test_environments/cluster-01/env-02/cloud.yml index 9b99c3901..855b9e8bf 100644 --- a/test_data/test_environments/cluster-01/env-02/cloud.yml +++ b/test_data/test_environments/cluster-01/env-02/cloud.yml @@ -40,7 +40,7 @@ deployParameters: CDN_STORAGE_USERNAME: "${STORAGE_USERNAME}" # cloud passport: cluster-01 version: 1.5 CLOUD_DASHBOARD_URL: "https://dashboard.cluster-01.qubership.org" # cloud passport: cluster-01 version: 1.5 CMDB_URL: "https://cluster-01.qubership.org" # cloud passport: cluster-01 version: 1.5 - CONSUL_ENABLED: "true" # cloud passport: cluster-01 version: 1.5 + CONSUL_ENABLED: true # cloud passport: cluster-01 version: 1.5 DEFAULT_TENANT_ADMIN_LOGIN: "admin" # cloud passport: cluster-01 version: 1.5 DEFAULT_TENANT_ADMIN_PASSWORD: "password" # cloud passport: cluster-01 version: 1.5 DEFAULT_TENANT_NAME: "tenant" # cloud passport: cluster-01 version: 1.5 diff --git a/test_data/test_environments/cluster-01/env-03/cloud.yml b/test_data/test_environments/cluster-01/env-03/cloud.yml index 19b15aecf..bf4322d8a 100644 --- a/test_data/test_environments/cluster-01/env-03/cloud.yml +++ b/test_data/test_environments/cluster-01/env-03/cloud.yml @@ -40,7 +40,7 @@ deployParameters: CDN_STORAGE_USERNAME: "${STORAGE_USERNAME}" # cloud passport: cluster-01 version: 1.5 CLOUD_DASHBOARD_URL: "https://dashboard.cluster-01.qubership.org" # cloud passport: cluster-01 version: 1.5 CMDB_URL: "https://cluster-01.qubership.org" # cloud passport: cluster-01 version: 1.5 - CONSUL_ENABLED: "true" # cloud passport: cluster-01 version: 1.5 + CONSUL_ENABLED: true # cloud passport: cluster-01 version: 1.5 DEFAULT_TENANT_ADMIN_LOGIN: "admin" # cloud passport: cluster-01 version: 1.5 DEFAULT_TENANT_ADMIN_PASSWORD: "password" # cloud passport: cluster-01 version: 1.5 DEFAULT_TENANT_NAME: "tenant" # cloud passport: cluster-01 version: 1.5 diff --git a/test_data/test_environments/cluster-01/env-04/cloud.yml b/test_data/test_environments/cluster-01/env-04/cloud.yml index cbacd692f..59fc611db 100644 --- a/test_data/test_environments/cluster-01/env-04/cloud.yml +++ b/test_data/test_environments/cluster-01/env-04/cloud.yml @@ -40,7 +40,7 @@ deployParameters: CDN_STORAGE_USERNAME: "${STORAGE_USERNAME}" # cloud passport: cluster-01 version: 1.5 CLOUD_DASHBOARD_URL: "https://dashboard.cluster-01.qubership.org" # cloud passport: cluster-01 version: 1.5 CMDB_URL: "https://cluster-01.qubership.org" # cloud passport: cluster-01 version: 1.5 - CONSUL_ENABLED: "true" # cloud passport: cluster-01 version: 1.5 + CONSUL_ENABLED: true # cloud passport: cluster-01 version: 1.5 DEFAULT_TENANT_ADMIN_LOGIN: "admin" # cloud passport: cluster-01 version: 1.5 DEFAULT_TENANT_ADMIN_PASSWORD: "password" # cloud passport: cluster-01 version: 1.5 DEFAULT_TENANT_NAME: "tenant" # cloud passport: cluster-01 version: 1.5 diff --git a/test_data/test_environments/cluster-02/env-01/cloud.yml b/test_data/test_environments/cluster-02/env-01/cloud.yml index 482a75bfd..4fa2aec6b 100644 --- a/test_data/test_environments/cluster-02/env-01/cloud.yml +++ b/test_data/test_environments/cluster-02/env-01/cloud.yml @@ -40,7 +40,7 @@ deployParameters: CDN_STORAGE_USERNAME: "${STORAGE_USERNAME}" # cloud passport: cluster-01 version: 1.5 CLOUD_DASHBOARD_URL: "https://dashboard.cluster-01.qubership.org" # cloud passport: cluster-01 version: 1.5 CMDB_URL: "https://cluster-01.qubership.org" # cloud passport: cluster-01 version: 1.5 - CONSUL_ENABLED: "true" # cloud passport: cluster-01 version: 1.5 + CONSUL_ENABLED: true # cloud passport: cluster-01 version: 1.5 DEFAULT_TENANT_ADMIN_LOGIN: "admin" # cloud passport: cluster-01 version: 1.5 DEFAULT_TENANT_ADMIN_PASSWORD: "password" # cloud passport: cluster-01 version: 1.5 DEFAULT_TENANT_NAME: "tenant" # cloud passport: cluster-01 version: 1.5 From e28be0c0d63a9e5a0b167ef70be6fe2c9413add4 Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:38:41 +0300 Subject: [PATCH 111/161] chore: Added SonarQube check (#1181) --- .github/workflows/sonar-check.yml | 28 +++++++++++++++++++ .../.github/workflows/Envgene.yml | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/sonar-check.yml diff --git a/.github/workflows/sonar-check.yml b/.github/workflows/sonar-check.yml new file mode 100644 index 000000000..f90382ed1 --- /dev/null +++ b/.github/workflows/sonar-check.yml @@ -0,0 +1,28 @@ +name: Sonar Check + +on: + workflow_dispatch: {} + push: + branches: [main] + +jobs: + mvn-build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build java + uses: netcracker/qubership-workflow-hub/actions/maven-snapshot-deploy@main + with: + java-version: 17 + pom-file: build_effective_set_generator/pom.xml + # Default target-store is "central"; the action then adds -Pcentral and GPG runs. Sonar only needs -Pgithub. + target-store: github + maven-command: > + --batch-mode clean verify + org.sonarsource.scanner.maven:sonar-maven-plugin:${{ vars.SONAR_PLUGIN_VERSION }}:sonar + -Dsonar.projectKey=${{ vars.SONAR_PROJECT_KEY }} + -Dsonar.organization=${{ vars.SONAR_ORGANIZATION }} + -Dsonar.host.url=${{ vars.SONAR_HOST_URL }} + maven-token: ${{ secrets.GITHUB_TOKEN }} + sonar-token: ${{ secrets.SONAR_TOKEN }} \ No newline at end of file diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 982b99124..a54b15114 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -419,7 +419,7 @@ jobs: ### GIT COMMIT ### - name: GIT_COMMIT run: | - docker run --rm -v ${{ github.workspace }}:${{ env.CI_PROJECT_DIR }} -w ${{ env.CI_PROJECT_DIR }} --env-file .env.container --user root -e HOME=/root \ + docker run --rm -v ${{ github.workspace }}:${{ env.CI_PROJECT_DIR }} -w ${{ env.CI_PROJECT_DIR }} -e COMMIT_ENV=true --env-file .env.container --user root -e HOME=/root \ ${{ env.DOCKER_IMAGE_NAME_ENVGENE }}:${{ env.DOCKER_IMAGE_TAG_ENVGENE }} \ bash -c " source /module/venv/bin/activate From b598835fd2e3476df44a7e6e09df6210cc81ecf0 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Thu, 26 Mar 2026 08:53:33 +0000 Subject: [PATCH 112/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index a54b15114..8c9ba4e4c 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -71,9 +71,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.13" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.13" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.13" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.14" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.14" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.14" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 9f06a1d6c..1e09be612 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.13 +version: 1.31.14 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index f8d7f3dc4..92543dc73 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.13 +version: 1.31.14 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index faa845636..de466d536 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.13", + "envgene_version": "1.31.14", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 772cdac94e5375f450bcd5ce1936fb0a967bb299 Mon Sep 17 00:00:00 2001 From: basudev91 Date: Thu, 26 Mar 2026 18:47:06 +0530 Subject: [PATCH 113/161] docs: add nested app_reg_defs in template composition (#1182) * docs: add nested app_reg_defs in template composition * docs: review --------- Co-authored-by: popoveugene --- docs/features/template-composition.md | 29 ++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/docs/features/template-composition.md b/docs/features/template-composition.md index 68159c578..e65b1fe6d 100644 --- a/docs/features/template-composition.md +++ b/docs/features/template-composition.md @@ -5,6 +5,8 @@ - [Proposed Approach](#proposed-approach) - [Key Capabilities](#key-capabilities) - [Detailed Composition Algorithm](#detailed-composition-algorithm) + - [Nested Application and Registry Definitions (appdefs / regdefs)](#nested-application-and-registry-definitions-appdefs--regdefs) + - [Examples (app-reg-defs)](#examples-app-reg-defs) - [Use Cases](#use-cases) - [Case 1](#case-1) - [Case 2](#case-2) @@ -78,13 +80,17 @@ This diagram shows parent and child templates with their components. The color o - Parent templates are regular EnvGene templates needing no special configuration - Supports multi-level composition chains +5. **Application & Registry Definitions composition**: + - `appdefs` and `regdefs` live under `templates/` like other resources. Template composition does not merge YAML across files: if the same path appears in more than one source, the file from the later source in copy order replaces the earlier file entirely. + - See [Nested Application and Registry Definitions (appdefs / regdefs)](#nested-application-and-registry-definitions-appdefs--regdefs) below for precedence and how this differs from field-level merge. + ### Detailed Composition Algorithm The sequence below describes how composition is executed during template build. 1. **Discover descriptors** - Read all `*.yml|*.yaml` files from `templates/env_templates` (top-level only, non-recursive). + Read all `*.yml|*.yaml` files from `templates/env_templates` (top-level only, non-recursive). Each discovered file is processed as an independent child Template Descriptor. 2. **Check whether composition is needed** @@ -156,6 +162,27 @@ The sequence below describes how composition is executed during template build. > Effective precedence is: > **namespace parents** - **tenant parent** - **cloud parent** - **child template files**. +### Nested Application and Registry Definitions (appdefs / regdefs) + +Application Definitions (`appdefs`) and Registry Definitions (`regdefs`) can be shipped in parent templates, child templates, or both. Bringing them together during template composition is file-based, not a content merge: overlapping paths are resolved by replacement (last copy wins). + +- **Different relative paths** (for example parent `.../billing-app.yml` and child `.../oss-app.yml`) all remain in the composed artifact; each file is kept as authored. + +- **Same relative path** in more than one source (for example parent and child both ship `templates/appdefs/my-app.yml`): the whole file from the winning source replaces the other. There is **no** field-by-field merge of two YAML documents into one definition. To customize a parent's app or registry file in the child, provide the complete desired file at that path in a source that copies after the parent. + +- Copy order matches template composition precedence: **namespace parents** → **tenant parent** → **cloud parent** → **child template files**. The last source in that order for a given path is what ends up in the built template artifact. + +> [!NOTE] +> After composition, the instance pipeline still runs `app_reg_def_process` on the resulting definition files. +> That pipeline behavior is documented in [app-reg-defs](/docs/features/app-reg-defs.md). +> It does not reintroduce a cross-parent merge for two files that shared the same path. + +#### Examples (app-reg-defs) + +1. Parent ships `templates/appdefs/my-app.yml` and the child adds `templates/appdefs/my-app.yml` with the same relative path. The child's file replaces the parent's. If you only need a small change, duplicate the parent content into the child file and edit the full document there - partial overlays are not applied automatically. + +2. Parent defines one application in `templates/appdefs/app-a.yml` and the child adds `templates/appdefs/app-b.yml`. Both paths are distinct, so both files appear in the composed template. + ### Use Cases This feature can be used in scenarios where EnvGene manages configuration parameters for complex solutions consisting of multiple applications or application groups, with parameters developed and tested by different teams. From 6fc687ebe624d12be736b6029d7cd8d32303257d Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Mon, 30 Mar 2026 10:51:54 +0300 Subject: [PATCH 114/161] feat: Add Gitlab CI pipeline for extend Github workflow (#1191) Added the gitlab ci for extend the github pipeline --- .../.gitignore | 10 + .../.gitlab-ci.yml | 27 + .../extend_github_instance_pipeline/README.md | 373 +++++++++++++ .../scripts/apply_envgene_patch.py | 513 ++++++++++++++++++ .../scripts/git_commit.py | 108 ++++ .../.github/workflows/Envgene.yml | 32 +- .../gitlab-ci/pipeline_vars.yaml | 2 +- 7 files changed, 1048 insertions(+), 17 deletions(-) create mode 100644 extend_github_pipeline/extend_github_instance_pipeline/.gitignore create mode 100644 extend_github_pipeline/extend_github_instance_pipeline/.gitlab-ci.yml create mode 100644 extend_github_pipeline/extend_github_instance_pipeline/README.md create mode 100644 extend_github_pipeline/extend_github_instance_pipeline/scripts/apply_envgene_patch.py create mode 100644 extend_github_pipeline/extend_github_instance_pipeline/scripts/git_commit.py diff --git a/extend_github_pipeline/extend_github_instance_pipeline/.gitignore b/extend_github_pipeline/extend_github_instance_pipeline/.gitignore new file mode 100644 index 000000000..c9150660e --- /dev/null +++ b/extend_github_pipeline/extend_github_instance_pipeline/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +.gradle +venv +.venv +.lint_venv +.git_hook_venv +.idea +/.project +/encryption_key +secret_key.txt \ No newline at end of file diff --git a/extend_github_pipeline/extend_github_instance_pipeline/.gitlab-ci.yml b/extend_github_pipeline/extend_github_instance_pipeline/.gitlab-ci.yml new file mode 100644 index 000000000..0eb1e4a01 --- /dev/null +++ b/extend_github_pipeline/extend_github_instance_pipeline/.gitlab-ci.yml @@ -0,0 +1,27 @@ +--- +#Variables +variables: + INSTANCE_REPO_PIPELINE_IMAGE: DOCKER_IMAGE_NAME + INSTANCE_REPO_PIPELINE_IMAGE_TAG: DOCKER_IMAGE_TAG + +#Rules +workflow: + rules: + - if: '$CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event"' + when: never + - when: always + +#Stages +stages: + - extend-pipeline + +extend-the-gh-pipeline: + stage: extend-pipeline + image: + name: ${INSTANCE_REPO_PIPELINE_IMAGE}:${INSTANCE_REPO_PIPELINE_IMAGE_TAG} + script: + - python3 scripts/apply_envgene_patch.py PATH_TO_COMPONENT + - python3 scripts/git_commit.py + artifacts: + paths: + - extended_github_instance_pipeline/ diff --git a/extend_github_pipeline/extend_github_instance_pipeline/README.md b/extend_github_pipeline/extend_github_instance_pipeline/README.md new file mode 100644 index 000000000..22b3e13eb --- /dev/null +++ b/extend_github_pipeline/extend_github_instance_pipeline/README.md @@ -0,0 +1,373 @@ +# Extension Pipeline & apply_envgene_patch Documentation + +This document describes the GitLab CI extension pipeline and the `apply_envgene_patch` script used to customize the EnvGene GitHub Actions workflow for your instance. + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Pipeline Flow](#pipeline-flow) +3. [apply_envgene_patch Script](#apply_envgene_patch-script) +4. [Patch File Format](#patch-file-format) +5. [Operations Reference](#operations-reference) +6. [Examples](#examples) + +--- + +## Overview + +The pipeline uses the Docker image `qubership-instance-repo-pipeline` from [Netcracker/qubership-envgene](https://github.com/Netcracker/qubership-envgene) and extends the base EnvGene workflow by adding new variables and components to `Envgene.yml` — as **steps** or **jobs** depending on the project. + +**Flow:** + +1. **Init & apply** — `apply_envgene_patch.py` removes old output, copies base workflow from `/opt/github` to `extended_github_instance_pipeline/`, then applies YAML patch files (components) that add variables and workflow steps/jobs +2. **Commit & push** — `git_commit.py` commits and pushes the modified `extended_github_instance_pipeline/` directory + +This allows instance repositories to extend the base EnvGene workflow with custom variables, steps, and configuration without forking the entire workflow. + +--- + +## Pipeline Flow + +The pipeline is defined in `.gitlab-ci.yml` and runs in the `qubership-instance-repo-pipeline` Docker image (from [qubership-envgene](https://github.com/Netcracker/qubership-envgene)): + +```yaml +extend-the-gh-pipeline: + stage: extend-pipeline + image: + name: ${INSTANCE_REPO_PIPELINE_IMAGE}:${INSTANCE_REPO_PIPELINE_IMAGE_TAG} + script: + - python3 scripts/apply_envgene_patch.py components/component-a.yaml components/variables.yaml + - python3 scripts/git_commit.py + artifacts: + paths: + - extended_github_instance_pipeline/ +``` + +**Steps:** + +| Step | Description | +|------|-------------| +| Init & apply | Runs `apply_envgene_patch.py`: removes `extended_github_instance_pipeline/` and `.github/`, copies `/opt/github` to output dir, applies patches (adds variables and steps/jobs to `Envgene.yml`) | +| Commit & push | Commits changes and pushes to the current branch | + +**Requirements:** + +- `GITLAB_TOKEN` with `write_repository` scope (for `git_commit.py`) +- Pipeline runs on schedule or manual trigger (not on push/MR by default) + +--- + +## apply_envgene_patch Script + +**Location:** `scripts/apply_envgene_patch.py` + +**Usage:** + +```bash +# CI / full run (init from /opt/github + apply patches) +python3 scripts/apply_envgene_patch.py components/component-a.yaml components/variables.yaml + +# Local run (skip init, apply patches to existing output dir) +python3 scripts/apply_envgene_patch.py --no-init components/component-a.yaml components/variables.yaml + +# Custom output dir +python3 scripts/apply_envgene_patch.py --output-dir my_pipeline components/component-a.yaml +``` + +**Options:** + +| Option | Description | +|--------|-------------| +| `--output-dir DIR` | Output directory. Maps `target_file` paths starting with `.github/` to this directory. Default: `extended_github_instance_pipeline`. | +| `--init-from DIR` | Before applying patches: remove output-dir and `.github`, copy DIR to output-dir. Default: `/opt/github`. | +| `--no-init` | Skip init step. Use when output-dir already exists (e.g. local runs without `/opt/github`). | + +**Default behavior:** The script first initializes the output directory (removes `extended_github_instance_pipeline/` and `.github/`, copies `/opt/github` to output-dir), then applies patches. Use `--no-init` for local runs. + +**Dependencies:** `ruamel.yaml` (install via `pip install ruamel.yaml`) + +The script reads patch files and applies a sequence of operations to target files. Each operation has an `action` and optional `target_file` (defaults to the first operation's target). Paths like `.github/workflows/Envgene.yml` are resolved to `output_dir/workflows/Envgene.yml`. + +--- + +## Patch File Format + +Patch files are YAML documents containing a list of operations. Use `target_file` paths starting with `.github/` — they are resolved to the output directory (e.g. `.github/workflows/Envgene.yml` → `extended_github_instance_pipeline/workflows/Envgene.yml`): + +```yaml +--- +- target_file: .github/workflows/Envgene.yml + action: merge + section: DOCKER_IMAGE_NAMES + content: + DOCKER_IMAGE_NAME_DEPLOYTOOL: "my-registry/my-image" + +- target_file: .github/configuration/config.env + action: merge + content: + MY_VAR: "value" +``` + +--- + +## Operations Reference + +### 1. Merge (action: `merge`) + +Adds or updates key-value pairs in the target file. + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `target_file` | Yes* | Path to target file | +| `action` | Yes | `merge` | +| `content` | Yes | Dict of key-value pairs | +| `section` | For YAML | Section comment (e.g. `#DOCKER_IMAGE_NAMES`) | +| `path` | For YAML | Dotted path (e.g. `jobs.process_environment_variables.outputs`) | + +**Merge variants:** + +- **.env files** — Merges `KEY=value`; no section/path needed +- **YAML with section** — Merges into block after `#SECTION` comment +- **YAML with path** — Merges into block at dotted path (e.g. `jobs.job_name.outputs`) + +--- + +### 2. Insert (action: `insert`) + +Inserts a block of content at a specific position. Requires exactly one anchor. + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `target_file` | Yes* | Path to target file | +| `action` | Yes | `insert` | +| `content` | Yes | String or YAML block to insert | +| `after_section` | One of | Insert after `### SECTION - END ###` | +| `before_section` | One of | Insert before `### SECTION - START ###` | +| `after_step` | One of | Insert after step with given name | +| `before_step` | One of | Insert before step with given name | +| `skip_if_present` | No | Skip if this string is already in the file | + +**Formatting:** Inserted content gets 2 empty lines before and after; indentation is preserved. + +--- + +## Examples + +### Example 1: Merge Docker image names (section-based) + +Add or override Docker image names in the `#DOCKER_IMAGE_NAMES` section: + +```yaml +--- +- path: env + target_file: .github/workflows/Envgene.yml + action: merge + section: DOCKER_IMAGE_NAMES + content: + DOCKER_IMAGE_NAME_DEPLOYTOOL: "ghcr.io/myorg/deploytool" + DOCKER_IMAGE_NAME_ENVGENE: "ghcr.io/myorg/envgene" +``` + +--- + +### Example 2: Merge job outputs (path-based) + +Add outputs to a specific job: + +```yaml +--- +- path: jobs.process_environment_variables.outputs + target_file: .github/workflows/Envgene.yml + action: merge + content: + MY_CUSTOM_OUTPUT: "custom_value" + ANOTHER_OUTPUT: ${{ env.SOME_VAR }} +``` + +--- + +### Example 3: Merge .env variables + +Add or update variables in configuration files: + +```yaml +--- +- target_file: .github/configuration/config.env + action: merge + content: + CONFIG_VAR_1: "value1" + CONFIG_VAR_2: "value2" + +- target_file: .github/pipeline_vars.env + action: merge + content: + PIPELINE_VAR: "my_value" +``` + +--- + +### Example 4: Insert block after section (with markers) + +Insert a new step block after `### GIT COMMIT - END ###`: + +```yaml +--- +- target_file: .github/workflows/Envgene.yml + action: insert + after_section: "GIT COMMIT" + content: | + ### MY CUSTOM STEP - START ### + - name: My Custom Step + run: echo "Hello from custom step" + ### MY CUSTOM STEP - END ### +``` + +--- + +### Example 5: Insert block before section + +Insert content before a section starts: + +```yaml +--- +- target_file: .github/workflows/Envgene.yml + action: insert + before_section: "BG MANAGE" + content: | + - name: Pre-BG step + run: echo "Running before BG MANAGE" +``` + +--- + +### Example 6: Insert by step name (no section markers) + +Insert a step after "Prepare environment" or before "Create env file for container": + +```yaml +--- +- target_file: .github/workflows/Envgene.yml + action: insert + after_step: "Prepare environment" + content: | + - name: Create name for dynamic secret + run: | + SECRET_NAME=$(echo "${{ matrix.environment }}" | awk -F "/" '{print $1}')_${{ env.SECRET_POSTFIX }} + echo "SECRET_NAME=$SECRET_NAME" >> $GITHUB_ENV +``` + +Or insert before a step: + +```yaml +--- +- target_file: .github/workflows/Envgene.yml + action: insert + before_step: "Create env file for container" + content: | + - name: My step before Create env file + run: echo "pre-step" +``` + +**Step name matching:** Case-insensitive, supports partial match (e.g. `"Prepare environment"` matches `"Prepare environment"`). + +--- + +### Example 7: Skip if already present + +Avoid duplicate insertion: + +```yaml +--- +- target_file: .github/workflows/Envgene.yml + action: insert + after_section: "GIT COMMIT" + skip_if_present: "### MY CUSTOM STEP - START ###" + content: | + ### MY CUSTOM STEP - START ### + - name: My Custom Step + ... +``` + +--- + +### Example 8: Full component file (component-a.yaml) + +```yaml +--- +- path: env + target_file: .github/workflows/Envgene.yml + action: merge + section: DOCKER_IMAGE_NAMES + content: + DOCKER_IMAGE_NAME_DEPLOYTOOL: "MY_VALUE" + +- path: env + target_file: .github/workflows/Envgene.yml + action: merge + section: DOCKER_IMAGE_TAGS + content: + DOCKER_IMAGE_TAG_DEPLOYTOOL: "MY_VALUE" + +- path: jobs.process_environment_variables.outputs + target_file: .github/workflows/Envgene.yml + action: merge + content: + MY_NEW_VARIABLE: "MY_VALUE" + +- path: jobs.envgene_execution.steps + target_file: .github/workflows/Envgene.yml + action: insert + after_section: "GIT COMMIT" + content: | + ### MY CUSTOM STEP - START ### + - name: My Custom Step + if: needs.process_environment_variables.outputs.MY_FEATURE_ENABLED == 'true' + env: + DYNAMIC_SECRET: ${{ secrets[env.SECRET_NAME] }} + run: | + echo "Custom step logic..." + ### MY CUSTOM STEP - END ### +``` + +--- + +## Envgene.yml Structure (Reference) + +The base workflow uses these extension points: + +| Extension point | Type | Example | +|-----------------|------|---------| +| `#DOCKER_IMAGE_NAMES` | Section comment | Merge env vars | +| `#DOCKER_IMAGE_TAGS` | Section comment | Merge env vars | +| `jobs.process_environment_variables.outputs` | Dotted path | Merge job outputs | +| `### SECTION - START ###` / `### SECTION - END ###` | Section markers | Insert steps | +| Step names | `- name: "..."` | Insert by step anchor | + +--- + +## Adding New Patch Files + +1. Create a YAML file in `components/` (e.g. `components/my-feature.yaml`) +1. Add operations following the format above +1. Register the file in `.gitlab-ci.yml`: + +```yaml +- python3 scripts/apply_envgene_patch.py components/component-a.yaml components/variables.yaml components/my-feature.yaml +``` + +1. Patches are applied in order; later patches can override or extend earlier ones. + +--- + +## Troubleshooting + +| Error | Cause | Solution | +|-------|-------|----------| +| `Source directory not found: /opt/github` | Running locally without Docker image | Use `--no-init` and ensure output-dir exists, or `--init-from` a local path | +| `Section #X not found` | Section comment missing in target | Add `#SECTION` comment or use `path` | +| `Marker '### X - END ###' not found` | Section markers missing | Add `### SECTION - START ###` and `### SECTION - END ###` | +| `Step 'X' not found` | Step name doesn't match | Use exact or partial step name (case-insensitive) | +| `Block 'path' not found` | Dotted path invalid | Verify YAML structure (jobs → job_name → outputs) | +| `File not found` | Wrong target path or output-dir missing | Use path relative to repository root; run with init or `--no-init` on existing dir | diff --git a/extend_github_pipeline/extend_github_instance_pipeline/scripts/apply_envgene_patch.py b/extend_github_pipeline/extend_github_instance_pipeline/scripts/apply_envgene_patch.py new file mode 100644 index 000000000..78daf0e15 --- /dev/null +++ b/extend_github_pipeline/extend_github_instance_pipeline/scripts/apply_envgene_patch.py @@ -0,0 +1,513 @@ +#!/usr/bin/env python3 +import re +import shutil +import sys +from pathlib import Path + +try: + from ruamel.yaml import YAML +except ImportError: + print("Error: ruamel.yaml required. Add to Dockerfile: pip install ruamel.yaml", file=sys.stderr) + sys.exit(1) + + +# ========== MERGE ENV: add or replace variables in .env files ========== + +def do_merge_env(target_file, content): + """Add or replace KEY=value in .env file.""" + text = target_file.read_text(encoding="utf-8") + lines = text.splitlines(keepends=True) + + # Parse existing: key -> (line_index, full_line) + existing = {} + for i, line in enumerate(lines): + s = line.strip() + if s and not s.startswith("#") and "=" in s: + key = s.split("=", 1)[0].strip() + if key: + existing[key] = (i, line) + + to_add = {k: v for k, v in content.items() if k not in existing} + to_update = {k: v for k, v in content.items() if k in existing} + + if not to_add and not to_update: + return "skipped (no changes)" + + def format_env(key, val): + val_str = str(val).strip('"\'') + return f'{key}="{val_str}"' if " " in val_str or "\n" in val_str else f"{key}={val_str}" + + # Replace existing + for key, value in to_update.items(): + idx, _ = existing[key] + lines[idx] = format_env(key, value) + "\n" + + # Append new at end (no extra blank line) + if to_add: + new_lines = "".join(format_env(k, v) + "\n" for k, v in to_add.items()) + lines.append(new_lines) + + target_file.write_text("".join(lines), encoding="utf-8") + + parts = [] + if to_update: + parts.append(f"updated {list(to_update.keys())}") + if to_add: + parts.append(f"inserted {list(to_add.keys())}") + return "; ".join(parts) + + +# ========== MERGE YAML PATH: add keys to block at dotted path (no section needed) ========== + +def _find_block_by_path(lines, path_str): + """Find (line_index, content_indent) of block at path like jobs.process_environment_variables.outputs.""" + parts = path_str.split(".") + search_start = 0 + block_line = None + content_indent = 2 + prev_indent = -1 + + for part in parts: + found = None + for i in range(search_start, len(lines)): + line = lines[i] + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + line_indent = len(line) - len(line.lstrip()) + if ":" in stripped and not stripped.startswith("-"): + key = stripped.split(":")[0].strip() + if key == part and line_indent > prev_indent: + found = (i, line_indent) + search_start = i + 1 + break + if found is None: + return None, 2 + block_line, prev_indent = found + content_indent = prev_indent + 2 + + return block_line, content_indent + + +def do_merge_yaml_path(target_file, content, path_str): + """Merge content into YAML block at dotted path (e.g. jobs.process_environment_variables.outputs).""" + lines = target_file.read_text(encoding="utf-8").splitlines(keepends=True) + + block_line, block_indent = _find_block_by_path(lines, path_str) + if block_line is None: + raise ValueError(f"Block '{path_str}' not found") + + block_key_indent = block_indent - 2 + existing = {} + for i in range(block_line + 1, len(lines)): + line = lines[i] + line_indent = len(line) - len(line.lstrip()) + if line.strip() and line_indent <= block_key_indent: + break + s = line.strip() + if s and not s.startswith("#") and ":" in s and not s.startswith("-"): + key = s.split(":")[0].strip() + if key: + existing[key] = (i, line) + + to_add = {k: v for k, v in content.items() if k not in existing} + to_update = {k: v for k, v in content.items() if k in existing} + + if not to_add and not to_update: + return "skipped (no changes)" + + yaml = YAML() + yaml.preserve_quotes = True + yaml.width = 4096 + + for key, value in to_update.items(): + idx, old_line = existing[key] + indent = len(old_line) - len(old_line.lstrip()) + from io import StringIO + buf = StringIO() + yaml.dump({key: value}, buf) + formatted = buf.getvalue().rstrip().split("\n") + new_line = "".join(" " * indent + s + "\n" for s in formatted) + lines[idx] = new_line + + if to_add: + from io import StringIO + buf = StringIO() + yaml.dump(to_add, buf) + text = buf.getvalue().rstrip() + spaces = " " * block_indent + indented = "\n".join(spaces + (s.lstrip() if s.startswith(" ") else s) for s in text.split("\n")) + lines.insert(block_line + 1, indented + "\n") + + target_file.write_text("".join(lines), encoding="utf-8") + + parts = [] + if to_update: + parts.append(f"updated {list(to_update.keys())}") + if to_add: + parts.append(f"inserted {list(to_add.keys())}") + return "; ".join(parts) + + +# ========== MERGE: add or replace keys after comment #SECTION (YAML) ========== + +def do_merge(target_file, content, section): + lines = target_file.read_text(encoding="utf-8").splitlines(keepends=True) + + # Find the line with comment (e.g. #DOCKER_IMAGE_NAMES) + section_line = None + indent = 2 + for i, line in enumerate(lines): + text = line.strip() + if text.startswith("#") and section.upper() in text.upper(): + section_line = i + indent = len(line) - len(line.lstrip()) + break + + if section_line is None: + raise ValueError(f"Section #{section} not found") + + # Collect all existing keys in file: key -> (line_number, line) + existing = {} + for i, line in enumerate(lines): + text = line.strip() + if text and not text.startswith("#") and ":" in text and not text.startswith("-"): + key = text.split(":")[0].strip() + if key: + existing[key] = (i, line) + + # Split: what to add, what to replace + to_add = {k: v for k, v in content.items() if k not in existing} + to_update = {k: v for k, v in content.items() if k in existing} + + if not to_add and not to_update: + return "skipped (no changes)" + + yaml = YAML() + yaml.preserve_quotes = True + yaml.width = 4096 + + # Replace existing keys + for key, value in to_update.items(): + idx, old_line = existing[key] + spaces = " " * (len(old_line) - len(old_line.lstrip())) + from io import StringIO + buf = StringIO() + yaml.dump({key: value}, buf) + formatted = buf.getvalue().rstrip().split("\n") + new_line = "".join(spaces + s + "\n" for s in formatted) + lines[idx] = new_line + + # Insert new keys after section comment + if to_add: + from io import StringIO + buf = StringIO() + yaml.dump(to_add, buf) + text = buf.getvalue().rstrip() + spaces = " " * max(indent, 2) + indented = "\n".join(spaces + (s.lstrip() if s.startswith(" ") else s) for s in text.split("\n")) + lines.insert(section_line + 1, indented + "\n") + + target_file.write_text("".join(lines), encoding="utf-8") + + parts = [] + if to_update: + parts.append(f"updated {list(to_update.keys())}") + if to_add: + parts.append(f"inserted {list(to_add.keys())}") + return "; ".join(parts) + + +# ========== INSERT: insert block after/before ### SECTION ### or by step name ========== + +def find_insert_position(lines, section, after=True): + """Find position and indent of marker line. + after=True: insert after ### SECTION - END ### + after=False: insert before ### SECTION - START ### + Returns (insert_pos, base_indent).""" + marker = f"### {section} - {'END' if after else 'START'} ###" + for i, line in enumerate(lines): + if marker in line.strip(): + indent = len(line) - len(line.lstrip()) + # after: insert at i+1; before: insert at i + pos = i + 1 if after else i + return pos, indent + return None, 0 + + +def find_step_position(lines, step_name, after=True): + """Find insert position by step name (e.g. 'Create env file for container'). + after=True: insert after the step ends; after=False: insert before the step starts. + Step name matching is case-insensitive and supports partial match. + Returns (insert_pos, base_indent) or (None, 0).""" + step_indent = None + step_start = None + step_end = None + step_name_lower = step_name.lower().strip() + + for i, line in enumerate(lines): + stripped = line.strip() + # Match step: - name: "..." or - name: '...' or - name: ... + m = re.match(r'^-\s*name\s*:\s*(?:"([^"]*)"|\'([^\']*)\'|(\S.*?))\s*$', stripped) + if m: + name = (m.group(1) or m.group(2) or m.group(3) or "").strip() + if step_name_lower in name.lower() or name.lower() in step_name_lower: + line_indent = len(line) - len(line.lstrip()) + step_start = i + step_indent = line_indent + # Step continues until next step (- name:) at same indent + for j in range(i + 1, len(lines)): + next_line = lines[j] + next_stripped = next_line.strip() + if not next_stripped: + continue + next_indent = len(next_line) - len(next_line.lstrip()) + # Next step: "- name:" at same indent level + if (next_indent <= step_indent and next_stripped.startswith("-") and + re.match(r'^-\s*name\s*:', next_stripped)): + step_end = j + break + if step_end is None: + step_end = len(lines) + break + + if step_start is None: + return None, 0 + + base_indent = step_indent if step_indent is not None else 6 + if after: + return step_end, base_indent + return step_start, base_indent + + +def fix_indent(text, spaces=4): + lines = text.split("\n") + min_indent = min((len(s) - len(s.lstrip()) for s in lines if s.strip()), default=0) + result = [] + for s in lines: + if s.strip(): + stripped = s[min_indent:] if min_indent else s + result.append(" " * spaces + stripped) + else: + result.append("") + return "\n".join(result) + + +def _make_insertion(content, base_indent, pos, total_lines): + """Build insertion string with 2 empty lines before and after content.""" + content = fix_indent(content.rstrip(), spaces=max(base_indent, 4)) + return "\n\n" + content + "\n\n" + + +def do_insert(target_file, content, after_section=None, before_section=None, + after_step=None, before_step=None, skip_if=None): + """Insert content at position determined by section marker or step name. + Exactly one of after_section, before_section, after_step, before_step must be set.""" + text = target_file.read_text(encoding="utf-8") + if skip_if and skip_if in text: + return "skipped (already present)" + + lines = text.splitlines(keepends=True) + pos = None + base_indent = 0 + + if after_section: + pos, base_indent = find_insert_position(lines, after_section, after=True) + if pos is None: + raise ValueError(f"Marker '### {after_section} - END ###' not found") + elif before_section: + pos, base_indent = find_insert_position(lines, before_section, after=False) + if pos is None: + raise ValueError(f"Marker '### {before_section} - START ###' not found") + elif after_step: + pos, base_indent = find_step_position(lines, after_step, after=True) + if pos is None: + raise ValueError(f"Step '{after_step}' not found") + elif before_step: + pos, base_indent = find_step_position(lines, before_step, after=False) + if pos is None: + raise ValueError(f"Step '{before_step}' not found") + else: + raise ValueError("One of after_section, before_section, after_step, before_step required") + + insertion = _make_insertion(content, base_indent, pos, len(lines)) + lines.insert(pos, insertion) + target_file.write_text("".join(lines), encoding="utf-8") + return "inserted" + + +# ========== MAIN LOGIC ========== + +def apply_patch(patch_path, base_dir): + yaml = YAML() + yaml.preserve_quotes = True + yaml.width = 4096 + + with open(patch_path, encoding="utf-8") as f: + operations = yaml.load(f) + + if not isinstance(operations, list): + operations = [operations] + + def resolve_target(target_file_str): + """Resolve target file path. If base_dir is output dir, map .github/ -> output_dir/.""" + if target_file_str.startswith(".github/") and base_dir != Path("."): + return base_dir / target_file_str[len(".github/"):] + return base_dir / target_file_str + + # Default target from first operation (for operations without own target_file) + default_target = None + for op in operations: + if op.get("target_file"): + default_target = resolve_target(op["target_file"]) + break + + if not default_target: + raise ValueError("target_file required in patch (e.g. .github/workflows/Envgene.yml)") + + result = [] + + for i, op in enumerate(operations): + action = op.get("action") + path_str = op.get("path") or op.get("target_file", "file") + # Each operation can have its own target_file + target_file = ( + resolve_target(op["target_file"]) if op.get("target_file") else default_target + ) + + if not target_file.is_file(): + raise FileNotFoundError(f"File not found: {target_file}") + + if not action: + print(f" [Skip] Operation {i+1}: missing action", file=sys.stderr) + continue + + if action == "merge": + section = op.get("section") + content = op.get("content") or {} + if not isinstance(content, dict): + print(f" [Skip] Operation {i+1}: content must be dict", file=sys.stderr) + continue + # .env files: merge without section (KEY=value format) + if not section and str(target_file).endswith(".env"): + msg = do_merge_env(target_file, content) + result.append(f"merge env {path_str} ({msg})") + elif section: + msg = do_merge(target_file, content, section) + result.append(f"merge {path_str} ({msg})") + elif path_str and path_str != "file": + # YAML merge by path (e.g. jobs.process_environment_variables.outputs) + msg = do_merge_yaml_path(target_file, content, path_str) + result.append(f"merge {path_str} ({msg})") + else: + print(f" [Skip] Operation {i+1}: merge requires section, path, or .env target", file=sys.stderr) + + elif action == "insert": + after_section = op.get("after_section") + before_section = op.get("before_section") + after_step = op.get("after_step") + before_step = op.get("before_step") + content = op.get("content") + skip_if = op.get("skip_if_present") + + anchor_count = sum(1 for x in (after_section, before_section, after_step, before_step) if x) + if anchor_count != 1: + print( + f" [Skip] Operation {i+1}: insert requires exactly one of " + "after_section, before_section, after_step, before_step", + file=sys.stderr + ) + continue + if content is None: + print(f" [Skip] Operation {i+1}: insert requires content", file=sys.stderr) + continue + if isinstance(content, dict): + from io import StringIO + buf = StringIO() + yaml.dump(content, buf) + content = buf.getvalue() + + msg = do_insert( + target_file, content, + after_section=after_section, before_section=before_section, + after_step=after_step, before_step=before_step, skip_if=skip_if + ) + anchor = after_section or before_section or after_step or before_step + if after_section: + anchor_type = "after_section" + elif before_section: + anchor_type = "before_section" + elif after_step: + anchor_type = "after_step" + else: + anchor_type = "before_step" + result.append(f"insert {anchor_type} '{anchor}' ({msg})") + + else: + print(f" [Skip] Operation {i+1}: unknown action '{action}'", file=sys.stderr) + + return result + + +def init_output_dir(output_dir, source_dir): + """Remove output_dir and .github, then copy source_dir into output_dir.""" + output_path = Path(output_dir) + source_path = Path(source_dir) + if not source_path.is_dir(): + raise FileNotFoundError(f"Source directory not found: {source_path}") + for path in (output_path, Path(".github")): + if path.exists(): + shutil.rmtree(path) + shutil.copytree(source_path, output_path) + print(f"Initialized {output_dir} from {source_dir}") + + +def main(): + import argparse + parser = argparse.ArgumentParser(description="Apply YAML patch to Envgene.yml") + parser.add_argument( + "--output-dir", + default="extended_github_instance_pipeline", + help="Output directory. Maps .github/ paths to this directory. " + "Default: extended_github_instance_pipeline.", + ) + parser.add_argument( + "--init-from", + default="/opt/github", + help="Before applying patches, remove output-dir and copy this source into it. " + "Default: /opt/github. Use --no-init to skip.", + ) + parser.add_argument( + "--no-init", + action="store_true", + help="Skip init step (do not rm/cp). Use when output-dir already exists.", + ) + parser.add_argument( + "patch", + nargs="+", + help="Patch file(s) (e.g. components/component-a.yaml components/variables.yaml)", + ) + args = parser.parse_args() + + base = Path(args.output_dir) + all_results = [] + + try: + if not args.no_init: + init_output_dir(args.output_dir, args.init_from) + + for patch_path in args.patch: + patch_path = Path(patch_path) + if not patch_path.is_file(): + raise FileNotFoundError(f"File not found: {patch_path}") + all_results.extend(apply_patch(patch_path, base)) + print("Applied:") + for r in all_results: + print(f" - {r}") + except (FileNotFoundError, ValueError, KeyError) as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/extend_github_pipeline/extend_github_instance_pipeline/scripts/git_commit.py b/extend_github_pipeline/extend_github_instance_pipeline/scripts/git_commit.py new file mode 100644 index 000000000..323978f11 --- /dev/null +++ b/extend_github_pipeline/extend_github_instance_pipeline/scripts/git_commit.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +""" +git_commit.py - Commit and push changes from pipeline (GitLab CI). + +Requires: GITLAB_TOKEN, CI_PROJECT_DIR +Optional: GITLAB_CI_USER (default: oauth2), CI_SERVER_HOST, CI_PROJECT_PATH, CI_COMMIT_REF_NAME + PIPELINE_OUTPUT_DIR (default: extended_github_instance_pipeline) - output folder name for commit message +""" + +import os +import subprocess +import sys + + +def run(cmd, check=True, env=None): + """Run command (list of args), return completed process.""" + env = env or os.environ.copy() + result = subprocess.run(cmd, env=env) + if check and result.returncode != 0: + sys.exit(result.returncode) + return result + + +def main(): + token = os.environ.get("GITLAB_TOKEN") + if not token: + print("Error: GITLAB_TOKEN is not set", file=sys.stderr) + sys.exit(1) + + project_dir = os.environ.get("CI_PROJECT_DIR") + if not project_dir: + print("Error: CI_PROJECT_DIR is not set", file=sys.stderr) + sys.exit(1) + + os.chdir(project_dir) + + ci_user = os.environ.get("GITLAB_CI_USER", "oauth2") + server_host = os.environ.get("CI_SERVER_HOST", "gitlab.com") + project_path = os.environ.get("CI_PROJECT_PATH") + ref_name = os.environ.get("CI_COMMIT_REF_NAME", "main") + + # CI_PROJECT_PATH not set (e.g. running locally) — derive from git remote + if not project_path: + result = subprocess.run( + ["git", "remote", "get-url", "origin"], + capture_output=True, + text=True, + ) + if result.returncode == 0 and result.stdout.strip(): + url = result.stdout.strip() + # Parse https://host/ns/proj.git or git@host:ns/proj.git + if "://" in url: + path = url.split("://", 1)[1].split("/", 1)[-1] + elif ":" in url: + path = url.split(":", 1)[1] + else: + path = "" + path = path.rstrip("/").removesuffix(".git").strip("/") + if path and path != ".git": + project_path = path + if not project_path: + print( + "Error: CI_PROJECT_PATH not set and could not derive from git remote.\n" + " Set CI_PROJECT_PATH (e.g. 'group/project') or fix origin:\n" + " git remote set-url origin https://gitlab.com/OWNER/REPO.git", + file=sys.stderr, + ) + sys.exit(1) + + print(f"Push: token OK, user={ci_user}, pwd={os.getcwd()}", flush=True) + + env = os.environ.copy() + env["HOME"] = "/tmp" + + run(["git", "config", "--global", "--add", "safe.directory", project_dir], env=env) + run(["git", "config", "user.email", "ci@gitlab"], env=env) + run(["git", "config", "user.name", "GitLab CI"], env=env) + + origin_url = f"https://{ci_user}:{token}@{server_host}/{project_path}.git" + run(["git", "remote", "set-url", "origin", origin_url], env=env) + + run(["git", "add", "."], env=env) + + result = subprocess.run( + ["git", "diff", "--staged", "--quiet"], + env=env, + capture_output=True, + ) + + if result.returncode != 0: + output_dir = os.environ.get("PIPELINE_OUTPUT_DIR", "extended_github_instance_pipeline") + run(["git", "commit", "-m", f"chore: update {output_dir} from pipeline"], env=env) + result = subprocess.run( + ["git", "push", "origin", f"HEAD:{ref_name}"], + env=env, + ) + if result.returncode != 0: + print( + "Error: git push failed - check token scope (write_repository)", + file=sys.stderr, + ) + sys.exit(1) + else: + print("No changes to commit") + + +if __name__ == "__main__": + main() diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 8c9ba4e4c..4c3a807cc 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -199,7 +199,7 @@ jobs: ### ENV VARS PROCESSING - END ### - ### BG MANAGE ### + ### BG MANAGE - START ### - name: BG_MANAGE if: needs.process_environment_variables.outputs.BG_MANAGE == 'true' env: @@ -224,10 +224,10 @@ jobs: name: bg_manage_${{ env.PACKAGE_NAME }} path: environments/${{ matrix.environment }} include-hidden-files: true - ########################## + ### BG MANAGE - END ### - ### ENV_INVENTORY_GENERATION ### + ### ENV_INVENTORY_GENERATION - START ### - name: ENV_INVENTORY_GENERATION if: (needs.process_environment_variables.outputs.ENV_INVENTORY_CONTENT != '') || (needs.process_environment_variables.outputs.ENV_SPECIFIC_PARAMS != '' || needs.process_environment_variables.outputs.ENV_TEMPLATE_NAME != '') run: | @@ -250,10 +250,10 @@ jobs: name: env_inventory_generation_${{ env.PACKAGE_NAME }} path: environments/${{ matrix.environment }} include-hidden-files: true - ########################## + ### ENV_INVENTORY_GENERATION - END ### - ### CREDENTIAL ROTATION ### + ### CREDENTIAL ROTATION - START ### - name: CREDENTIAL_ROTATION if: needs.process_environment_variables.outputs.CRED_ROTATION_PAYLOAD != '' run: | @@ -276,10 +276,10 @@ jobs: name: credential_rotation_${{ env.PACKAGE_NAME }} path: environments/${{ matrix.environment }} include-hidden-files: true - ########################## + ### CREDENTIAL ROTATION - END ### - ### APP_REG_DEF_PROCESS ### + ### APP_REG_DEF_PROCESS - START ### - name: APP_REG_DEF_PROCESS if: needs.process_environment_variables.outputs.ENV_BUILDER == 'true' run: | @@ -304,10 +304,10 @@ jobs: configuration tmp include-hidden-files: true - ########################## + ### APP_REG_DEF_PROCESS - END ### - ### PROCESS_SD ### + ### PROCESS_SD - START ### - name: PROCESS_SD if: (needs.process_environment_variables.outputs.SD_SOURCE_TYPE == 'json' && needs.process_environment_variables.outputs.SD_DATA != '') || (needs.process_environment_variables.outputs.SD_SOURCE_TYPE == 'artifact' && needs.process_environment_variables.outputs.SD_VERSION != '') run: | @@ -336,10 +336,10 @@ jobs: name: process_sd_${{ env.PACKAGE_NAME }} path: environments/${{ matrix.environment }} include-hidden-files: true - ################## + ### PROCESS_SD - END ### - ### ENV_BUILD ### + ### ENV_BUILD - START ### - name: ENV_BUILD if: needs.process_environment_variables.outputs.ENV_BUILDER == 'true' run: | @@ -363,10 +363,10 @@ jobs: configuration tmp include-hidden-files: true - ########################## + ### ENV_BUILD - END ### - ### GENERATE EFFECTIVE SET ### + ### GENERATE EFFECTIVE SET - START ### - name: GENERATE_EFFECTIVE_SET if: needs.process_environment_variables.outputs.GENERATE_EFFECTIVE_SET == 'true' run: | @@ -413,10 +413,10 @@ jobs: sboms configuration include-hidden-files: true - ########################## + ### GENERATE EFFECTIVE SET - END ### - ### GIT COMMIT ### + ### GIT COMMIT - START ### - name: GIT_COMMIT run: | docker run --rm -v ${{ github.workspace }}:${{ env.CI_PROJECT_DIR }} -w ${{ env.CI_PROJECT_DIR }} -e COMMIT_ENV=true --env-file .env.container --user root -e HOME=/root \ @@ -449,4 +449,4 @@ jobs: git_envs sboms include-hidden-files: true - ########################## + ### GIT COMMIT - END ### diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/gitlab-ci/pipeline_vars.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/gitlab-ci/pipeline_vars.yaml index cc536ab14..86b02682c 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/gitlab-ci/pipeline_vars.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/gitlab-ci/pipeline_vars.yaml @@ -35,4 +35,4 @@ variables: description: "Will EnvGene generate Effective set" options: - "true" - - "false" + - "false" \ No newline at end of file From 9c8fc931537aa51bee5e3fd4c83b9436bfe84c92 Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:49:34 +0300 Subject: [PATCH 115/161] feat: Added empty folder to github_extend --- .../components/components_to_add.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 extend_github_pipeline/extend_github_instance_pipeline/components/components_to_add.yaml diff --git a/extend_github_pipeline/extend_github_instance_pipeline/components/components_to_add.yaml b/extend_github_pipeline/extend_github_instance_pipeline/components/components_to_add.yaml new file mode 100644 index 000000000..e69de29bb From db825cf1a1a78159c0e256c5489584aa3453018e Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:40:43 +0300 Subject: [PATCH 116/161] feat: Updated Envgene yaml (#1197) --- .../.github/workflows/Envgene.yml | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 4c3a807cc..2bf2ecd26 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -208,6 +208,7 @@ jobs: docker run --rm -v ${{ github.workspace }}:${{ env.CI_PROJECT_DIR }} -w ${{ env.CI_PROJECT_DIR }} -e GITHUB_TOKEN -e FULL_ENV_NAME -e CI_PROJECT_DIR --user root --env-file .env.container --user root -e HOME=/root \ ${{ env.DOCKER_IMAGE_NAME_ENVGENE }}:${{ env.DOCKER_IMAGE_TAG_ENVGENE }} \ bash -c " + set -eo pipefail source /module/venv/bin/activate echo 'Executing BG Manage...' @@ -234,6 +235,7 @@ jobs: docker run --rm -v ${{ github.workspace }}:${{ env.CI_PROJECT_DIR }} -w ${{ env.CI_PROJECT_DIR }} --env-file .env.container --user root -e HOME=/root \ ${{ env.DOCKER_IMAGE_NAME_ENVGENE }}:${{ env.DOCKER_IMAGE_TAG_ENVGENE }} \ bash -c " + set -eo pipefail source /module/venv/bin/activate echo 'Executing inventory generation...' @@ -260,6 +262,7 @@ jobs: docker run --rm -v ${{ github.workspace }}:${{ env.CI_PROJECT_DIR }} -w ${{ env.CI_PROJECT_DIR }} --env-file .env.container --user root -e HOME=/root \ ${{ env.DOCKER_IMAGE_NAME_ENVGENE }}:${{ env.DOCKER_IMAGE_TAG_ENVGENE }} \ bash -c " + set -eo pipefail source /module/venv/bin/activate echo 'Executing credential rotation...' @@ -286,6 +289,7 @@ jobs: docker run --rm -v ${{ github.workspace }}:${{ env.CI_PROJECT_DIR }} -w ${{ env.CI_PROJECT_DIR }} --env-file .env.container --user root -e HOME=/root \ ${{ env.DOCKER_IMAGE_NAME_ENVGENE }}:${{ env.DOCKER_IMAGE_TAG_ENVGENE }} \ bash -c " + set -eo pipefail source /module/venv/bin/activate echo 'Executing APP_REG_DEF_PROCESS...' python3 /module/scripts/utils/log_pipe_params.py @@ -314,6 +318,7 @@ jobs: docker run --rm -v ${{ github.workspace }}:${{ env.CI_PROJECT_DIR }} -w ${{ env.CI_PROJECT_DIR }} --env-file .env.container --user root -e HOME=/root \ ${{ env.DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR }}:${{ env.DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR }} \ bash -c " + set -eo pipefail source /module/venv/bin/activate echo 'Executing PROCESS_SD...' @@ -346,11 +351,13 @@ jobs: docker run --rm -v ${{ github.workspace }}:${{ env.CI_PROJECT_DIR }} -w ${{ env.CI_PROJECT_DIR }} -e COMMIT_ENV=true --env-file .env.container --user root -e HOME=/root \ ${{ env.DOCKER_IMAGE_NAME_ENVGENE }}:${{ env.DOCKER_IMAGE_TAG_ENVGENE }} \ bash -c " + set -eo pipefail source /module/venv/bin/activate echo 'Executing ENV_BUILD...' python /module/scripts/utils/log_pipe_params.py /module/scripts/utils/handle_certs.sh - cd /build_env; python3 /build_env/scripts/build_env/main.py + cd /build_env + python3 /build_env/scripts/build_env/main.py " - name: ENV_BUILD - Upload Build Env Package @@ -373,22 +380,32 @@ jobs: docker run --rm -v ${{ github.workspace }}:${{ env.CI_PROJECT_DIR }} -w ${{ env.CI_PROJECT_DIR }} --env-file .env.container --user root -e HOME=/root \ ${{ env.DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR }}:${{ env.DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR }} \ bash -c " + set -eo pipefail echo 'Executing effective set generation...' source /module/venv/bin/activate /module/scripts/utils/handle_certs.sh mkdir -p \"\${CI_PROJECT_DIR}/configuration/certs\" [ -f /default_cert.pem ] && cp /default_cert.pem \"\${CI_PROJECT_DIR}/configuration/certs/default_cert.pem\" || true for cert in \"\${CI_PROJECT_DIR}/configuration/certs/\"*; do [ -f \"\$cert\" ] && keytool -import -trustcacerts -alias \"\$(basename \"\$cert\")\" -file \"\$cert\" -keystore /etc/ssl/certs/keystore.jks -storepass changeit -noprompt; done - python3 /module/scripts/main.py decrypt_cred_files + if ! python3 /module/scripts/main.py decrypt_cred_files; then + echo 'decrypt_cred_files failed with exit code '\$? + exit 1 + fi base_env_path=\"\${CI_PROJECT_DIR}/environments/${{ matrix.environment }}\" app_defs_path=\"\${base_env_path}/AppDefs\" reg_defs_path=\"\${base_env_path}/RegDefs\" if [ -n \"\$APP_REG_DEFS_JOB\" ] && [ -n \"\$APP_DEFS_PATH\" ]; then mkdir -p \"\$app_defs_path\" && cp -rf \"\$APP_DEFS_PATH\"/* \"\$app_defs_path\"; fi if [ -n \"\$APP_REG_DEFS_JOB\" ] && [ -n \"\$REG_DEFS_PATH\" ]; then mkdir -p \"\$reg_defs_path\" && cp -fr \"\$REG_DEFS_PATH\"/* \"\$reg_defs_path\"; fi - python3 /module/scripts/main.py validate_creds + if ! python3 /module/scripts/main.py validate_creds; then + echo 'validate_creds failed with exit code '\$? + exit 1 + fi java_cmd=\"/deployments/run-java.sh --env-id=${{ matrix.environment }} --envs-path=\${CI_PROJECT_DIR}/environments --output=\${CI_PROJECT_DIR}/environments/${{ matrix.environment }}/effective-set --registries=\${CI_PROJECT_DIR}/configuration/registry.yml --sboms-path=\${CI_PROJECT_DIR}/sboms --sd-path=\${CI_PROJECT_DIR}/environments/${{ matrix.environment }}/Inventory/solution-descriptor/sd.yaml\" if [ -n \"\$EFFECTIVE_SET_CONFIG\" ]; then - python3 /module/scripts/handle_effective_set_config.py --effective-set-config \"\$EFFECTIVE_SET_CONFIG\" + if ! python3 /module/scripts/handle_effective_set_config.py --effective-set-config \"\$EFFECTIVE_SET_CONFIG\"; then + echo 'handle_effective_set_config failed with exit code '\$? + exit 1 + fi extra_args=\$(jq -r '.extra_args // [] | join(\" \")' /tmp/effective_set_output.json) java_cmd=\"\$java_cmd \$extra_args\" fi @@ -398,7 +415,10 @@ jobs: echo 'Effective set generation failed with exit code '\$? exit 1 fi - python3 /module/scripts/main.py encrypt_cred_files + if ! python3 /module/scripts/main.py encrypt_cred_files; then + echo 'encrypt_cred_files failed with exit code '\$? + exit 1 + fi env_path=\"\${CI_PROJECT_DIR}/environments/${{ matrix.environment }}\" if [ -d \"\$env_path/Credentials\" ]; then chmod -R ugo+rw \"\$env_path/Credentials\"; fi " @@ -422,12 +442,16 @@ jobs: docker run --rm -v ${{ github.workspace }}:${{ env.CI_PROJECT_DIR }} -w ${{ env.CI_PROJECT_DIR }} -e COMMIT_ENV=true --env-file .env.container --user root -e HOME=/root \ ${{ env.DOCKER_IMAGE_NAME_ENVGENE }}:${{ env.DOCKER_IMAGE_TAG_ENVGENE }} \ bash -c " + set -eo pipefail source /module/venv/bin/activate echo 'Prepare git_commit job for \${ENVIRONMENT_NAME}...' /module/scripts/utils/handle_certs.sh # Execute git commit with proper error handling echo 'Executing git commit operations...' - /module/scripts/git_commit.sh + if ! /module/scripts/git_commit.sh; then + echo 'git_commit.sh failed with exit code '\$? + exit 1 + fi env_path=\$(find \"\${CI_PROJECT_DIR}/environments\" -type d -name \"\$env_name\") for path in \$env_path; do From f5cb144692b9d1b98bc37c2e70394a8315378acd Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Wed, 1 Apr 2026 06:36:30 +0000 Subject: [PATCH 117/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 2bf2ecd26..bd37e9b1e 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -71,9 +71,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.14" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.14" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.14" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.15" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.15" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.15" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 1e09be612..dfa73b14e 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.14 +version: 1.31.15 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 92543dc73..5be56808a 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.14 +version: 1.31.15 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index de466d536..fb57125ab 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.14", + "envgene_version": "1.31.15", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 91cde627d4f1f50fea88145ec19134571dda3b77 Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Wed, 1 Apr 2026 19:10:13 +0300 Subject: [PATCH 118/161] feat: Added python and git to instance image (#1200) --- .../instance-repo-pipeline/Dockerfile | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/Dockerfile b/github_workflows/instance-repo-pipeline/Dockerfile index a749e2730..48b4451ed 100644 --- a/github_workflows/instance-repo-pipeline/Dockerfile +++ b/github_workflows/instance-repo-pipeline/Dockerfile @@ -1,13 +1,21 @@ +# syntax=docker/dockerfile:1 #-----------Stage 1----------- -FROM alpine:3.19 AS initial +FROM alpine:3.21 AS initial WORKDIR /workspace + COPY github_workflows/instance-repo-pipeline/.github /opt/github -# Make all shell scripts executable RUN find /opt/github/scripts -type f -name "*.sh" -exec chmod +x {} \; #-----------Stage 2----------- -FROM alpine:3.19 +FROM alpine:3.21 + +LABEL org.opencontainers.image.description="Github Workflows - Envgene Instance pipeline" + +RUN apk add --no-cache python3=3.12.12-r0 py3-pip=24.3.1-r0 git=2.47.3-r0 \ + && pip3 install --no-cache-dir --break-system-packages ruamel.yaml==0.18.6 + COPY --from=initial /opt/github /opt/github + USER 1000 -CMD ["sh", "-c", "sleep infinity"] \ No newline at end of file +CMD ["sh", "-c", "sleep infinity"] From a022b5d8f3b28e9ebf17cc2ddaf4e0a79a95ebe2 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Thu, 2 Apr 2026 07:04:45 +0000 Subject: [PATCH 119/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index bd37e9b1e..a8b07be10 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -71,9 +71,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.15" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.15" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.15" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.16" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.16" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.16" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index dfa73b14e..4285df746 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.15 +version: 1.31.16 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 5be56808a..9a814e53a 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.15 +version: 1.31.16 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index fb57125ab..5aaeccbbc 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.15", + "envgene_version": "1.31.16", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 841f8ef52b7280eb705f93a376ddd8a57320159a Mon Sep 17 00:00:00 2001 From: basudev91 Date: Thu, 2 Apr 2026 13:50:47 +0530 Subject: [PATCH 120/161] fix: gsf instance files override issue in configuration (#1202) --- .../configuration/.gitkeep | 0 .../artifact_definitions/.gitkeep | 0 .../configuration/config.yml | 17 -------------- .../configuration/credentials/.gitkeep | 0 .../configuration/credentials/credentials.yml | 8 ------- .../configuration/integration.yml | 22 ------------------- 6 files changed, 47 deletions(-) create mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/.gitkeep create mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/artifact_definitions/.gitkeep delete mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/config.yml create mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/credentials/.gitkeep delete mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/credentials/credentials.yml delete mode 100644 gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/integration.yml diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/.gitkeep b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/artifact_definitions/.gitkeep b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/artifact_definitions/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/config.yml b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/config.yml deleted file mode 100644 index ffe2508b1..000000000 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/config.yml +++ /dev/null @@ -1,17 +0,0 @@ -# See full reference at https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-configs.md#configyml -# -# Optional -# Parameter defines the encryption mode. Default - `true` -crypt: false -# Optional -# Defines the encryption technology. Default - `Fernet` -# crypt_backend: enum [Fernet, SOPS] -# Optional -# SBOM retention configuration -# sbom_retention: - # Optional, default value - false - # If `true`, SBOM retention will be enabled - # enabled: false - # Optional, default value - 10 - # Number of latest versions to keep per application when enabled - # keep_versions_per_app: 10 \ No newline at end of file diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/credentials/.gitkeep b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/credentials/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/credentials/credentials.yml b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/credentials/credentials.yml deleted file mode 100644 index c1cf3a02e..000000000 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/credentials/credentials.yml +++ /dev/null @@ -1,8 +0,0 @@ -# See full reference at https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-objects.md#credential -# -# Self-token credential for EnvGene to commit changes to this repository -# Referenced in `/configuration/integration.yml` -self-token-cred: - type: secret - data: - secret: "{{cookiecutter.self_token}}" diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/integration.yml b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/integration.yml deleted file mode 100644 index 88f5f9020..000000000 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/configuration/integration.yml +++ /dev/null @@ -1,22 +0,0 @@ -# See full reference at https://github.com/Netcracker/qubership-envgene/blob/main/docs/envgene-configs.md#integrationyml -# -# Optional -# Configuration for Cloud Passport discovery integration -# cp_discovery: - # Optional - # Parameters for GitLab-based discovery repository - # gitlab: - # Mandatory - # Full project path of the discovery repository - # project: string - # Mandatory - # Branch name of the discovery repository - # branch: master - # Mandatory - # Authentication token for the discovery repository - # token: string - -# Mandatory -# Authentication token for EnvGene to access this instance repository -# Required for EnvGene to commit changes -self_token: "${creds.get('self-token-cred').secret}" From e3e27301d3463718113a890e8ae1d5e3b15f7472 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Thu, 2 Apr 2026 08:28:27 +0000 Subject: [PATCH 121/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index a8b07be10..4a13ae06b 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -71,9 +71,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.16" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.16" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.16" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.17" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.17" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.17" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 4285df746..f396d2849 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.16 +version: 1.31.17 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 9a814e53a..b4260a34d 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.16 +version: 1.31.17 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 5aaeccbbc..5d231b162 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.16", + "envgene_version": "1.31.17", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From d0f87cd958b3cb1be6c72a6fb096b3147e149633 Mon Sep 17 00:00:00 2001 From: dysmon <120464230+dysmon@users.noreply.github.com> Date: Fri, 3 Apr 2026 12:41:37 +0500 Subject: [PATCH 122/161] fix: optimize git commit job (#1187) --- build_envgene/scripts/git_commit.sh | 37 +++++++++++++---------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/build_envgene/scripts/git_commit.sh b/build_envgene/scripts/git_commit.sh index 3d5934a3a..e5db60920 100755 --- a/build_envgene/scripts/git_commit.sh +++ b/build_envgene/scripts/git_commit.sh @@ -1,11 +1,9 @@ #!/bin/bash set -e -job=$1 + retries=0 exit_code=0 -pattern="^[A-Z]+-[0-9]+$" - if [ -n "${GITHUB_ACTIONS}" ]; then # Logic for GitHub PLATFORM="github" @@ -28,6 +26,10 @@ elif [ -n "${GITLAB_CI}" ]; then TOKEN="${GITLAB_TOKEN}" fi +if [ -z "${TOKEN}" ]; then + echo "No auth token was found. Please check!" + exit 1 +fi echo "Platform: ${PLATFORM}" echo "Server Protocol: ${SERVER_PROTOCOL}" @@ -37,11 +39,6 @@ echo "Branch/Ref Name: ${REF_NAME}" echo "User Email: ${USER_EMAIL}" echo "User Name: ${USER_NAME}" -if [ -z "${TOKEN}" ]; then - echo "No auth token was found. Please check!" - exit 1 -fi - echo "ENV_NAME=${ENV_NAME}" echo "CLUSTER_NAME=${CLUSTER_NAME}" echo "ENVIRONMENT_NAME=${ENVIRONMENT_NAME}" @@ -52,7 +49,6 @@ echo "DEPLOYMENT_SESSION_ID=${DEPLOY_SESSION_ID}" export ticket_id=${DEPLOYMENT_TICKET_ID} -# commit message if [ -z "${COMMIT_MESSAGE}" ]; then message="${ticket_id} [ci_skip] Update \"${CLUSTER_NAME}/${ENVIRONMENT_NAME}\" environment" else @@ -122,7 +118,7 @@ if [ -d environments ]; then done fi -#Copying cred files modified as part of cred rotation job. +# Copying cred files modified as part of cred rotation job. CREDS_FILE="environments/credfilestoupdate.yml" if [ -f "$CREDS_FILE" ]; then echo "Processing $CREDS_FILE for copying filtered creds..." @@ -174,8 +170,9 @@ echo "Adding remote: ${REMOTE_URL}" git remote add origin "${REMOTE_URL}" -echo "Pulling contents from GIT (branch: ${REF_NAME})" -git pull origin "${REF_NAME}" +echo "Fetching contents from GIT (branch: ${REF_NAME})" +git fetch --depth=1 origin ${REF_NAME} +git switch -C ${REF_NAME} origin/${REF_NAME} # moving back environments folder and committing @@ -301,17 +298,17 @@ if [ "$exit_code" -ne 0 ]; then echo "Waiting ${sleep_time} seconds before retry..." sleep $sleep_time - echo "Pulling latest changes from origin/${REF_NAME}..." - git pull origin "${REF_NAME}" - pull_exit_code=$? + echo "Fetching latest changes from origin/${REF_NAME}..." + git fetch --depth=1 origin "${REF_NAME}" + fetch_exit_code=$? - if [ "$pull_exit_code" -ne 0 ]; then - echo "⚠ Pull failed with exit code: $pull_exit_code, continuing to next retry..." - continue + if [ "$fetch_exit_code" -ne 0 ]; then + echo "⚠ Fetch failed with exit code: $fetch_exit_code" + break fi - echo "Successfully pulled changes. Remote is now at: $(git rev-parse origin/${REF_NAME})" - echo "Local HEAD is at: $(git rev-parse HEAD)" + git reset --soft origin/"${REF_NAME}" + git commit -m "${message}" echo "Attempting push (retry $retries)..." git push origin HEAD:"${REF_NAME}" From ab16a89159761c27543d274f285d870d501d407a Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:42:35 +0300 Subject: [PATCH 123/161] =?UTF-8?q?feat:=20Updated=20the=20link=20in=20AGE?= =?UTF-8?q?NTS=20for=20Di=C3=A1taxis=20Framework=20(#1205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index d955ae96e..45df5629b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -272,7 +272,7 @@ profile: ## Documentation Structure (Diátaxis Framework) -This repository follows the [Diátaxis documentation framework](https://diataxis.fr/). +This repository follows the [Diátaxis documentation framework](https://github.com/evildmp/diataxis-documentation-framework). ### Documentation Types From 278f243bc39ebb9e079c089ea66b9359b7204226 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Fri, 3 Apr 2026 13:43:38 +0000 Subject: [PATCH 124/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 4a13ae06b..42312b80b 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -71,9 +71,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.17" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.17" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.17" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.18" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.18" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.18" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index f396d2849..c9cd84a18 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.17 +version: 1.31.18 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index b4260a34d..e248f734c 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.17 +version: 1.31.18 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 5d231b162..9d497a90e 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.17", + "envgene_version": "1.31.18", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 508665022b8590cdf2f4b736a90f76dd7c26531f Mon Sep 17 00:00:00 2001 From: Nurlybek Kamelov <79522742+GlimmerCape@users.noreply.github.com> Date: Wed, 8 Apr 2026 16:21:49 +0500 Subject: [PATCH 125/161] fix: issue with namespace filter (#1214) --- scripts/build_env/filter_namespaces.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/build_env/filter_namespaces.py b/scripts/build_env/filter_namespaces.py index 19786738a..22a4a36e9 100644 --- a/scripts/build_env/filter_namespaces.py +++ b/scripts/build_env/filter_namespaces.py @@ -1,7 +1,9 @@ +from os import getenv + from pathlib import Path import shutil -from envgenehelper.business_helper import NamespaceFile, get_bgd_object, get_namespaces, get_namespaces_path, getenv_and_log, getenv_with_error +from envgenehelper.business_helper import NamespaceFile, get_bgd_object, get_namespaces, getenv_with_error from envgenehelper import logger def filter_namespaces(namespaces: list[str], filter: str, bgd_object: dict) -> list[str]: @@ -29,7 +31,10 @@ def filter_namespaces(namespaces: list[str], filter: str, bgd_object: dict) -> l return filtered_namespaces def apply_ns_build_filter(): - filter = getenv_and_log('NS_BUILD_FILTER', default='') + filter = getenv('NS_BUILD_FILTER') + if not filter: + logger.info('NS_BUILD_FILTER is empty, skipping filtering') + return logger.info(f"Filtering namespaces with NS_BUILD_FILTER: {filter}") base_dir = getenv_with_error("CI_PROJECT_DIR") From 0804def6da8e58810f2c72495613f2fc21f5a6be Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Wed, 8 Apr 2026 11:33:12 +0000 Subject: [PATCH 126/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 42312b80b..5fdd94bc7 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -71,9 +71,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.18" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.18" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.18" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.19" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.19" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.19" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index c9cd84a18..b71c1df66 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.18 +version: 1.31.19 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index e248f734c..d194e3f72 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.18 +version: 1.31.19 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 9d497a90e..fe9fa0556 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.18", + "envgene_version": "1.31.19", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From cd27156aa8ead7c54f494d67f6c64bd9cdab2ff1 Mon Sep 17 00:00:00 2001 From: miyamuraga <198181742+miyamuraga@users.noreply.github.com> Date: Wed, 8 Apr 2026 16:42:14 +0500 Subject: [PATCH 127/161] fix: remove deprecated regex (#1038) --- scripts/build_env/render_config_env.py | 33 +++++++------------ .../expected/appdefs/application-1.yml | 8 ++--- .../templates/appdefs/application-1.yaml.j2 | 8 ++--- 3 files changed, 20 insertions(+), 29 deletions(-) diff --git a/scripts/build_env/render_config_env.py b/scripts/build_env/render_config_env.py index 034b85946..94aced3d0 100644 --- a/scripts/build_env/render_config_env.py +++ b/scripts/build_env/render_config_env.py @@ -465,35 +465,26 @@ def find_templates(self, path: str, patterns) -> list[Path]: def render_app_defs(self): for def_tmpl_path in self.ctx.appdef_templates: - app_def_str = openFileAsString(def_tmpl_path) - matches = re.findall( - r'^\s*(name|artifactId|groupId):\s*"([^"]+)"', - app_def_str, - flags=re.MULTILINE, - ) - appdef_meta = dict(matches) - ensure_valid_fields(appdef_meta, ["artifactId", "groupId", "name"]) - group_id = appdef_meta["groupId"] - artifact_id = appdef_meta["artifactId"] + app_def = self.render_from_file_to_obj(def_tmpl_path) + ensure_valid_fields(app_def, ["artifactId", "groupId", "name"]) + + app_name = app_def.get("name") + group_id = app_def["groupId"] + artifact_id = app_def["artifactId"] + self.ctx.update({ "app_lookup_key": f"{group_id}:{artifact_id}", "groupId": group_id, "artifactId": artifact_id, }) - app_def_trg_path = f"{self.ctx.current_env_dir}/AppDefs/{appdef_meta.get("name")}.yml" - self.render_from_file_to_file(def_tmpl_path, app_def_trg_path) + app_def_trg_path = f"{self.ctx.current_env_dir}/AppDefs/{app_name}.yml" + writeYamlToFile(app_def_trg_path, app_def) def render_reg_defs(self): for def_tmpl_path in self.ctx.regdef_templates: - reg_def_str = openFileAsString(def_tmpl_path) - matches = re.findall( - r'^\s*(name):\s*"([^"]+)"', - reg_def_str, - flags=re.MULTILINE, - ) - regdef_meta = dict(matches) - ensure_valid_fields(regdef_meta, ["name"]) - reg_def_trg_path = f"{self.ctx.current_env_dir}/RegDefs/{regdef_meta['name']}.yml" + reg_def = self.render_from_file_to_obj(def_tmpl_path) + ensure_valid_fields(reg_def, ["name"]) + reg_def_trg_path = f"{self.ctx.current_env_dir}/RegDefs/{reg_def.get('name')}.yml" self.render_from_file_to_file(def_tmpl_path, reg_def_trg_path) def set_appreg_def_overrides(self): diff --git a/test_data/test_app_reg_defs/TC-001-001/expected/appdefs/application-1.yml b/test_data/test_app_reg_defs/TC-001-001/expected/appdefs/application-1.yml index cf96b3568..056998b9f 100644 --- a/test_data/test_app_reg_defs/TC-001-001/expected/appdefs/application-1.yml +++ b/test_data/test_app_reg_defs/TC-001-001/expected/appdefs/application-1.yml @@ -1,7 +1,7 @@ -name: "application-1" -registryName: "registry-1" -artifactId: "application-1" -groupId: "org.qubership" +name: application-1 +registryName: registry-1 +artifactId: application-1 +groupId: org.qubership supportParallelDeploy: true deployParameters: {} technicalConfigurationParameters: {} diff --git a/test_data/test_app_reg_defs/TC-001-001/templates/appdefs/application-1.yaml.j2 b/test_data/test_app_reg_defs/TC-001-001/templates/appdefs/application-1.yaml.j2 index b70639ff3..5d25e6652 100644 --- a/test_data/test_app_reg_defs/TC-001-001/templates/appdefs/application-1.yaml.j2 +++ b/test_data/test_app_reg_defs/TC-001-001/templates/appdefs/application-1.yaml.j2 @@ -1,7 +1,7 @@ -name: "application-1" -registryName: "{{ appdefs.overrides.registryName | default('registry-1') }}" -artifactId: "application-1" -groupId: "org.qubership" +name: application-1 +registryName: {{ appdefs.overrides.registryName | default('registry-1') }} +artifactId: application-1 +groupId: org.qubership supportParallelDeploy: true deployParameters: {} technicalConfigurationParameters: {} From 737ef459dae8049ad619de6a00243738aa6b4a76 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Wed, 8 Apr 2026 11:44:46 +0000 Subject: [PATCH 128/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 5fdd94bc7..417ea5a33 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -71,9 +71,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.19" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.19" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.19" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.20" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.20" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.20" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index b71c1df66..4448c3bda 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.19 +version: 1.31.20 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index d194e3f72..4791f3d4c 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.19 +version: 1.31.20 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index fe9fa0556..fdcd04479 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.19", + "envgene_version": "1.31.20", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From ed3297c551649d7cce8b9d718363fd349f643e69 Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Fri, 10 Apr 2026 08:58:17 +0300 Subject: [PATCH 129/161] feat: Updated the extend logic for Github Instance Pipe (#1221) --- README.md | 2 +- docs/how-to/docker-registry-configuration.md | 2 +- .../how-to/extend-github-instance-pipeline.md | 144 +++++++++++++----- .../.gitignore | 10 -- .../.gitlab-ci.yml | 27 ---- .../components/components_to_add.yaml | 0 .../.github/{docs => }/README.md | 20 ++- .../.github/configuration/config.env | 3 - .../.github/workflows/Envgene.yml | 13 +- .../instance-repo-pipeline/Dockerfile | 5 +- .../scripts/apply_envgene_patch.py | 126 +++++++++++++-- .../extend_logic}/scripts/git_commit.py | 0 12 files changed, 240 insertions(+), 112 deletions(-) rename extend_github_pipeline/extend_github_instance_pipeline/README.md => docs/how-to/extend-github-instance-pipeline.md (52%) delete mode 100644 extend_github_pipeline/extend_github_instance_pipeline/.gitignore delete mode 100644 extend_github_pipeline/extend_github_instance_pipeline/.gitlab-ci.yml delete mode 100644 extend_github_pipeline/extend_github_instance_pipeline/components/components_to_add.yaml rename github_workflows/instance-repo-pipeline/.github/{docs => }/README.md (97%) delete mode 100644 github_workflows/instance-repo-pipeline/.github/configuration/config.env rename {extend_github_pipeline/extend_github_instance_pipeline => github_workflows/instance-repo-pipeline/extend_logic}/scripts/apply_envgene_patch.py (80%) mode change 100644 => 100755 rename {extend_github_pipeline/extend_github_instance_pipeline => github_workflows/instance-repo-pipeline/extend_logic}/scripts/git_commit.py (100%) mode change 100644 => 100755 diff --git a/README.md b/README.md index 151670385..b5aa19700 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ EnvGene simplifies Environment management by providing: ``` > [!NOTE] For special instructions on the GitHub pipeline, see [GH_ADDITIONAL_PARAMS docs](/docs/instance-pipeline-parameters.md) - > and the [pipeline description](/github_workflows/instance-repo-pipeline/.github/docs/README.md) + > and the [pipeline description](/github_workflows/instance-repo-pipeline/.github/README.md) After the pipeline finishes, the Environment configuration will be generated and committed to your instance repository: diff --git a/docs/how-to/docker-registry-configuration.md b/docs/how-to/docker-registry-configuration.md index 7628d7d1b..f090457f8 100644 --- a/docs/how-to/docker-registry-configuration.md +++ b/docs/how-to/docker-registry-configuration.md @@ -165,4 +165,4 @@ To switch back to GHCR: **GAR authentication step does not run:** - Confirm `DOCKER_CLOUD_REGISTRY_PROVIDER` is set to `GCP` (case-sensitive). -- Variables are passed via `process_environment_variables` job outputs. Ensure the variable is not overridden in `config.env` or `pipeline_vars.env` with an empty value. +- Variables are passed via `process_environment_variables` job outputs. Ensure the variable is not overridden in `pipeline_vars.env` with an empty value. diff --git a/extend_github_pipeline/extend_github_instance_pipeline/README.md b/docs/how-to/extend-github-instance-pipeline.md similarity index 52% rename from extend_github_pipeline/extend_github_instance_pipeline/README.md rename to docs/how-to/extend-github-instance-pipeline.md index 22b3e13eb..9dcaa3ce8 100644 --- a/extend_github_pipeline/extend_github_instance_pipeline/README.md +++ b/docs/how-to/extend-github-instance-pipeline.md @@ -2,27 +2,30 @@ This document describes the GitLab CI extension pipeline and the `apply_envgene_patch` script used to customize the EnvGene GitHub Actions workflow for your instance. +In the [qubership-envgene](https://github.com/Netcracker/qubership-envgene) repository, the Python scripts (`apply_envgene_patch.py`, `git_commit.py`) live under **`github_workflows/instance-repo-pipeline/extend_logic/scripts/`**. The `qubership-instance-repo-pipeline` Docker image copies that folder into the container at **`/opt/github/extend_logic/scripts/`**. + --- ## Table of Contents 1. [Overview](#overview) 2. [Pipeline Flow](#pipeline-flow) -3. [apply_envgene_patch Script](#apply_envgene_patch-script) -4. [Patch File Format](#patch-file-format) -5. [Operations Reference](#operations-reference) -6. [Examples](#examples) +3. [GitLab CI configuration](#gitlab-ci-configuration) +4. [apply_envgene_patch Script](#apply_envgene_patch-script) +5. [Patch File Format](#patch-file-format) +6. [Operations Reference](#operations-reference) +7. [Examples](#examples) --- ## Overview -The pipeline uses the Docker image `qubership-instance-repo-pipeline` from [Netcracker/qubership-envgene](https://github.com/Netcracker/qubership-envgene) and extends the base EnvGene workflow by adding new variables and components to `Envgene.yml` — as **steps** or **jobs** depending on the project. +The pipeline uses the Docker image `qubership-instance-repo-pipeline` from [Netcracker/qubership-envgene](https://github.com/Netcracker/qubership-envgene) and extends the base EnvGene workflow by adding new variables and components to `Envgene.yml` - as **steps** or **jobs** depending on the project. **Flow:** -1. **Init & apply** — `apply_envgene_patch.py` removes old output, copies base workflow from `/opt/github` to `extended_github_instance_pipeline/`, then applies YAML patch files (components) that add variables and workflow steps/jobs -2. **Commit & push** — `git_commit.py` commits and pushes the modified `extended_github_instance_pipeline/` directory +1. **Init & apply** — `apply_envgene_patch.py` copies the base workflow from `/opt/github` into a **staging directory** `extended_github_instance_pipeline//`, applies YAML patch files (components), then by default **packs the result into** **`extended_github_instance_pipeline/.zip`** and deletes the staging folder. `` comes from `DOCKER_IMAGE_TAG` or `INSTANCE_REPO_PIPELINE_IMAGE_TAG` (default `latest`). If you pass **no patch files**, only the base snapshot is written as a ZIP (nothing merged or inserted). Use **`--output-format dir`** to keep the versioned folder instead of a ZIP. +2. **Commit & push** — `git_commit.py` commits and pushes changes under `extended_github_instance_pipeline/` (ZIP files and, if used, leftover directories) This allows instance repositories to extend the base EnvGene workflow with custom variables, steps, and configuration without forking the entire workflow. @@ -30,71 +33,139 @@ This allows instance repositories to extend the base EnvGene workflow with custo ## Pipeline Flow -The pipeline is defined in `.gitlab-ci.yml` and runs in the `qubership-instance-repo-pipeline` Docker image (from [qubership-envgene](https://github.com/Netcracker/qubership-envgene)): +The pipeline is defined in `.gitlab-ci.yml` in your instance repository and runs in the `qubership-instance-repo-pipeline` Docker image (from [qubership-envgene](https://github.com/Netcracker/qubership-envgene)). Scripts are executed from paths inside the image: `/opt/github/extend_logic/scripts/` (same files as in the repository under `github_workflows/instance-repo-pipeline/extend_logic/scripts/`). + +A complete copypaste example for `.gitlab-ci.yml` is in [GitLab CI configuration](#gitlab-ci-configuration). + +**Steps:** + +| Step | Description | +|------|-------------| +| Init & apply | Runs `apply_envgene_patch.py`: removes `extended_github_instance_pipeline//` (staging) and `.github/`, copies `/opt/github`, applies patches, then writes **`extended_github_instance_pipeline/.zip`** by default and removes the staging dir | +| Commit & push | Commits changes and pushes to the current branch | + +**Requirements:** + +- `GITLAB_TOKEN` with `write_repository` scope (for `git_commit.py`) +- `DOCKER_IMAGE_TAG` or `INSTANCE_REPO_PIPELINE_IMAGE_TAG` set so the snapshot path matches the pipeline image tag (see [GitLab CI configuration](#gitlab-ci-configuration)) +- Pipeline runs on schedule or manual trigger (not on push/MR by default) + +--- + +## GitLab CI configuration + +Copy the following into the root of your instance repository as `.gitlab-ci.yml`, or merge the `extend-the-gh-pipeline` job into an existing file. The image must be a build of [instance-repo-pipeline](https://github.com/Netcracker/qubership-envgene/tree/main/github_workflows/instance-repo-pipeline) whose Dockerfile copies **`extend_logic/scripts/`** from that directory into **`/opt/github/extend_logic/scripts/`** in the image. + +**Placeholders:** + +| Placeholder | Description | +|-------------|-------------| +| `DOCKER_IMAGE_NAME` | Container image name without tag (for example `registry.example.com/org/qubership-instance-repo-pipeline`) | +| `DOCKER_IMAGE_TAG` | Image tag (for example `1.2.3` or `latest`). Must match the pipeline image tag; the artifact file is **`extended_github_instance_pipeline/.zip`**. | +| `PATH_TO_COMPONENT` | Zero or more patch YAML files in your repository (for example `components/component-a.yaml`). Omit all arguments to only materialize the base workflow as a ZIP (no merges). | + +GitLab artifacts should still publish **`extended_github_instance_pipeline/`** as a whole. By default each run produces **`extended_github_instance_pipeline/.zip`** (for example `extended_github_instance_pipeline/1.2.3.zip`). Extract the ZIP to get `workflows/`, `configuration/`, and so on at the archive root. ```yaml +--- +#Variables +variables: + INSTANCE_REPO_PIPELINE_IMAGE: DOCKER_IMAGE_NAME + INSTANCE_REPO_PIPELINE_IMAGE_TAG: DOCKER_IMAGE_TAG + DOCKER_IMAGE_TAG: "${INSTANCE_REPO_PIPELINE_IMAGE_TAG}" + +#Rules +workflow: + rules: + - if: '$CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event"' + when: never + - when: always + +#Stages +stages: + - extend-pipeline + extend-the-gh-pipeline: stage: extend-pipeline image: name: ${INSTANCE_REPO_PIPELINE_IMAGE}:${INSTANCE_REPO_PIPELINE_IMAGE_TAG} script: - - python3 scripts/apply_envgene_patch.py components/component-a.yaml components/variables.yaml - - python3 scripts/git_commit.py + - python3 /opt/github/extend_logic/scripts/apply_envgene_patch.py PATH_TO_COMPONENT + - python3 /opt/github/extend_logic/scripts/git_commit.py artifacts: paths: - extended_github_instance_pipeline/ ``` -**Steps:** +To run **without component patches** (only pack the base workflow into **`extended_github_instance_pipeline/.zip`**), use a script line with no patch arguments: -| Step | Description | -|------|-------------| -| Init & apply | Runs `apply_envgene_patch.py`: removes `extended_github_instance_pipeline/` and `.github/`, copies `/opt/github` to output dir, applies patches (adds variables and steps/jobs to `Envgene.yml`) | -| Commit & push | Commits changes and pushes to the current branch | +```yaml + - python3 /opt/github/extend_logic/scripts/apply_envgene_patch.py +``` -**Requirements:** +To **keep a directory** instead of a ZIP (for example for debugging), add **`--output-format dir`**: -- `GITLAB_TOKEN` with `write_repository` scope (for `git_commit.py`) -- Pipeline runs on schedule or manual trigger (not on push/MR by default) +```yaml + - python3 /opt/github/extend_logic/scripts/apply_envgene_patch.py --output-format dir PATH_TO_COMPONENT +``` --- ## apply_envgene_patch Script -**Location:** `scripts/apply_envgene_patch.py` +**Location:** `git_commit.py` is in the same directory as `apply_envgene_patch.py`. + +- **qubership-envgene tree:** `github_workflows/instance-repo-pipeline/extend_logic/scripts/` +- **`qubership-instance-repo-pipeline` image:** `/opt/github/extend_logic/scripts/` **Usage:** ```bash -# CI / full run (init from /opt/github + apply patches) -python3 scripts/apply_envgene_patch.py components/component-a.yaml components/variables.yaml +# From the root of a qubership-envgene clone - CI / full run (init from /opt/github + apply patches) +python3 github_workflows/instance-repo-pipeline/extend_logic/scripts/apply_envgene_patch.py components/component-a.yaml components/variables.yaml # Local run (skip init, apply patches to existing output dir) -python3 scripts/apply_envgene_patch.py --no-init components/component-a.yaml components/variables.yaml +python3 github_workflows/instance-repo-pipeline/extend_logic/scripts/apply_envgene_patch.py --no-init components/component-a.yaml components/variables.yaml # Custom output dir -python3 scripts/apply_envgene_patch.py --output-dir my_pipeline components/component-a.yaml +python3 github_workflows/instance-repo-pipeline/extend_logic/scripts/apply_envgene_patch.py --output-dir my_pipeline components/component-a.yaml + +# Keep a versioned directory instead of a ZIP (no packaging step) +python3 github_workflows/instance-repo-pipeline/extend_logic/scripts/apply_envgene_patch.py --output-format dir components/component-a.yaml ``` +Adjust the path to the script if your working directory is not the repository root. + +**Environment variables (versioned name):** + +| Variable | Description | +|----------|-------------| +| `DOCKER_IMAGE_TAG` | Preferred. Used in **`extended_github_instance_pipeline/.zip`** (or folder `/` with `--output-format dir`). | +| `INSTANCE_REPO_PIPELINE_IMAGE_TAG` | Used if `DOCKER_IMAGE_TAG` is unset (same value as the pipeline image tag in CI). | +| (none) | Defaults to `latest`. | + +Patches are applied against the **staging tree** **`//`**. Unless **`--output-format dir`** is used, that directory is removed after **`extended_github_instance_pipeline/.zip`** is written. + **Options:** | Option | Description | |--------|-------------| -| `--output-dir DIR` | Output directory. Maps `target_file` paths starting with `.github/` to this directory. Default: `extended_github_instance_pipeline`. | -| `--init-from DIR` | Before applying patches: remove output-dir and `.github`, copy DIR to output-dir. Default: `/opt/github`. | -| `--no-init` | Skip init step. Use when output-dir already exists (e.g. local runs without `/opt/github`). | +| `--output-dir DIR` | Parent directory for artifacts and staging. Maps `target_file` paths starting with `.github/` into the staging directory. Default: `extended_github_instance_pipeline`. | +| `--output-format` | **ZIP** (default): after patching, create **`/.zip`** and delete staging. **Directory** (`dir`): keep **`//`** and do not create a ZIP file. | +| `--init-from DIR` | Before applying patches: remove `//` and `.github`, copy DIR into the staging directory. Default: `/opt/github`. | +| `--no-init` | Skip init step. Use when the staging directory already exists (e.g. local runs without `/opt/github`). | -**Default behavior:** The script first initializes the output directory (removes `extended_github_instance_pipeline/` and `.github/`, copies `/opt/github` to output-dir), then applies patches. Use `--no-init` for local runs. +**Default behavior:** The script initializes **`//`**, applies patches (if any), then **writes a ZIP archive** unless **`--output-format dir`**. If you pass **no patch file paths**, it only initializes and writes the base snapshot as a ZIP. Use `--no-init` for local runs when the staging tree already exists. **Dependencies:** `ruamel.yaml` (install via `pip install ruamel.yaml`) -The script reads patch files and applies a sequence of operations to target files. Each operation has an `action` and optional `target_file` (defaults to the first operation's target). Paths like `.github/workflows/Envgene.yml` are resolved to `output_dir/workflows/Envgene.yml`. +The script reads patch files and applies a sequence of operations to target files. Each operation has an `action` and optional `target_file` (defaults to the first operation's target). Paths like `.github/workflows/Envgene.yml` are resolved to **`//workflows/Envgene.yml`** during processing (that path exists only until the ZIP packaging step when using the default format). --- ## Patch File Format -Patch files are YAML documents containing a list of operations. Use `target_file` paths starting with `.github/` — they are resolved to the output directory (e.g. `.github/workflows/Envgene.yml` → `extended_github_instance_pipeline/workflows/Envgene.yml`): +Patch files are YAML documents containing a list of operations. Use `target_file` paths starting with `.github/` — they are resolved under the **staging** snapshot root (e.g. `.github/workflows/Envgene.yml` → `extended_github_instance_pipeline//workflows/Envgene.yml` before the result is packaged as a ZIP): ```yaml --- @@ -104,7 +175,7 @@ Patch files are YAML documents containing a list of operations. Use `target_file content: DOCKER_IMAGE_NAME_DEPLOYTOOL: "my-registry/my-image" -- target_file: .github/configuration/config.env +- target_file: .github/pipeline_vars.env action: merge content: MY_VAR: "value" @@ -190,19 +261,15 @@ Add outputs to a specific job: ### Example 3: Merge .env variables -Add or update variables in configuration files: +Add or update variables in `.github/pipeline_vars.env`: ```yaml --- -- target_file: .github/configuration/config.env +- target_file: .github/pipeline_vars.env action: merge content: CONFIG_VAR_1: "value1" CONFIG_VAR_2: "value2" - -- target_file: .github/pipeline_vars.env - action: merge - content: PIPELINE_VAR: "my_value" ``` @@ -351,10 +418,10 @@ The base workflow uses these extension points: 1. Create a YAML file in `components/` (e.g. `components/my-feature.yaml`) 1. Add operations following the format above -1. Register the file in `.gitlab-ci.yml`: +1. Register the file in `.gitlab-ci.yml` (paths below match scripts inside the pipeline image): ```yaml -- python3 scripts/apply_envgene_patch.py components/component-a.yaml components/variables.yaml components/my-feature.yaml +- python3 /opt/github/extend_logic/scripts/apply_envgene_patch.py components/component-a.yaml components/variables.yaml components/my-feature.yaml ``` 1. Patches are applied in order; later patches can override or extend earlier ones. @@ -371,3 +438,4 @@ The base workflow uses these extension points: | `Step 'X' not found` | Step name doesn't match | Use exact or partial step name (case-insensitive) | | `Block 'path' not found` | Dotted path invalid | Verify YAML structure (jobs → job_name → outputs) | | `File not found` | Wrong target path or output-dir missing | Use path relative to repository root; run with init or `--no-init` on existing dir | +| `Invalid DOCKER_IMAGE_TAG` | Tag contains path separators | Use a plain tag only (for example `1.2.3`), not a path | diff --git a/extend_github_pipeline/extend_github_instance_pipeline/.gitignore b/extend_github_pipeline/extend_github_instance_pipeline/.gitignore deleted file mode 100644 index c9150660e..000000000 --- a/extend_github_pipeline/extend_github_instance_pipeline/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -.DS_Store -.gradle -venv -.venv -.lint_venv -.git_hook_venv -.idea -/.project -/encryption_key -secret_key.txt \ No newline at end of file diff --git a/extend_github_pipeline/extend_github_instance_pipeline/.gitlab-ci.yml b/extend_github_pipeline/extend_github_instance_pipeline/.gitlab-ci.yml deleted file mode 100644 index 0eb1e4a01..000000000 --- a/extend_github_pipeline/extend_github_instance_pipeline/.gitlab-ci.yml +++ /dev/null @@ -1,27 +0,0 @@ ---- -#Variables -variables: - INSTANCE_REPO_PIPELINE_IMAGE: DOCKER_IMAGE_NAME - INSTANCE_REPO_PIPELINE_IMAGE_TAG: DOCKER_IMAGE_TAG - -#Rules -workflow: - rules: - - if: '$CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event"' - when: never - - when: always - -#Stages -stages: - - extend-pipeline - -extend-the-gh-pipeline: - stage: extend-pipeline - image: - name: ${INSTANCE_REPO_PIPELINE_IMAGE}:${INSTANCE_REPO_PIPELINE_IMAGE_TAG} - script: - - python3 scripts/apply_envgene_patch.py PATH_TO_COMPONENT - - python3 scripts/git_commit.py - artifacts: - paths: - - extended_github_instance_pipeline/ diff --git a/extend_github_pipeline/extend_github_instance_pipeline/components/components_to_add.yaml b/extend_github_pipeline/extend_github_instance_pipeline/components/components_to_add.yaml deleted file mode 100644 index e69de29bb..000000000 diff --git a/github_workflows/instance-repo-pipeline/.github/docs/README.md b/github_workflows/instance-repo-pipeline/.github/README.md similarity index 97% rename from github_workflows/instance-repo-pipeline/.github/docs/README.md rename to github_workflows/instance-repo-pipeline/.github/README.md index 2f45354d1..fb3fb3625 100644 --- a/github_workflows/instance-repo-pipeline/.github/docs/README.md +++ b/github_workflows/instance-repo-pipeline/.github/README.md @@ -9,7 +9,7 @@ User Guide -![EnvGene Workflow](assets/envgene-workflow-header.png) +![EnvGene Workflow](docs/assets/envgene-workflow-header.png) - [EnvGene GitHub Workflow](#envgene-github-workflow) - [Overview](#overview) @@ -136,8 +136,7 @@ See [Repository Variables (vars)](#repository-variables-vars) for details. For a ### Step 4: Optional — Customize Configuration -- **`.github/configuration/config.env`** — Base pipeline configuration (e.g. `CI_PROJECT_DIR`, `GITHUB_USER_*`). Edit if you need different defaults. -- **`.github/pipeline_vars.env`** — Override pipeline parameters for debugging or recurring runs. Leave empty or add variables as needed. +- **`.github/pipeline_vars.env`** — Optional overrides loaded by the workflow (for example debugging or recurring runs). Leave empty or add variables as needed. Default pipeline settings live in the `env:` block of `.github/workflows/Envgene.yml`. ### Verifying the Setup @@ -177,7 +176,7 @@ The following sections describe each step in the pipeline as defined in `Envgene | Step | Description | |---------------------------------|--------------------------------------------------------------| | Repository Checkout | Checks out the repository (without persisting credentials) | -| Load environment variables | Loads `config.env` and `pipeline_vars.env` into `GITHUB_ENV` | +| Load environment variables | Loads `pipeline_vars.env` into `GITHUB_ENV` | | Process Input Parameters | Exports workflow inputs to environment | | Process additional variables | Parses `GH_ADDITIONAL_PARAMS` and adds to environment | | Create env_generation_params | Builds `ENV_GENERATION_PARAMS` JSON from SD/ENV variables | @@ -356,7 +355,7 @@ The variable must be present in `GITHUB_ENV` after the `process_environment_vari - A workflow input (and the "Process Input Parameters" step) - `GH_ADDITIONAL_PARAMS` (parsed by `process_additional_variables.sh`) -- `pipeline_vars.env` or `config.env` (loaded by `load-env-files`) +- `pipeline_vars.env` (loaded by `load-env-files`) ### Step 2: Expose the Variable as a Job Output @@ -495,7 +494,7 @@ GHCR is the default registry. No additional configuration is required. **Authentication:** GitHub Actions automatically authenticates to `ghcr.io` using `GITHUB_TOKEN` when pulling images. No extra secrets are needed. -**Image names:** The workflow builds image paths as `$DOCKER_REGISTRY/qubership-envgene`, `$DOCKER_REGISTRY/qubership-pipegene`, etc. For GHCR, the full path is `ghcr.io/netcracker/qubership-envgene:1.31.9`. +**Image names:** The workflow builds image paths as `$DOCKER_REGISTRY/qubership-envgene`, `$DOCKER_REGISTRY/qubership-pipegene`, etc. For GHCR, the full path is `ghcr.io/netcracker/qubership-envgene:1.31.18` (see `DOCKER_IMAGE_TAG_*` in `.github/workflows/Envgene.yml`). ### Google Artifact Registry (GAR) @@ -574,14 +573,13 @@ Replace ``, ``, ``, and `main` as needed. ```text instance-repo-pipeline/ -├── README.md # This file └── .github/ + ├── README.md # This guide (EnvGene GitHub workflow) + ├── docs/ + │ └── assets/ + │ └── envgene-workflow-header.png ├── actions/ │ └── load-env-files/ # Loads .env files into GITHUB_ENV - ├── configuration/ - │ └── config.env # Base pipeline configuration - ├── docs/ - │ └── README.md # Additional usage notes ├── scripts/ │ ├── generate_env_matrix.sh # Builds environment matrix from ENV_NAMES │ ├── process_additional_variables.sh # Parses GH_ADDITIONAL_PARAMS diff --git a/github_workflows/instance-repo-pipeline/.github/configuration/config.env b/github_workflows/instance-repo-pipeline/.github/configuration/config.env deleted file mode 100644 index 56b99fd69..000000000 --- a/github_workflows/instance-repo-pipeline/.github/configuration/config.env +++ /dev/null @@ -1,3 +0,0 @@ -GITHUB_USER_EMAIL=envgene@qubership.org -GITHUB_USER_NAME=envgene -SECRET_POSTFIX=custom_secret diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 417ea5a33..97fbe3a4a 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -56,6 +56,10 @@ on: default: "" env: + #CONFIGURATION + GITHUB_USER_EMAIL: envgene@qubership.org + GITHUB_USER_NAME: envgene + SECRET_POSTFIX: custom_secret CI_COMMIT_REF_NAME: ${{ github.ref_name }} CI_PROJECT_DIR: /workspace INSTANCES_DIR: /workspace/environments @@ -104,7 +108,6 @@ jobs: uses: ./.github/actions/load-env-files with: paths: | - .github/configuration/config.env .github/pipeline_vars.env - name: Process Input Parameters @@ -193,9 +196,11 @@ jobs: - name: Authenticate to GAR (Google Artifact Registry) if: needs.process_environment_variables.outputs.DOCKER_CLOUD_REGISTRY_PROVIDER == 'GCP' - run: | - REGISTRY_HOST=$(echo "${{ vars.DOCKER_REGISTRY }}" | cut -d'/' -f1) - echo '${{ secrets.GCP_SA_KEY }}' | docker login -u _json_key --password-stdin "$REGISTRY_HOST" + uses: docker/login-action@v4 + with: + registry: ${{ vars.DOCKER_REGISTRY }} + username: _json_key + password: ${{ secrets.GCP_SA_KEY }} ### ENV VARS PROCESSING - END ### diff --git a/github_workflows/instance-repo-pipeline/Dockerfile b/github_workflows/instance-repo-pipeline/Dockerfile index 48b4451ed..8cbfa88b4 100644 --- a/github_workflows/instance-repo-pipeline/Dockerfile +++ b/github_workflows/instance-repo-pipeline/Dockerfile @@ -5,7 +5,10 @@ WORKDIR /workspace COPY github_workflows/instance-repo-pipeline/.github /opt/github -RUN find /opt/github/scripts -type f -name "*.sh" -exec chmod +x {} \; +COPY github_workflows/instance-repo-pipeline/extend_logic/scripts/ /opt/github/extend_logic/scripts/ + +RUN find /opt/github/scripts -type f -name "*.sh" -exec chmod +x {} \; \ + && find /opt/github/extend_logic/scripts -type f -name "*.py" -exec chmod +x {} \; #-----------Stage 2----------- FROM alpine:3.21 diff --git a/extend_github_pipeline/extend_github_instance_pipeline/scripts/apply_envgene_patch.py b/github_workflows/instance-repo-pipeline/extend_logic/scripts/apply_envgene_patch.py old mode 100644 new mode 100755 similarity index 80% rename from extend_github_pipeline/extend_github_instance_pipeline/scripts/apply_envgene_patch.py rename to github_workflows/instance-repo-pipeline/extend_logic/scripts/apply_envgene_patch.py index 78daf0e15..cd327bdad --- a/extend_github_pipeline/extend_github_instance_pipeline/scripts/apply_envgene_patch.py +++ b/github_workflows/instance-repo-pipeline/extend_logic/scripts/apply_envgene_patch.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 +import os import re import shutil import sys +import zipfile from pathlib import Path try: @@ -11,6 +13,31 @@ sys.exit(1) +def resolve_version_tag(): + """Tag used in the output path (folder or zip name; matches pipeline image version).""" + tag = ( + os.environ.get("DOCKER_IMAGE_TAG") + or os.environ.get("INSTANCE_REPO_PIPELINE_IMAGE_TAG") + or "latest" + ).strip() + return tag or "latest" + + +def sanitize_tag(tag: str) -> str: + """Filesystem-safe directory name; rejects path separators.""" + tag = (tag or "").strip() + if not tag: + return "latest" + if ".." in tag or "/" in tag or "\\" in tag: + raise ValueError( + f"Invalid DOCKER_IMAGE_TAG: {tag!r} (must not contain path separators)" + ) + safe = re.sub(r"[^a-zA-Z0-9._-]", "_", tag) + if not safe or not re.search(r"[0-9a-zA-Z]", safe): + return "latest" + return safe + + # ========== MERGE ENV: add or replace variables in .env files ========== def do_merge_env(target_file, content): @@ -346,9 +373,14 @@ def apply_patch(patch_path, base_dir): with open(patch_path, encoding="utf-8") as f: operations = yaml.load(f) - if not isinstance(operations, list): + if operations is None: + operations = [] + elif not isinstance(operations, list): operations = [operations] + if not operations: + return [] + def resolve_target(target_file_str): """Resolve target file path. If base_dir is output dir, map .github/ -> output_dir/.""" if target_file_str.startswith(".github/") and base_dir != Path("."): @@ -449,9 +481,9 @@ def resolve_target(target_file_str): return result -def init_output_dir(output_dir, source_dir): - """Remove output_dir and .github, then copy source_dir into output_dir.""" - output_path = Path(output_dir) +def init_output_dir(output_root, source_dir): + """Remove output_root and .github, then copy source_dir into output_root.""" + output_path = Path(output_root) source_path = Path(source_dir) if not source_path.is_dir(): raise FileNotFoundError(f"Source directory not found: {source_path}") @@ -459,7 +491,29 @@ def init_output_dir(output_dir, source_dir): if path.exists(): shutil.rmtree(path) shutil.copytree(source_path, output_path) - print(f"Initialized {output_dir} from {source_dir}") + print(f"Initialized {output_root} from {source_dir}") + + +def make_zip_archive(source_dir: Path, zip_path: Path) -> None: + """Pack contents of source_dir into zip_path (files at archive root, no tag prefix).""" + source_dir = source_dir.resolve() + if not source_dir.is_dir(): + raise FileNotFoundError(f"Not a directory: {source_dir}") + zip_path.parent.mkdir(parents=True, exist_ok=True) + if zip_path.exists(): + zip_path.unlink() + with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf: + for path in sorted(source_dir.rglob("*")): + if path.is_file(): + arcname = path.relative_to(source_dir) + zf.write(path, arcname) + + +def finalize_zip_output(base: Path, zip_path: Path, suffix: str = "") -> None: + """Create zip from staging dir and remove staging dir.""" + make_zip_archive(base, zip_path) + shutil.rmtree(base) + print(f"Created {zip_path}{suffix}", flush=True) def main(): @@ -468,14 +522,22 @@ def main(): parser.add_argument( "--output-dir", default="extended_github_instance_pipeline", - help="Output directory. Maps .github/ paths to this directory. " - "Default: extended_github_instance_pipeline.", + help="Parent directory for artifacts. Default: extended_github_instance_pipeline. " + "With --output-format zip (default), writes /.zip; with dir, " + "//. Tag from DOCKER_IMAGE_TAG or INSTANCE_REPO_PIPELINE_IMAGE_TAG.", + ) + parser.add_argument( + "--output-format", + choices=("zip", "dir"), + default="zip", + help="zip: stage under // then pack to /.zip " + "and remove the folder. dir: keep the versioned directory (no zip).", ) parser.add_argument( "--init-from", default="/opt/github", - help="Before applying patches, remove output-dir and copy this source into it. " - "Default: /opt/github. Use --no-init to skip.", + help="Before applying patches, remove // and .github, copy this " + "source into the versioned output directory. Default: /opt/github. Use --no-init to skip.", ) parser.add_argument( "--no-init", @@ -484,26 +546,58 @@ def main(): ) parser.add_argument( "patch", - nargs="+", - help="Patch file(s) (e.g. components/component-a.yaml components/variables.yaml)", + nargs="*", + help="Patch file(s) (e.g. components/component-a.yaml components/variables.yaml). " + "If omitted, only the base workflow is copied (no patches).", ) args = parser.parse_args() - base = Path(args.output_dir) + version_tag = sanitize_tag(resolve_version_tag()) + parent = Path(args.output_dir) + base = parent / version_tag + zip_path = parent / f"{version_tag}.zip" all_results = [] try: + if not args.patch: + if not args.no_init: + if args.output_format == "zip" and zip_path.exists(): + zip_path.unlink() + init_output_dir(base, args.init_from) + if args.output_format == "zip" and base.exists(): + finalize_zip_output( + base, + zip_path, + " (no component patches to apply)", + ) + elif base.exists(): + print( + f"Initialized {base} (no component patches to apply).", + flush=True, + ) + else: + print( + "No work done (--no-init and staging directory missing).", + flush=True, + ) + return + if not args.no_init: - init_output_dir(args.output_dir, args.init_from) + if args.output_format == "zip" and zip_path.exists(): + zip_path.unlink() + init_output_dir(base, args.init_from) for patch_path in args.patch: patch_path = Path(patch_path) if not patch_path.is_file(): raise FileNotFoundError(f"File not found: {patch_path}") all_results.extend(apply_patch(patch_path, base)) - print("Applied:") - for r in all_results: - print(f" - {r}") + if all_results: + print("Applied:") + for r in all_results: + print(f" - {r}") + if args.output_format == "zip" and base.exists(): + finalize_zip_output(base, zip_path) except (FileNotFoundError, ValueError, KeyError) as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) diff --git a/extend_github_pipeline/extend_github_instance_pipeline/scripts/git_commit.py b/github_workflows/instance-repo-pipeline/extend_logic/scripts/git_commit.py old mode 100644 new mode 100755 similarity index 100% rename from extend_github_pipeline/extend_github_instance_pipeline/scripts/git_commit.py rename to github_workflows/instance-repo-pipeline/extend_logic/scripts/git_commit.py From c62fbf12f2b8c9301da55df165aca240a68456f9 Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:26:10 +0300 Subject: [PATCH 130/161] feat: Updated the initial image in Instance pipe (#1230) --- github_workflows/instance-repo-pipeline/Dockerfile | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/Dockerfile b/github_workflows/instance-repo-pipeline/Dockerfile index 8cbfa88b4..7cf0b8ef0 100644 --- a/github_workflows/instance-repo-pipeline/Dockerfile +++ b/github_workflows/instance-repo-pipeline/Dockerfile @@ -1,6 +1,7 @@ # syntax=docker/dockerfile:1 #-----------Stage 1----------- -FROM alpine:3.21 AS initial +FROM ghcr.io/netcracker/qubership-envgene-base-modules:1.0.0 AS initial + WORKDIR /workspace COPY github_workflows/instance-repo-pipeline/.github /opt/github @@ -11,13 +12,10 @@ RUN find /opt/github/scripts -type f -name "*.sh" -exec chmod +x {} \; \ && find /opt/github/extend_logic/scripts -type f -name "*.py" -exec chmod +x {} \; #-----------Stage 2----------- -FROM alpine:3.21 +FROM ghcr.io/netcracker/qubership-envgene-base-modules:1.0.0 LABEL org.opencontainers.image.description="Github Workflows - Envgene Instance pipeline" -RUN apk add --no-cache python3=3.12.12-r0 py3-pip=24.3.1-r0 git=2.47.3-r0 \ - && pip3 install --no-cache-dir --break-system-packages ruamel.yaml==0.18.6 - COPY --from=initial /opt/github /opt/github USER 1000 From 80d99948d28ab0cd65051e36a1e6ddb089adeb81 Mon Sep 17 00:00:00 2001 From: dysmon <120464230+dysmon@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:31:36 +0500 Subject: [PATCH 131/161] feat: extend github pipeline (#1201) --- .../instance-repo-pipeline/extend_logic/scripts/git_commit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github_workflows/instance-repo-pipeline/extend_logic/scripts/git_commit.py b/github_workflows/instance-repo-pipeline/extend_logic/scripts/git_commit.py index 323978f11..a3dba4f6d 100755 --- a/github_workflows/instance-repo-pipeline/extend_logic/scripts/git_commit.py +++ b/github_workflows/instance-repo-pipeline/extend_logic/scripts/git_commit.py @@ -89,7 +89,7 @@ def main(): if result.returncode != 0: output_dir = os.environ.get("PIPELINE_OUTPUT_DIR", "extended_github_instance_pipeline") - run(["git", "commit", "-m", f"chore: update {output_dir} from pipeline"], env=env) + run(["git", "commit", "-m", f"chore: update {output_dir} from pipeline [ci skip]"], env=env) result = subprocess.run( ["git", "push", "origin", f"HEAD:{ref_name}"], env=env, From 0d1265d7942d1593fb529327b8826977afe7b52b Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:42:28 +0300 Subject: [PATCH 132/161] feat: Updated base image version for instance pipe (#1232) --- github_workflows/instance-repo-pipeline/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/Dockerfile b/github_workflows/instance-repo-pipeline/Dockerfile index 7cf0b8ef0..a1183d179 100644 --- a/github_workflows/instance-repo-pipeline/Dockerfile +++ b/github_workflows/instance-repo-pipeline/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 #-----------Stage 1----------- -FROM ghcr.io/netcracker/qubership-envgene-base-modules:1.0.0 AS initial +FROM ghcr.io/netcracker/qubership-envgene-base-modules:1.0.1 AS initial WORKDIR /workspace @@ -12,7 +12,7 @@ RUN find /opt/github/scripts -type f -name "*.sh" -exec chmod +x {} \; \ && find /opt/github/extend_logic/scripts -type f -name "*.py" -exec chmod +x {} \; #-----------Stage 2----------- -FROM ghcr.io/netcracker/qubership-envgene-base-modules:1.0.0 +FROM ghcr.io/netcracker/qubership-envgene-base-modules:1.0.1 LABEL org.opencontainers.image.description="Github Workflows - Envgene Instance pipeline" From 1e393f3315031091d2b38a1778a435758d289521 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Mon, 13 Apr 2026 13:51:55 +0000 Subject: [PATCH 133/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 97fbe3a4a..4eb0e2e31 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -75,9 +75,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.20" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.20" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.20" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.21" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.21" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.21" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 4448c3bda..bec2436f0 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.20 +version: 1.31.21 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 4791f3d4c..5afcfea7a 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.20 +version: 1.31.21 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index fdcd04479..44fe7007f 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.20", + "envgene_version": "1.31.21", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 3c0f99e3e1db3bb624ad0383ae2023522919b9dd Mon Sep 17 00:00:00 2001 From: Geetha Gadde Date: Tue, 14 Apr 2026 11:53:12 +0530 Subject: [PATCH 134/161] fix: ssl verification failing (#1219) --- build_pipegene/scripts/pipeline_helper.py | 1 + scripts/utils/update_ca_cert.sh | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/build_pipegene/scripts/pipeline_helper.py b/build_pipegene/scripts/pipeline_helper.py index 9cb022d17..e72b5c935 100644 --- a/build_pipegene/scripts/pipeline_helper.py +++ b/build_pipegene/scripts/pipeline_helper.py @@ -47,6 +47,7 @@ def job_instance(params, vars, needs=None, rules=None): global_before = [ 'python /module/scripts/utils/log_pipe_params.py', '/module/scripts/utils/handle_certs.sh', + 'source ~/.bashrc', ] job.prepend_scripts(*global_before) diff --git a/scripts/utils/update_ca_cert.sh b/scripts/utils/update_ca_cert.sh index c29a30199..a1a76a25a 100755 --- a/scripts/utils/update_ca_cert.sh +++ b/scripts/utils/update_ca_cert.sh @@ -40,7 +40,8 @@ function debugPrintCertsFromFile { fi local cert_num=0 local block="" - while IFS= read -r line; do + while IFS= read -r line || [[ -n "$line" ]]; do + line="${line%$'\r'}" if [[ "$line" == "-----BEGIN CERTIFICATE-----" ]]; then block="$line" continue @@ -50,7 +51,7 @@ function debugPrintCertsFromFile { if [[ "$line" == "-----END CERTIFICATE-----" ]]; then cert_num=$((cert_num + 1)) echo "[DEBUG] --- Certificate #${cert_num} in ${file} ---" - echo "$block" | openssl x509 -noout -subject -issuer -dates 2>/dev/null || echo "[DEBUG] (openssl could not decode this block)" + printf "%s\n" "$block" | openssl x509 -noout -subject -issuer -dates 2>/dev/null || echo "[DEBUG] (openssl could not decode this block)" block="" fi fi From 0c66f26c6c21f64350e751b05d895865e7bbfcac Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Tue, 14 Apr 2026 06:25:24 +0000 Subject: [PATCH 135/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 4eb0e2e31..86bc6432f 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -75,9 +75,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.21" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.21" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.21" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.22" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.22" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.22" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index bec2436f0..01ca5f09c 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.21 +version: 1.31.22 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 5afcfea7a..c8bd9f276 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.21 +version: 1.31.22 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 44fe7007f..bd5c279d2 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.21", + "envgene_version": "1.31.22", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 43c27589d64f7d67cf8c0319da0b309d74103ccc Mon Sep 17 00:00:00 2001 From: Siva Reddy Kunduru <35566000+sivareddyit@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:32:19 +0530 Subject: [PATCH 136/161] fix: modified k8_token logic to keep only namespaces instead of deploy postfixes (#1170) --- .../devops/cli/parser/CliParameterParser.java | 16 ++++------------ .../effective-set/topology/credentials.yaml | 2 -- .../service/ParametersCalculationServiceV1.java | 17 +++-------------- 3 files changed, 7 insertions(+), 28 deletions(-) diff --git a/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/parser/CliParameterParser.java b/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/parser/CliParameterParser.java index 9e3e94873..19c15529f 100644 --- a/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/parser/CliParameterParser.java +++ b/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/parser/CliParameterParser.java @@ -106,12 +106,13 @@ private void processAndSaveParameters(Optional solutionDescripto Map errorList = new ConcurrentHashMap<>(); Map k8TokenMap = new ConcurrentHashMap<>(); namespaceDTOMap.keySet().parallelStream().forEach(namespaceName -> { + String originalNamespace = inputData.getNamespaceDTOMap().get(namespaceName).getName(); String credentialsId = findDefaultCredentialsId(namespaceName); if (StringUtils.isNotEmpty(credentialsId)) { CredentialDTO credentialDTO = inputData.getCredentialDTOMap().get(credentialsId); if (credentialDTO != null) { SecretCredentialsDTO secCred = (SecretCredentialsDTO) credentialDTO.getData(); - k8TokenMap.put(namespaceName, secCred.getSecret()); + k8TokenMap.put(originalNamespace, secCred.getSecret()); } } }); @@ -273,15 +274,7 @@ public void generateOutput(String tenantName, String cloudName, String namespace String appVersion, String appFileRef, Map k8TokenMap) throws IOException { DeployerInputs deployerInputs = DeployerInputs.builder().appVersion(appVersion).appFileRef(appFileRef).build(); String originalNamespace = inputData.getNamespaceDTOMap().get(namespaceName).getName(); - String credentialsId = findDefaultCredentialsId(namespaceName); - if (StringUtils.isNotEmpty(credentialsId)) { - CredentialDTO credentialDTO = inputData.getCredentialDTOMap().get(credentialsId); - if (credentialDTO != null) { - SecretCredentialsDTO secCred = (SecretCredentialsDTO) credentialDTO.getData(); - k8TokenMap.put(originalNamespace, secCred.getSecret()); - } - } - ParameterBundle parameterBundle = null; + ParameterBundle parameterBundle; if (EffectiveSetVersion.V2_0 == sharedData.getEffectiveSetVersion()) { CustomParameterDTO customParams = getCustomParameters(); parameterBundle = parametersServiceV2.getCliParameter(tenantName, @@ -300,8 +293,7 @@ public void generateOutput(String tenantName, String cloudName, String namespace namespaceName, appName, deployerInputs, - originalNamespace, - k8TokenMap); + originalNamespace); } createFiles(namespaceName, appName, parameterBundle, originalNamespace); diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/topology/credentials.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/topology/credentials.yaml index 38c4c40a6..511db0adf 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/topology/credentials.yaml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/topology/credentials.yaml @@ -3,7 +3,5 @@ bg_domain: password: pass-placeholder-123 username: user-placeholder-123 k8s_tokens: - monitoring-origin: token-placeholder-123 - pg: token-placeholder-123 pl-01-pg: token-placeholder-123 pl-01-monitoring: token-placeholder-123 diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV1.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV1.java index e126151f7..af40cc438 100644 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV1.java +++ b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/service/ParametersCalculationServiceV1.java @@ -43,16 +43,12 @@ public ParametersCalculationServiceV1(ParametersProcessor parametersProcessor) { } public ParameterBundle getCliParameter(String tenantName, String cloudName, String namespaceName, String applicationName, - DeployerInputs deployerInputs, String originalNamespace, Map k8TokenMap) { - return getParameterBundle(tenantName, cloudName, namespaceName, applicationName, deployerInputs, originalNamespace, k8TokenMap); - } - - public ParameterBundle getCliE2EParameter(String tenantName, String cloudName) { - return getE2EParameterBundle(tenantName, cloudName); + DeployerInputs deployerInputs, String originalNamespace) { + return getParameterBundle(tenantName, cloudName, namespaceName, applicationName, deployerInputs, originalNamespace); } private ParameterBundle getParameterBundle(String tenantName, String cloudName, String namespaceName, String applicationName, DeployerInputs deployerInputs - , String originalNamespace, Map k8TokenMap) { + , String originalNamespace) { Params parameters = parametersProcessor.processAllParameters(tenantName, cloudName, namespaceName, @@ -68,13 +64,6 @@ private ParameterBundle getParameterBundle(String tenantName, String cloudName, return parameterBundle; } - private ParameterBundle getE2EParameterBundle(String tenantName, String cloudName) { - Params parameters = parametersProcessor.processE2EParameters(tenantName, cloudName, null, null, null, null); - ParameterBundle parameterBundle = ParameterBundle.builder().build(); - prepareSecureInsecureParams(parameters.getE2eParams(), parameterBundle, ParameterType.E2E); - return parameterBundle; - } - public void prepareSecureInsecureParams(Map parameters, ParameterBundle parameterBundle, ParameterType parameterType) { Map securedParams = new TreeMap<>(); Map inSecuredParams = new TreeMap<>(); From 6a439f30332277580e175a3f733d637cf4418ae1 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Tue, 14 Apr 2026 08:04:18 +0000 Subject: [PATCH 137/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 86bc6432f..d27362cbc 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -75,9 +75,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.22" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.22" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.22" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.23" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.23" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.23" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 01ca5f09c..0cf3beb1e 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.22 +version: 1.31.23 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index c8bd9f276..a208d2e63 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.22 +version: 1.31.23 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index bd5c279d2..bf182039e 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.22", + "envgene_version": "1.31.23", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 1d133fb7f06ca185b13950ed470b4356c81e29b1 Mon Sep 17 00:00:00 2001 From: dysmon <120464230+dysmon@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:07:32 +0500 Subject: [PATCH 138/161] fix: shared cred processing (#1213) --- python/envgene/envgenehelper/yaml_helper.py | 11 +++++++- scripts/build_env/create_credentials.py | 30 +++++++++++---------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/python/envgene/envgenehelper/yaml_helper.py b/python/envgene/envgenehelper/yaml_helper.py index b49db0a6c..01147e15e 100644 --- a/python/envgene/envgenehelper/yaml_helper.py +++ b/python/envgene/envgenehelper/yaml_helper.py @@ -294,6 +294,15 @@ def beautifyYaml(file_path, schema_path="", header_text="", allign_comments=Fals alignYamlFileComments(file_path) +def find_yaml_file(dir_path: Path, search_name: str) -> Path | None: + for ext in (".yml", ".yaml"): + f = dir_path / f"{search_name}{ext}" + if f.is_file(): + logger.info(f"Found {search_name} in: {f}") + return f + return None + + def findYamls(dir, pattern, notPattern="", additionalRegexpPattern="", additionalRegexpNotPattern=""): fileList = findAllYamlsInDir(dir) return findFiles(fileList, pattern, notPattern, additionalRegexpPattern, additionalRegexpNotPattern) @@ -446,4 +455,4 @@ def find_files_by_basename(path: str, extensions_priority: tuple[str] = ("yml", jschon.create_catalog('2020-12') yaml = create_yaml_processor() -safe_yaml = create_yaml_processor(is_safe=True) +safe_yaml = create_yaml_processor(is_safe=True) \ No newline at end of file diff --git a/scripts/build_env/create_credentials.py b/scripts/build_env/create_credentials.py index 10b5eb3c8..3bf986810 100644 --- a/scripts/build_env/create_credentials.py +++ b/scripts/build_env/create_credentials.py @@ -1,5 +1,4 @@ -import re -import sys +from pathlib import Path import os from envgenehelper import * @@ -159,18 +158,21 @@ def mergeAndSaveYaml(yamlPath, newCreds) : logger.info("%s credentials created" % count) writeYamlToFile(yamlPath, credsYaml) -def findSharedCredentials(cred_name, env_dir, instances_dir): - logger.debug(f"Searching for cred file {cred_name} from {env_dir} to {instances_dir}") - credFiles = findResourcesBottomTop(env_dir, instances_dir, f"/{cred_name}") - if len(credFiles) == 1: - yamlPath = credFiles[0] - logger.info(f"Shared credentials for {cred_name} found in: {yamlPath}") - return yamlPath - elif len(credFiles) > 1: - logger.error(f"Duplicate shared credentials with key {cred_name} found in {instances_dir}: \n\t" + ",\n\t".join(str(x) for x in credFiles)) - raise ReferenceError(f"Duplicate shared credentials with key {cred_name} found. See logs above.") - else: - raise ReferenceError(f"Shared credentials with key {cred_name} not found in {instances_dir}") +def findSharedCredentials(cred_name, env_dir, instances_dir) -> Path: + env_level = Path(env_dir) / "Inventory" / "credentials" + cluster_level = Path(env_dir).parent / "credentials" + site_level = Path(instances_dir) / "credentials" + + shared_cred_paths = [env_level, cluster_level, site_level] + + logger.debug(f"Searching for '{cred_name}' in paths: {shared_cred_paths}") + for p in shared_cred_paths: + found_path = find_yaml_file(p, cred_name) + if found_path: + return found_path + + raise FileNotFoundError(f"Shared credentials with key '{cred_name}' not found.") + def mergeSharedCreds(credYamlPath, envDir, instancesDir) : inventoryYaml = getEnvDefinition(envDir) From 1cbb15698960131d7efafef7638e37dbca99e683 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Tue, 14 Apr 2026 08:43:48 +0000 Subject: [PATCH 139/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index d27362cbc..686dbe1d7 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -75,9 +75,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.23" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.23" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.23" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.24" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.24" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.24" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 0cf3beb1e..aa22e7e3c 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.23 +version: 1.31.24 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index a208d2e63..f55c091a3 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.23 +version: 1.31.24 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index bf182039e..09def37e9 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.23", + "envgene_version": "1.31.24", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From fc46556b0be22dd7394ce2f8ed4980fa48f2a73d Mon Sep 17 00:00:00 2001 From: Jackson Raj A <150916845+BackendBits@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:33:28 +0530 Subject: [PATCH 140/161] fix: preserve parameter data types for variable references in Expression Language (#842) --- .../expression/ExpressionLanguage.java | 155 +++++++++++++----- .../processor/expression/binding/Binding.java | 5 - .../expression/ExpressionLanguageTest.java | 121 ++++++++++++++ 3 files changed, 234 insertions(+), 47 deletions(-) diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/ExpressionLanguage.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/ExpressionLanguage.java index 0175d6fc1..328b50068 100644 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/ExpressionLanguage.java +++ b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/ExpressionLanguage.java @@ -16,13 +16,13 @@ package org.qubership.cloud.parameters.processor.expression; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.FatalTemplateErrorsException; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; import groovy.lang.GroovyClassLoader; import groovy.lang.GroovyRuntimeException; import groovy.text.GStringTemplateEngine; @@ -35,7 +35,6 @@ import org.qubership.cloud.devops.gstringtojinjavatranslator.jinjava.*; import org.qubership.cloud.devops.gstringtojinjavatranslator.translator.GStringToJinJavaTranslator; import org.qubership.cloud.parameters.processor.MergeMap; -import org.qubership.cloud.parameters.processor.ParametersProcessor; import org.qubership.cloud.parameters.processor.exceptions.ExpressionLanguageException; import org.qubership.cloud.parameters.processor.expression.binding.Binding; import org.qubership.cloud.parameters.processor.expression.binding.DynamicMap; @@ -58,11 +57,17 @@ public class ExpressionLanguage extends AbstractLanguage { private static final Pattern EXPRESSION_PATTERN = Pattern.compile("(?$") // <% VARIABLE %> + }; private boolean insecure; private final Jinjava jinjava; private final GStringToJinJavaTranslator gStringToJinJavaTranslator; + private final DynamicPropertyResolver dynamicResolver; public ExpressionLanguage(Binding binding) { super(binding); @@ -80,6 +85,8 @@ public ExpressionLanguage(Binding binding) { this.binding.forEach((key1, value) -> this.binding.put(key1, translateParameter(value.getValue()))); + + this.dynamicResolver = new DynamicPropertyResolver(this.binding); } private Parameter translateParameter(Object value) { @@ -211,29 +218,32 @@ private Parameter processValue(Object value, Map binding, boo parameter.setValue(removeEscaping(escapeDollar, parameter.getValue())); return parameter; } + Object val = getValue(value); boolean isProcessed = false; boolean isSecured = getIsSecured(value); if (val instanceof String) { - String strValue = (String) val; + Parameter preserved = tryPreserveType(value, binding); + if (preserved != null) { + return preserved; + } + String strValue = (String) val; - String rendered = ""; - this.binding.getTypeCollector().clear(); + String jinJavaRendered = ""; try { - rendered = renderStringByJinJava(strValue, binding, escapeDollar); + jinJavaRendered = renderStringByJinJava(strValue, binding, escapeDollar); + val = jinJavaRendered; } catch (Exception e) { - logDebug(String.format("Parameter {} was not processed by JinJava, hence reverting to Groovy.", strValue)); - rendered = renderStringByGroovy(strValue, binding, escapeDollar); + log.debug(String.format("Parameter {} was not processed by JinJava, hence reverting to Groovy.", strValue)); + String groovyRendered = renderStringByGroovy(strValue, binding, escapeDollar); + val = groovyRendered; } - Object originalValue = this.binding.getTypeCollector().get(rendered); // Object - Class targetType = (originalValue != null) ? (Class) originalValue : String.class; - val = convertToType(rendered, targetType); isProcessed = true; - Matcher secureMarkerMatcher = SECURED_PATTERN.matcher(String.valueOf(val)); + Matcher secureMarkerMatcher = SECURED_PATTERN.matcher((String) val); if (secureMarkerMatcher.find()) { isSecured = true; val = ((String) Objects.requireNonNull(val)).replaceAll("([\\u0096\\u0097])", ""); @@ -250,6 +260,88 @@ private Parameter processValue(Object value, Map binding, boo return ret; } + // Extracts variable name from simple parameter reference patterns: ${VAR}, $VAR, or <% VAR %>. + private String extractParameterReference(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + for (Pattern pattern : PARAMETER_REFERENCE_PATTERNS) { + Matcher matcher = pattern.matcher(trimmed); + if (matcher.matches()) { + return matcher.group(1); + } + } + return null; + } + + // Preserves data type for simple parameter references (${VAR}, $VAR) using Jinjava's expression resolver. + // Returns null if type preservation is not applicable (complex expressions or String values). + private Parameter tryPreserveType(Object value, Map binding) { + Object val = getValue(value); + if (!(val instanceof String)) { + return null; + } + + String referencedVar = extractParameterReference((String) val); + if (referencedVar == null) { + return null; + } + + // Use Jinjava's expression resolver - returns typed Object (Integer, Boolean, etc.) + Object resolvedValue = resolveWithJinjava(referencedVar, binding); + + // Unwrap if still a Parameter + if (resolvedValue instanceof Parameter) { + resolvedValue = ((Parameter) resolvedValue).getValue(); + } + + if (resolvedValue == null || resolvedValue instanceof String) { + return null; + } + + // Get secured flag from source parameter + Parameter srcParam = binding.get(referencedVar); + if (srcParam == null) { + srcParam = this.binding.get(referencedVar); + } + boolean isSecured = srcParam != null && srcParam.isSecured(); + + Parameter result = new Parameter(resolvedValue); + if (value instanceof Parameter) { + result.setOrigin(((Parameter) value).getOrigin()); + } + result.setParsed(true); + result.setProcessed(true); + result.setSecured(isSecured); + result.setValid(true); + + log.debug("Type preserved for {}: {} ({})", referencedVar, resolvedValue, resolvedValue.getClass().getSimpleName()); + return result; + } + + // Uses Jinjava's expression resolver which returns typed Object instead of String. + private Object resolveWithJinjava(String expression, Map binding) { + try { + Context context = new Context(jinjava.getGlobalContextCopy(), binding, jinjava.getGlobalConfig().getDisabled()); + context.setDynamicVariableResolver(this.dynamicResolver); + + JinjavaInterpreter interpreter = jinjava.getGlobalConfig() + .getInterpreterFactory() + .newInstance(jinjava, context, jinjava.getGlobalConfig()); + + JinjavaInterpreter.pushCurrent(interpreter); + try { + return interpreter.resolveELExpression(expression, -1); + } finally { + JinjavaInterpreter.popCurrent(); + } + } catch (Exception e) { + log.debug("Failed to resolve '{}' with Jinjava: {}", expression, e.getMessage()); + return null; + } + } + private String renderStringByGroovy(String value, Map binding, boolean escapeDollar) { int i = 0; String rendered = value; @@ -280,21 +372,14 @@ private String renderStringByJinJava(String value, Map bindin return rendered; } - private Object removeEscaping(boolean escapeDollar, Object val) throws JsonProcessingException { - - if (escapeDollar && val != null) { - Class originalType = val.getClass(); - String strValue; - if (val instanceof String) { - strValue = val.toString(); - } else { - strValue = mapper.writeValueAsString(val); - } - strValue = strValue.replaceAll("\\\\\\$", "\\$"); // \$ -> $ - strValue = strValue.replaceAll("\\\\\\\\", "\\\\"); // \\ -> \ - return convertToType(strValue, originalType); + private Object removeEscaping(boolean escapeDollar, Object val) { + // Only process escaping for String values - non-String types (Integer, Boolean, etc.) + // don't need escape processing and should preserve their original type + if (escapeDollar && val instanceof String) { + String strValue = (String) val; + strValue = strValue.replaceAll("\\\\\\$", "\\$"); // \$ -> $ + val = strValue.replaceAll("\\\\\\\\", "\\\\"); // \\ -> \ } - return val; } @@ -476,18 +561,4 @@ public Map processParameters(Map parameters) return processedParams; } - private static Object convertToType(String value, Class type) { - if (type == String.class) { - return value; - } else if (type == Integer.class || type == int.class) { - return Integer.parseInt(value); - } else if (type == Long.class || type == long.class) { - return Long.parseLong(value); - } else if (type == Boolean.class || type == boolean.class) { - return Boolean.parseBoolean(value); - } else if (type == Double.class || type == double.class) { - return Double.parseDouble(value); - } - return value; - } } diff --git a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/Binding.java b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/Binding.java index 4bbead23f..e7a9160f6 100644 --- a/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/Binding.java +++ b/build_effective_set_generator/parameters-processor/src/main/java/org/qubership/cloud/parameters/processor/expression/binding/Binding.java @@ -44,8 +44,6 @@ public class Binding extends HashMap implements Cloneable { @Getter private String tenant; private ParametersParser escapeParser; - @Getter - private Map> typeCollector = new HashMap<>(); public Binding() { this.tenant = ""; @@ -205,9 +203,6 @@ public Parameter get(Object key) { if (result == null || result.getValue() == null) { return null; } - if (result != null && result.getValue() != null) { - typeCollector.put(result.getValue().toString(), result.getValue().getClass()); - } return result; } diff --git a/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/expression/ExpressionLanguageTest.java b/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/expression/ExpressionLanguageTest.java index 8374027e1..89357e9b9 100644 --- a/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/expression/ExpressionLanguageTest.java +++ b/build_effective_set_generator/parameters-processor/src/test/java/org/qubership/cloud/expression/ExpressionLanguageTest.java @@ -566,4 +566,125 @@ void processedGlobalResourceProfileMustBeSuccessfullyProcessedAgain() throws NoS processMap.setAccessible(true); assertEquals("{GLOBAL_RESOURCE_PROFILE={key1=value1, key2=value2}}", processMap.invoke(el, binding, binding, true).toString()); } + + // Test that data types are preserved when one parameter references another. + // This addresses the issue where EXPVAR: ${ORGVAR} was being converted to a String instead of preserving the Integer type. + + @Test + void testTypePreservationForIntegerReference() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Binding binding = new Binding(); + + Parameter intParam = new Parameter(27017); + binding.put("MONGO_DB_PORT", intParam); + + Parameter refParam = new Parameter("${MONGO_DB_PORT}"); + binding.put("DUMPS_MONGO_PORT", refParam); + + ExpressionLanguage el = new ExpressionLanguage(binding); + Method processValue = ExpressionLanguage.class.getDeclaredMethod("processValue", Object.class); + processValue.setAccessible(true); + + Parameter result = (Parameter) processValue.invoke(el, refParam); + + assertThat(result.getValue(), instanceOf(Integer.class)); + assertEquals(27017, result.getValue()); + } + + @Test + void testTypePreservationForBooleanWithBraceSyntax() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Binding binding = new Binding(); + + Parameter boolParam = new Parameter(true); + binding.put("ENABLE_SSL", boolParam); + + Parameter refParam = new Parameter("${ENABLE_SSL}"); + + ExpressionLanguage el = new ExpressionLanguage(binding); + Method processValue = ExpressionLanguage.class.getDeclaredMethod("processValue", Object.class); + processValue.setAccessible(true); + + Parameter result = (Parameter) processValue.invoke(el, refParam); + + assertThat(result.getValue(), instanceOf(Boolean.class)); + assertEquals(true, result.getValue()); + } + + @Test + void testTypePreservationForBooleanWithDollarSyntax() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Binding binding = new Binding(); + + Parameter boolParam = new Parameter(true); + binding.put("ENABLE_SSL", boolParam); + + Parameter refParam = new Parameter("$ENABLE_SSL"); + + ExpressionLanguage el = new ExpressionLanguage(binding); + Method processValue = ExpressionLanguage.class.getDeclaredMethod("processValue", Object.class); + processValue.setAccessible(true); + + Parameter result = (Parameter) processValue.invoke(el, refParam); + + assertThat(result.getValue(), instanceOf(Boolean.class)); + assertEquals(true, result.getValue()); + } + + @Test + void testTypePreservationForGroovyStyleReference() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Binding binding = new Binding(); + + Parameter intParam = new Parameter(8080); + binding.put("BASE_PORT", intParam); + + Parameter refParam = new Parameter("$BASE_PORT"); + binding.put("SERVER_PORT", refParam); + + ExpressionLanguage el = new ExpressionLanguage(binding); + Method processValue = ExpressionLanguage.class.getDeclaredMethod("processValue", Object.class); + processValue.setAccessible(true); + + Parameter result = (Parameter) processValue.invoke(el, refParam); + + assertThat(result.getValue(), instanceOf(Integer.class)); + assertEquals(8080, result.getValue()); + } + + @Test + void testTypePreservationForLongReference() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Binding binding = new Binding(); + + Parameter longParam = new Parameter(604800000L); + binding.put("CDC_TOPIC_STREAMING_RETENTION_MS", longParam); + + Parameter refParam = new Parameter("${CDC_TOPIC_STREAMING_RETENTION_MS}"); + binding.put("RETENTION_COPY", refParam); + + ExpressionLanguage el = new ExpressionLanguage(binding); + Method processValue = ExpressionLanguage.class.getDeclaredMethod("processValue", Object.class); + processValue.setAccessible(true); + + Parameter result = (Parameter) processValue.invoke(el, refParam); + + assertThat(result.getValue(), instanceOf(Long.class)); + assertEquals(604800000L, result.getValue()); + } + + @Test + void testStringReferenceShouldNotPreserveType() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Binding binding = new Binding(); + + Parameter strParam = new Parameter("myhost.local"); + binding.put("TEST_HOST", strParam); + + Parameter refParam = new Parameter("${TEST_HOST}"); + + ExpressionLanguage el = new ExpressionLanguage(binding); + Method processValue = ExpressionLanguage.class.getDeclaredMethod("processValue", Object.class); + processValue.setAccessible(true); + + Parameter result = (Parameter) processValue.invoke(el, refParam); + + assertThat(result.getValue(), instanceOf(String.class)); + assertEquals("myhost.local", result.getValue()); + } + } From e987e5cd53ba4e8b7316c47d57775291af8100a1 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Wed, 15 Apr 2026 09:05:28 +0000 Subject: [PATCH 141/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 686dbe1d7..6a4b60956 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -75,9 +75,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.24" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.24" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.24" + DOCKER_IMAGE_TAG_PIPEGENE: "1.31.25" + DOCKER_IMAGE_TAG_ENVGENE: "1.31.25" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.25" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index aa22e7e3c..476e33492 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.24 +version: 1.31.25 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index f55c091a3..9f400260b 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.24 +version: 1.31.25 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 09def37e9..79b3b2f51 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.24", + "envgene_version": "1.31.25", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 5696283c0f4fb6bf65bb85f7cffef842ef3726c7 Mon Sep 17 00:00:00 2001 From: Jackson Raj A <150916845+BackendBits@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:28:36 +0530 Subject: [PATCH 142/161] feat: support cloud public registry (#1025) --- .../build/requirements.txt | 10 +- build_envgene/build/requirements.txt | 9 +- build_pipegene/build/requirements.txt | 8 +- creds_rotation/build/requirements.txt | 2 +- dependencies/tests_requirements.txt | 2 +- .../application-manifest-build-cli.md | 4 +- docs/envgene-objects.md | 2 +- .../artifact_searcher/artifact.py | 220 +++++++++++------ .../artifact_searcher/auth_resolver.py | 139 +++++++++++ .../artifact_searcher/test_artifact.py | 13 +- .../artifact_searcher/test_auth_resolver.py | 217 +++++++++++++++++ .../artifact_searcher/utils/models.py | 158 +++++++++++- python/artifact-searcher/pyproject.toml | 24 +- python/envgene/envgenehelper/__init__.py | 2 +- python/envgene/envgenehelper/config_helper.py | 27 +++ .../schemas}/regdef-v2.schema.json | 16 ++ .../envgenehelper/schemas/regdef.schema.json | 224 ++++++++++++++++++ python/envgene/pyproject.toml | 3 + .../env_template/process_env_template.py | 29 ++- scripts/build_env/process_sd.py | 11 +- scripts/build_env/render_config_env.py | 5 +- 21 files changed, 998 insertions(+), 127 deletions(-) create mode 100644 python/artifact-searcher/artifact_searcher/auth_resolver.py create mode 100644 python/artifact-searcher/artifact_searcher/test_auth_resolver.py rename {schemas => python/envgene/envgenehelper/schemas}/regdef-v2.schema.json (96%) create mode 100644 python/envgene/envgenehelper/schemas/regdef.schema.json diff --git a/build_effective_set_generator/build/requirements.txt b/build_effective_set_generator/build/requirements.txt index a4c3ffb31..52a6f65b7 100644 --- a/build_effective_set_generator/build/requirements.txt +++ b/build_effective_set_generator/build/requirements.txt @@ -2,14 +2,14 @@ PyGithub==1.55 certifi==2022.6.15 -boto3==1.29.3 -botocore==1.32.3 +boto3==1.39.4 +botocore==1.39.4 gcip==3.0.2 jmespath==1.0.1 packaging==23.2 python-dateutil==2.8.2 -PyYAML==6.0.1 -s3transfer==0.7.0 +PyYAML==6.0.2 +s3transfer==0.13.1 setuptools-git-versioning==1.13.5 six==1.16.0 toml==0.10.2 @@ -19,7 +19,7 @@ ruamel.yaml==0.18.5 ruamel.yaml.clib==0.2.8 ruyaml==0.91.0 jschon==0.11.0 -jsonschema==4.19.1 +jsonschema==4.24.1 diagrams==0.24.1 graphviz==0.20.3 attrs==23.2.0 diff --git a/build_envgene/build/requirements.txt b/build_envgene/build/requirements.txt index 5addb5d52..0316e1d2b 100644 --- a/build_envgene/build/requirements.txt +++ b/build_envgene/build/requirements.txt @@ -9,12 +9,12 @@ lxml==4.9.3 ruamel.yaml==0.18.5 ruamel.yaml.clib==0.2.8 jschon==0.11.0 -jsonschema==4.19.1 +jsonschema==4.24.1 jmespath==1.0.1 semantic-version==2.10.0 termcolor==2.4.0 cffi==1.16.0 -click==8.1.3 +click==8.1.7 deepmerge==2.0 GitPython==3.1.45 pydantic==2.10.6 @@ -23,7 +23,10 @@ Jinja2==3.1.6 # Additional required packages platformdirs>=3.0.0 +google-auth~=2.34.0 +qubership-pipelines-common-library==2.0.3 + # Removed heavy packages: # - shyaml, yamale, prettytable (not essential) # - ruyaml (duplicate of ruamel.yaml) -# - diagrams (heavy with typed-ast dependency) +# - diagrams (heavy with typed-ast dependency) \ No newline at end of file diff --git a/build_pipegene/build/requirements.txt b/build_pipegene/build/requirements.txt index 7591c8ed9..7d6f7e08b 100644 --- a/build_pipegene/build/requirements.txt +++ b/build_pipegene/build/requirements.txt @@ -1,5 +1,5 @@ -boto3==1.29.3 -botocore==1.32.3 +boto3==1.39.4 +botocore==1.39.4 gcip==3.0.2 jmespath==1.0.1 packaging==23.2 @@ -7,7 +7,7 @@ pip>=23.0 setuptools>=68,<82 python-dateutil==2.8.2 PyYAML==6.0.1 -s3transfer==0.7.0 +s3transfer==0.13.1 setuptools-git-versioning==1.13.5 six==1.16.0 toml==0.10.2 @@ -18,7 +18,7 @@ ruamel.yaml==0.18.5 ruamel.yaml.clib==0.2.8 ruyaml==0.91.0 jschon==0.11.0 -jsonschema==4.19.1 +jsonschema==4.24.1 diagrams==0.23.3 attrs==23.2.0 referencing==0.33.0 diff --git a/creds_rotation/build/requirements.txt b/creds_rotation/build/requirements.txt index 5c02cea82..e7aafecc4 100644 --- a/creds_rotation/build/requirements.txt +++ b/creds_rotation/build/requirements.txt @@ -1,6 +1,6 @@ pytest==7.4.3 PyYAML==6.0.1 -jsonschema==4.19.1 +jsonschema==4.24.1 attrs==23.2.0 referencing==0.33.0 rpds-py==0.17.1 diff --git a/dependencies/tests_requirements.txt b/dependencies/tests_requirements.txt index b37d7ad4a..c39f45cf8 100644 --- a/dependencies/tests_requirements.txt +++ b/dependencies/tests_requirements.txt @@ -16,7 +16,7 @@ ruamel.yaml==0.18.5 ruamel.yaml.clib==0.2.8 ruyaml==0.91.0 jschon==0.11.0 -jsonschema==4.19.1 +jsonschema==4.24.1 diagrams==0.23.3 attrs==23.2.0 referencing==0.33.0 diff --git a/docs/analysis/application-manifest-build-cli.md b/docs/analysis/application-manifest-build-cli.md index 7e98681c9..a5eb49bb9 100644 --- a/docs/analysis/application-manifest-build-cli.md +++ b/docs/analysis/application-manifest-build-cli.md @@ -86,7 +86,7 @@ flowchart TD ## Requirements 1. The CLI must generate AM that validates against [JSON Schema](/schemas/application-manifest.schema.json) -2. The CLI must use as input [Registry Definition v2.0](/schemas/regdef-v2.schema.json) +2. The CLI must use as input [Registry Definition v2.0](/python/envgene/envgenehelper/schemas/regdef-v2.schema.json) (bundled in envgenehelper package) 3. For each application entity listed below, an AM component with the corresponding MIME type must be generated: 1. "Service" -> `application/vnd.qubership.standalone-runnable` 2. Docker image -> `application/vnd.docker.image` @@ -498,7 +498,7 @@ Each individual registry is described by a separate `yaml` file in the `/configu The `name` attribute must match the filename without the extension. -[Registry Definition v2.0](/schemas/regdef-v2.schema.json) +[Registry Definition v2.0](/python/envgene/envgenehelper/schemas/regdef-v2.schema.json) (see envgene-objects.md for schema details) [Example](/examples/sandbox.yml) diff --git a/docs/envgene-objects.md b/docs/envgene-objects.md index 70594413a..778a40f22 100644 --- a/docs/envgene-objects.md +++ b/docs/envgene-objects.md @@ -2717,7 +2717,7 @@ rawConfig: rawTargetProxy: https://proxy.raw.local/ ``` -[Registry Definition v2.0 JSON schema](/schemas/regdef-v2.schema.json) +**[Registry Definition v2.0](/python/envgene/envgenehelper/schemas/regdef-v2.schema.json) JSON schema** — bundled in `envgenehelper` package at `python/envgene/envgenehelper/schemas/regdef-v2.schema.json` ### Application Definition diff --git a/python/artifact-searcher/artifact_searcher/artifact.py b/python/artifact-searcher/artifact_searcher/artifact.py index 3af0edf9b..934570360 100644 --- a/python/artifact-searcher/artifact_searcher/artifact.py +++ b/python/artifact-searcher/artifact_searcher/artifact.py @@ -1,4 +1,5 @@ import asyncio +import base64 import os import re import shutil @@ -8,15 +9,13 @@ from typing import Any, Optional from urllib.parse import urljoin, urlparse, urlunparse from zipfile import ZipFile +from collections import defaultdict import aiohttp import requests -from aiohttp import BasicAuth from artifact_searcher.utils.constants import DEFAULT_REQUEST_TIMEOUT, TCP_CONNECTION_LIMIT, METADATA_XML -from artifact_searcher.utils.models import Registry, Application, FileExtension, Credentials, ArtifactInfo +from artifact_searcher.utils.models import Registry, RegistryV2, Application, FileExtension, Credentials, ArtifactInfo, Provider, MavenConfig from envgenehelper import logger -from requests.auth import HTTPBasicAuth -from artifact_searcher.utils.models import MavenConfig WORKSPACE = os.getenv("WORKSPACE", Path(tempfile.gettempdir()) / "zips") @@ -36,11 +35,18 @@ def convert_nexus_repo_url_to_index_view(url: str) -> str: return urlunparse(parsed._replace(path=new_path)) -def create_artifact_path(app: Application, version: str, repo: str) -> str: - registry_url = app.registry.maven_config.repository_domain_name.rstrip("/") + "/" +def create_artifact_path(app: Application, version: str, repo: str = "") -> str: + registry_url = app.registry.maven_config.repository_domain_name group_id = app.group_id.replace(".", "/") folder = version_to_folder_name(version) - return urljoin(registry_url, f"{repo}/{group_id}/{app.artifact_id}/{folder}/") + + # For cloud providers (AWS/GCP), repo is empty since repositoryDomainName already contains full path + if repo: + path = f"{repo}/{group_id}/{app.artifact_id}/{folder}/" + else: + path = f"{group_id}/{app.artifact_id}/{folder}/" + + return urljoin(registry_url, path) def create_full_url(app: Application, version: str, repo: str, artifact_extension: FileExtension, @@ -135,27 +141,43 @@ def clean_temp_dir(): os.makedirs(WORKSPACE, exist_ok=True) -async def download_all_async(artifacts_info: list[ArtifactInfo], cred: Credentials | None = None): - auth = BasicAuth(login=cred.username, password=cred.password) if cred else None - connector = aiohttp.TCPConnector(limit=TCP_CONNECTION_LIMIT) - timeout = aiohttp.ClientTimeout(total=DEFAULT_REQUEST_TIMEOUT) - async with aiohttp.ClientSession(connector=connector, timeout=timeout, auth=auth) as session: - async with asyncio.TaskGroup() as tg: - tasks = [tg.create_task(download_async(session, artifact_info)) for artifact_info in artifacts_info] - results = [] - errors = [] +def credentials_to_headers(cred: Credentials) -> dict: + # Convert Credentials object to Authorization headers dict. + token = base64.b64encode(f"{cred.username}:{cred.password}".encode()).decode() + return {"Authorization": f"Basic {token}"} - for i, task in enumerate(tasks): - result = task.result() - if not result or result.local_path is None: - errors.append(f"Task {i}: artifact was not downloaded") - else: - results.append(result) - if errors: - raise ValueError("Some tasks failed:\n" + "\n".join(errors)) - - return results +async def download_all_async(artifacts_info: list[ArtifactInfo]): + auth_groups = defaultdict(list) + for artifact in artifacts_info: + # Use sorted tuple of auth items as key, or "none" for None + auth_key = tuple(sorted(artifact.auth_headers.items())) if artifact.auth_headers else "none" + auth_groups[auth_key].append(artifact) + + all_results = [] + for auth_key, artifacts in auth_groups.items(): + headers = artifacts[0].auth_headers + connector = aiohttp.TCPConnector(limit=TCP_CONNECTION_LIMIT) + timeout = aiohttp.ClientTimeout(total=DEFAULT_REQUEST_TIMEOUT) + async with aiohttp.ClientSession(connector=connector, timeout=timeout, headers=headers) as session: + async with asyncio.TaskGroup() as tg: + tasks = [tg.create_task(download_async(session, artifact)) for artifact in artifacts] + results = [] + errors = [] + + for i, task in enumerate(tasks): + result = task.result() + if not result or result.local_path is None: + errors.append(f"Task {i}: artifact was not downloaded") + else: + results.append(result) + + if errors: + raise ValueError("Some tasks failed:\n" + "\n".join(errors)) + + all_results.extend(results) + + return all_results def create_app_artifacts_local_path(app_name, app_version): @@ -199,7 +221,8 @@ async def check_artifact_by_full_url_async( classifier: str = "" ) -> tuple[str, tuple[str, str]] | None: repo_value, repo_pointer = repo - if not repo_value: + # Allow empty repo_value for cloud providers (repositoryName), but not for Nexus/Artifactory + if not repo_value and repo_pointer != "repositoryName": logger.warning(f"[Task {task_id}] [Registry: {app.registry.name}] - {repo_pointer} is not configured") return None @@ -231,19 +254,32 @@ async def check_artifact_by_full_url_async( f"[Task {task_id}] [Application: {app.name}: {version}] - Error checking artifact URL {full_url}: {e}") -def get_repo_value_pointer_dict(registry: Registry): - """Permanent set of repositories for searching of artifacts""" +def _is_cloud_provider(registry) -> bool: + """Check if registry uses AWS or GCP provider.""" + if not getattr(registry, "auth_config", None): + return False + for auth_cfg in registry.auth_config.values(): + if getattr(auth_cfg, "provider", None) in (Provider.AWS, Provider.GCP): + return True + return False + + +# TODO: delete after models are refactored to use polymorphism +def get_repo_value_pointer_dict(registry): + """V2 cloud providers: use empty repo (domain already contains full path). + V1/Nexus/Artifactory: use target fields.""" + if _is_cloud_provider(registry): + return {"": "repositoryName"} maven = registry.maven_config - repos = { + return { maven.target_snapshot: "targetSnapshot", maven.target_staging: "targetStaging", maven.target_release: "targetRelease", maven.snapshot_group: "snapshotGroup", } - return repos -def get_repo_pointer(repo_value: str, registry: Registry): +def get_repo_pointer(repo_value: str, registry): repos_dict = get_repo_value_pointer_dict(registry) return repos_dict.get(repo_value) @@ -253,18 +289,19 @@ async def _attempt_check( version: str, artifact_extension: FileExtension, registry_url: str | None = None, - cred: Credentials | None = None, + auth_headers: dict | None = None, classifier: str = "" ) -> Optional[tuple[str, tuple[str, str]]]: repos_dict = get_repo_value_pointer_dict(app.registry) if registry_url: app.registry.maven_config.repository_domain_name = registry_url - auth = BasicAuth(login=cred.username, password=cred.password) if cred else None + session_headers = auth_headers + timeout = aiohttp.ClientTimeout(total=DEFAULT_REQUEST_TIMEOUT) stop_snapshot_event_for_others = asyncio.Event() stop_artifact_event = asyncio.Event() - async with aiohttp.ClientSession(timeout=timeout, auth=auth) as session: + async with aiohttp.ClientSession(timeout=timeout, headers=session_headers) as session: async with asyncio.TaskGroup() as tg: tasks = [ tg.create_task( @@ -289,8 +326,36 @@ async def _attempt_check( return result +def _should_retry_nexus_url(registry) -> bool: + """Check whether the registry needs Nexus-style URL retry.""" + if isinstance(registry, RegistryV2): + return any(cfg.provider == Provider.NEXUS for cfg in registry.auth_config.values()) + return MavenConfig.is_nexus(registry.maven_config.repository_domain_name) + + +async def _retry_with_nexus_url( + app: Application, + version: str, + artifact_extension: FileExtension, + auth_headers: dict | None, + classifier: str = "" +) -> Optional[tuple[str, tuple[str, str]]]: + """Retry artifact check with Nexus index-view URL conversion.""" + original_domain = app.registry.maven_config.repository_domain_name + fixed_domain = convert_nexus_repo_url_to_index_view(original_domain) + if fixed_domain != original_domain: + logger.info(f"Retrying artifact check with edited domain: {fixed_domain}") + result = await _attempt_check(app, version, artifact_extension, fixed_domain, auth_headers, classifier) + if result is not None: + return result + else: + logger.debug("Domain is same after editing, skipping retry") + return None + + async def check_artifact_async( - app: Application, artifact_extension: FileExtension, version: str, cred: Credentials | None = None, + app: Application, artifact_extension: FileExtension, version: str, + auth_headers: dict | None = None, classifier: str = "") -> Optional[tuple[str, tuple[str, str]]] | None: """ Resolves the full artifact URL and the first repository where it was found. @@ -302,26 +367,34 @@ async def check_artifact_async( - tuple[str, str]: A pair of (repository name, repository pointer/alias in CMDB). Returns None if the artifact could not be resolved """ + repos_dict = get_repo_value_pointer_dict(app.registry) - result = await _attempt_check(app, version, artifact_extension, None, cred) - if result is not None: - return result + # Single repo: no parallelism + if len(repos_dict) == 1: + repo_value, repo_pointer = next(iter(repos_dict.items())) + if not repo_value and repo_pointer != "repositoryName": + logger.warning(f"[Registry: {app.registry.name}] - {repo_pointer} is not configured") + return None + domain = app.registry.maven_config.repository_domain_name + repo_url = domain if not repo_value else domain.rstrip('/') + '/' + repo_value + url = check_artifact(repo_url, app.group_id, app.artifact_id, version, + artifact_extension, auth_headers=auth_headers, classifier=classifier) + if url: + return url, (repo_value, repo_pointer) + if _should_retry_nexus_url(app.registry): + return await _retry_with_nexus_url(app, version, artifact_extension, auth_headers, classifier) + return None - if not MavenConfig.is_nexus(app.registry.maven_config.repository_domain_name): + # Multiple repos: use async parallel checking + result = await _attempt_check(app, version, artifact_extension, auth_headers=auth_headers, classifier=classifier) + if result is not None: return result - # trying to edit url for nexus and repeat - original_domain = app.registry.maven_config.repository_domain_name - fixed_domain = convert_nexus_repo_url_to_index_view(original_domain) - if fixed_domain != original_domain: - logger.info(f"Retrying artifact check with edited domain: {fixed_domain}") - result = await _attempt_check(app, version, artifact_extension, fixed_domain, cred, classifier) - if result is not None: - return result - else: - logger.debug("Domain is same after editing, skipping retry") + if _should_retry_nexus_url(app.registry): + return await _retry_with_nexus_url(app, version, artifact_extension, auth_headers, classifier) logger.warning("Artifact not found") + return None def unzip_file(artifact_id: str, app_name: str, app_version: str, zip_url: str): @@ -358,16 +431,19 @@ def create_aql_artifact(app: Application, artifact_extension: FileExtension, ver return aql -def check_artifacts_by_aql(aql: str, cred: Credentials, url: str) -> list[ArtifactInfo]: +def check_artifacts_by_aql(aql: str, url: str = "", + auth_headers: dict | None = None) -> list[ArtifactInfo]: artifacts = [] - response = requests.post(f"{url}/api/search/aql", data=aql, auth=HTTPBasicAuth(cred.username, cred.password)) + base_url = url.rstrip('/') + response = requests.post(f"{base_url}/api/search/aql", data=aql, headers=auth_headers) + response.raise_for_status() results = response.json() - for result in results.get("results"): + for result in (results.get("results") or []): repo = result.get("repo") path = result.get("path") name = result.get("name") - url = f"{url}/{repo}/{path}/{name}" - artifact = ArtifactInfo(repo=repo, path=path, name=name, url=url) + artifact_url = f"{base_url}/{repo}/{path}/{name}" + artifact = ArtifactInfo(repo=repo, path=path, name=name, url=artifact_url, auth_headers=auth_headers) artifacts.append(artifact) return artifacts @@ -376,23 +452,19 @@ def check_artifacts_by_aql(aql: str, cred: Credentials, url: str) -> list[Artifa # TODO delete after deletion feature getting artifact by not artifact def # -------------------------------------------------------------------------------------- -def download_json_content(url: str, cred: Credentials | None = None) -> dict[str, Any]: - auth = HTTPBasicAuth(cred.username, cred.password) if cred else None - response = requests.get( - url, - auth=auth, - timeout=DEFAULT_REQUEST_TIMEOUT - ) +def download_json_content(url: str, auth_headers: dict | None = None) -> dict[str, Any]: + headers = auth_headers + response = requests.get(url, headers=headers, timeout=DEFAULT_REQUEST_TIMEOUT) response.raise_for_status() json_data = response.json() logger.info(f"Got json data by url {url}") return json_data -def download(url: str, target_path: str, cred: Credentials | None = None) -> str: - auth = HTTPBasicAuth(cred.username, cred.password) if cred else None +def download(url: str, target_path: str, auth_headers: dict | None = None) -> str: + headers = auth_headers + response = requests.get(url, headers=headers, timeout=DEFAULT_REQUEST_TIMEOUT) os.makedirs(os.path.dirname(target_path), exist_ok=True) - response = requests.get(url, auth=auth, timeout=DEFAULT_REQUEST_TIMEOUT) response.raise_for_status() with open(target_path, "wb") as f: f.write(response.content) @@ -402,7 +474,7 @@ def download(url: str, target_path: str, cred: Credentials | None = None) -> str def check_artifact(repo_url: str, group_id: str, artifact_id: str, version: str, artifact_extension: FileExtension, - cred: Credentials | None = None, + auth_headers: dict | None = None, classifier: str = "") -> str | None: if MavenConfig.is_nexus(repo_url): repo_url = convert_nexus_repo_url_to_index_view(repo_url) @@ -412,7 +484,7 @@ def check_artifact(repo_url: str, group_id: str, artifact_id: str, version: str, if "SNAPSHOT" in version: base_path = urljoin(base, f"{group_id}/{artifact_id}/{version}/") - resolved_version = resolve_snapshot_version(base_path, artifact_extension, cred, classifier) + resolved_version = resolve_snapshot_version(base_path, artifact_extension, auth_headers, classifier) if not resolved_version: return None version = resolved_version @@ -420,10 +492,8 @@ def check_artifact(repo_url: str, group_id: str, artifact_id: str, version: str, folder = version_to_folder_name(version) filename = create_artifact_name(artifact_id, artifact_extension, version, classifier) full_url = urljoin(base, f"{group_id}/{artifact_id}/{folder}/{filename}") - auth = HTTPBasicAuth(cred.username, cred.password) if cred else None - try: - response = requests.head(full_url, auth=auth, timeout=DEFAULT_REQUEST_TIMEOUT) + response = requests.head(full_url, headers=auth_headers, timeout=DEFAULT_REQUEST_TIMEOUT) if response.status_code == 200: logger.info( f"[Repository: {repo_url}] [Artifact: {group_id}:{artifact_id}:{version}] - Artifact found: {full_url}" @@ -440,17 +510,13 @@ def check_artifact(repo_url: str, group_id: str, artifact_id: str, version: str, return None -def resolve_snapshot_version(base_path, extension: FileExtension, cred: Credentials | None = None, +def resolve_snapshot_version(base_path, extension: FileExtension, + auth_headers: dict | None = None, classifier: str = "") -> Optional[str]: metadata_url = urljoin(base_path, METADATA_XML) - auth = HTTPBasicAuth(cred.username, cred.password) if cred else None try: - response = requests.get( - metadata_url, - auth=auth, - timeout=DEFAULT_REQUEST_TIMEOUT, - ) + response = requests.get(metadata_url, headers=auth_headers, timeout=DEFAULT_REQUEST_TIMEOUT) if response.status_code != 200: logger.warning(f"Failed to fetch {metadata_url}, status={response.status_code}") return None diff --git a/python/artifact-searcher/artifact_searcher/auth_resolver.py b/python/artifact-searcher/artifact_searcher/auth_resolver.py new file mode 100644 index 000000000..1d0d33738 --- /dev/null +++ b/python/artifact-searcher/artifact_searcher/auth_resolver.py @@ -0,0 +1,139 @@ +import base64 +import json +from typing import Optional + +from artifact_searcher.utils.models import AuthConfig, Provider, RegistryV2 +from envgenehelper import logger + +AUTH_METHOD_USER_PASS = "user_pass" +AUTH_METHOD_SECRET = "secret" +AUTH_METHOD_SERVICE_ACCOUNT = "service_account" +AUTH_METHOD_ANONYMOUS = "anonymous" +AUTH_METHOD_ASSUME_ROLE = "assume_role" +AUTH_METHOD_FEDERATION = "federation" +AUTH_METHOD_OAUTH2 = "oauth2" + +AWS_SERVICE_CODEARTIFACT = "codeartifact" +AWS_TOKEN_KEY = "authorizationToken" +GCP_TOKEN_ATTR = "gcp_authorization_token" + +CRED_FIELD_USERNAME = "username" +CRED_FIELD_PASSWORD = "password" +CRED_FIELD_SECRET = "secret" +CRED_FIELD_DATA = "data" + + +def _get_cred_data(cred_id: str, env_creds: dict) -> dict: + if not env_creds or cred_id not in env_creds: + raise ValueError(f"Credential '{cred_id}' not found in decrypted credentials") + return env_creds[cred_id].get(CRED_FIELD_DATA, {}) + + +def _validate_user_pass_creds(cred_data: dict, context: str) -> tuple[str, str]: + username = cred_data.get(CRED_FIELD_USERNAME) + password = cred_data.get(CRED_FIELD_PASSWORD) + if not username or not password: + raise ValueError(f"{context} requires both username and password in credentials") + return username, password + + +def _aws_bearer(auth_cfg: AuthConfig, cred_data: dict) -> dict: + username, password = _validate_user_pass_creds(cred_data, "AWS secret auth") + + from qubership_pipelines_common_library.v1.utils.utils_aws import AWSCodeArtifactHelper + + token = AWSCodeArtifactHelper.get_authorization_token( + access_key=username, + secret_key=password, + domain=auth_cfg.aws_domain, + region_name=auth_cfg.aws_region + ) + logger.debug(f"AWS CodeArtifact token obtained for domain '{auth_cfg.aws_domain}' in region '{auth_cfg.aws_region}'") + return {"Authorization": f"Bearer {token}"} + + +def _aws_assume_role(auth_cfg: AuthConfig, cred_data: dict) -> dict: + if not auth_cfg.aws_role_arn: + raise ValueError("AWS assume_role requires awsRoleARN to be specified") + + _validate_user_pass_creds(cred_data, "AWS assume_role") + raise NotImplementedError("AWS assume_role auth is not yet implemented") + + +def _gcp_bearer(auth_cfg: AuthConfig, cred_data: dict) -> dict: + sa_key = cred_data.get(CRED_FIELD_SECRET) + if not sa_key: + raise ValueError("GCP service_account requires credential with 'secret' field containing SA JSON key") + + try: + json.loads(sa_key) + except json.JSONDecodeError: + raise ValueError("GCP service account key must be valid JSON") + + try: + from qubership_pipelines_common_library.v2.artifacts_finder.auth.gcp_credentials import GcpCredentialsProvider + except ImportError as e: + raise ValueError(f"GCP dependencies not available: {e}") + + creds = GcpCredentialsProvider().with_service_account_key( + service_account_key_content=sa_key, + ).get_credentials() + logger.debug(f"GCP token obtained for registry '{auth_cfg.gcp_reg_project}'") + return {"Authorization": f"Bearer {getattr(creds, GCP_TOKEN_ATTR)}"} + + +def _gcp_federation(auth_cfg: AuthConfig, cred_data: dict) -> dict: + if not auth_cfg.gcp_oidc: + raise ValueError("GCP federation requires gcpOIDC configuration") + + raise NotImplementedError("GCP federation (OIDC) auth is not yet implemented") + + +def _azure_oauth2(auth_cfg: AuthConfig, cred_data: dict) -> dict: + if not auth_cfg.azure_tenant_id: + raise ValueError("Azure OAuth2 requires azureTenantId") + + raise NotImplementedError("Azure OAuth2 auth is not yet implemented") + + +def _basic_auth(auth_cfg: AuthConfig, cred_data: dict) -> dict: + username, password = _validate_user_pass_creds(cred_data, "Basic auth") + token = base64.b64encode(f"{username}:{password}".encode()).decode() + return {"Authorization": f"Basic {token}"} + + +_PROVIDER_HANDLERS = { + (Provider.AWS, AUTH_METHOD_SECRET): _aws_bearer, + (Provider.AWS, AUTH_METHOD_ASSUME_ROLE): _aws_assume_role, + (Provider.GCP, AUTH_METHOD_SERVICE_ACCOUNT): _gcp_bearer, + (Provider.GCP, AUTH_METHOD_FEDERATION): _gcp_federation, + (Provider.AZURE, AUTH_METHOD_OAUTH2): _azure_oauth2, + (Provider.NEXUS, AUTH_METHOD_USER_PASS): _basic_auth, + (Provider.ARTIFACTORY, AUTH_METHOD_USER_PASS): _basic_auth, +} + + +def resolve_v2_auth_headers(registry: RegistryV2, env_creds: dict) -> Optional[dict]: + """Resolve V2 registry authConfig into HTTP Authorization headers. + Returns None for anonymous access.""" + auth_config = registry.maven_config.auth_config + if auth_config not in registry.auth_config: + raise ValueError( + f"AuthConfig '{auth_config}' not found in registry '{registry.name}'. " + f"Available: {list(registry.auth_config.keys())}") + + auth_cfg = registry.auth_config[auth_config] + + if auth_cfg.auth_method == AUTH_METHOD_ANONYMOUS: + logger.debug(f"Anonymous access for registry '{registry.name}'") + return None + + cred_data = _get_cred_data(auth_cfg.credentials_id, env_creds) + + handler = _PROVIDER_HANDLERS.get((auth_cfg.provider, auth_cfg.auth_method)) + if not handler: + raise ValueError( + f"Unsupported auth configuration (provider='{auth_cfg.provider.value}', " + f"authMethod='{auth_cfg.auth_method}') for registry '{registry.name}'") + + return handler(auth_cfg, cred_data) diff --git a/python/artifact-searcher/artifact_searcher/test_artifact.py b/python/artifact-searcher/artifact_searcher/test_artifact.py index 9ded075ff..da08f1010 100644 --- a/python/artifact-searcher/artifact_searcher/test_artifact.py +++ b/python/artifact-searcher/artifact_searcher/test_artifact.py @@ -78,9 +78,18 @@ def mock_get(url, *args, **kwargs): target_snapshot="repo", target_staging="repo", target_release="repo", - repository_domain_name=base_url, + repository_domain_name=base_url + ) + dcr_cfg = models.DockerConfig( + snapshot_uri="https://docker.example.com/snapshot", + staging_uri="https://docker.example.com/staging", + release_uri="https://docker.example.com/release", + group_uri="https://docker.example.com/group", + snapshot_repo_name="snapshot-repo", + staging_repo_name="staging-repo", + release_repo_name="release-repo", + group_name="test-group" ) - dcr_cfg = models.DockerConfig() reg = models.Registry( name="registry", maven_config=mvn_cfg, diff --git a/python/artifact-searcher/artifact_searcher/test_auth_resolver.py b/python/artifact-searcher/artifact_searcher/test_auth_resolver.py new file mode 100644 index 000000000..affacc7dd --- /dev/null +++ b/python/artifact-searcher/artifact_searcher/test_auth_resolver.py @@ -0,0 +1,217 @@ +import base64 +import json +import sys +from unittest.mock import MagicMock, patch + +import pytest + +from artifact_searcher.auth_resolver import resolve_v2_auth_headers +from artifact_searcher.utils.models import AuthConfig, Provider, RegistryV2, MavenConfigV2 + + +@pytest.fixture +def env_creds(): + return { + "aws-cred": { + "data": { + "username": "AKIA_ACCESS_KEY", + "password": "secret_key_value" + } + }, + "gcp-cred": { + "data": { + "secret": '{"type": "service_account", "project_id": "my-project"}' + } + }, + "nexus-cred": { + "data": { + "username": "nexus_user", + "password": "nexus_pass" + } + }, + "artifactory-cred": { + "data": { + "username": "artifactory_user", + "password": "artifactory_pass" + } + }, + "anonymous-cred": { + "data": {} + } + } + + +@pytest.fixture +def base_registry_v2(): + return RegistryV2( + version="2.0", + name="test-registry", + auth_config={}, + maven_config=MavenConfigV2( + auth_config="test-auth", + repository_domain_name="https://registry.example.com" + ) + ) + + +class TestAnonymousAccess: + def test_nexus_anonymous_auth(self, base_registry_v2, env_creds): + base_registry_v2.auth_config = { + "test-auth": AuthConfig(provider=Provider.NEXUS, auth_method="anonymous") + } + + result = resolve_v2_auth_headers(base_registry_v2, env_creds) + + assert result is None + + def test_artifactory_anonymous_auth(self, base_registry_v2, env_creds): + base_registry_v2.auth_config = { + "test-auth": AuthConfig(provider=Provider.ARTIFACTORY, auth_method="anonymous") + } + + result = resolve_v2_auth_headers(base_registry_v2, env_creds) + + assert result is None + + def test_missing_authconfig_reference(self, base_registry_v2, env_creds): + base_registry_v2.maven_config.auth_config = "nonexistent" + base_registry_v2.auth_config = {} + + with pytest.raises(ValueError, match="AuthConfig 'nonexistent' not found"): + resolve_v2_auth_headers(base_registry_v2, env_creds) + + +class TestAWSAuthentication: + def test_aws_secret_success(self, base_registry_v2, env_creds, monkeypatch): + # Mock v1 library module for AWSCodeArtifactHelper + fake_utils_aws = MagicMock() + + monkeypatch.setitem(sys.modules, 'qubership_pipelines_common_library.v1.utils.utils_aws', fake_utils_aws) + + # Mock AWSCodeArtifactHelper.get_authorization_token static method + mock_helper_class = MagicMock() + mock_helper_class.get_authorization_token.return_value = "aws_token_123" + + fake_utils_aws.AWSCodeArtifactHelper = mock_helper_class + + base_registry_v2.auth_config = { + "aws-auth": AuthConfig( + credentials_id="aws-cred", + provider=Provider.AWS, + auth_method="secret", + aws_region="us-east-1", + aws_domain="my-domain" + ) + } + base_registry_v2.maven_config.auth_config = "aws-auth" + + result = resolve_v2_auth_headers(base_registry_v2, env_creds) + + assert result == {"Authorization": "Bearer aws_token_123"} + mock_helper_class.get_authorization_token.assert_called_once_with( + access_key="AKIA_ACCESS_KEY", + secret_key="secret_key_value", + domain="my-domain", + region_name="us-east-1" + ) + + def test_aws_missing_credentials(self, base_registry_v2, env_creds): + env_creds["aws-cred"]["data"] = {"username": "access_key"} + + base_registry_v2.auth_config = { + "aws-auth": AuthConfig( + credentials_id="aws-cred", + provider=Provider.AWS, + auth_method="secret", + aws_region="us-east-1", + aws_domain="my-domain" + ) + } + base_registry_v2.maven_config.auth_config = "aws-auth" + + with pytest.raises(ValueError, match="AWS secret auth requires both username and password in credentials"): + resolve_v2_auth_headers(base_registry_v2, env_creds) + + +class TestGCPAuthentication: + def test_gcp_service_account_success(self, base_registry_v2, env_creds, monkeypatch): + # Create fake module + fake_gcp_creds = MagicMock() + monkeypatch.setitem(sys.modules, 'qubership_pipelines_common_library.v2.artifacts_finder.auth.gcp_credentials', fake_gcp_creds) + + # Setup mock + mock_gcp_provider = MagicMock() + fake_gcp_creds.GcpCredentialsProvider = mock_gcp_provider + + mock_creds = MagicMock() + mock_creds.gcp_authorization_token = "gcp_token_123" + mock_gcp_provider.return_value.with_service_account_key.return_value.get_credentials.return_value = mock_creds + + base_registry_v2.auth_config = { + "gcp-auth": AuthConfig( + credentials_id="gcp-cred", + provider=Provider.GCP, + auth_method="service_account", + gcp_reg_project="my-project" + ) + } + base_registry_v2.maven_config.auth_config = "gcp-auth" + + result = resolve_v2_auth_headers(base_registry_v2, env_creds) + + assert result == {"Authorization": "Bearer gcp_token_123"} + mock_gcp_provider.return_value.with_service_account_key.assert_called_once_with( + service_account_key_content='{"type": "service_account", "project_id": "my-project"}' + ) + + def test_gcp_missing_secret(self, base_registry_v2, env_creds): + env_creds["gcp-cred"]["data"] = {} + + base_registry_v2.auth_config = { + "gcp-auth": AuthConfig( + credentials_id="gcp-cred", + provider=Provider.GCP, + auth_method="service_account" + ) + } + base_registry_v2.maven_config.auth_config = "gcp-auth" + + # Empty cred_data should raise error about missing secret + with pytest.raises(ValueError, match="GCP service_account requires credential with 'secret' field"): + resolve_v2_auth_headers(base_registry_v2, env_creds) + + def test_gcp_invalid_json(self, base_registry_v2, env_creds): + env_creds["gcp-cred"]["data"]["secret"] = "not valid json" + + base_registry_v2.auth_config = { + "gcp-auth": AuthConfig( + credentials_id="gcp-cred", + provider=Provider.GCP, + auth_method="service_account" + ) + } + base_registry_v2.maven_config.auth_config = "gcp-auth" + + with pytest.raises(ValueError, match="GCP service account key must be valid JSON"): + resolve_v2_auth_headers(base_registry_v2, env_creds) + + +class TestNexusArtifactoryAuthentication: + @pytest.mark.parametrize("provider,cred_id,username,password", [ + (Provider.NEXUS, "nexus-cred", "nexus_user", "nexus_pass"), + (Provider.ARTIFACTORY, "artifactory-cred", "artifactory_user", "artifactory_pass"), + ]) + def test_basic_auth_success(self, provider, cred_id, username, password, base_registry_v2, env_creds): + base_registry_v2.auth_config = { + "basic-auth": AuthConfig( + credentials_id=cred_id, + provider=provider, + auth_method="user_pass" + ) + } + base_registry_v2.maven_config.auth_config = "basic-auth" + + result = resolve_v2_auth_headers(base_registry_v2, env_creds) + + expected_token = base64.b64encode(f"{username}:{password}".encode()).decode() + assert result == {"Authorization": f"Basic {expected_token}"} diff --git a/python/artifact-searcher/artifact_searcher/utils/models.py b/python/artifact-searcher/artifact_searcher/utils/models.py index bf34153c2..c8832eb1f 100644 --- a/python/artifact-searcher/artifact_searcher/utils/models.py +++ b/python/artifact-searcher/artifact_searcher/utils/models.py @@ -1,7 +1,10 @@ from enum import Enum from typing import Optional +import base64 -from pydantic import BaseModel, ConfigDict, field_validator, Field, model_validator +import jsonschema +from envgenehelper.config_helper import get_regdef_v2_schema +from pydantic import BaseModel, ConfigDict, field_validator, Field from pydantic.alias_generators import to_camel import requests @@ -99,6 +102,7 @@ class ArtifactInfo(BaseSchema): path: Optional[str] = "" local_path: Optional[str] = "" name: Optional[str] = "" + auth_headers: Optional[dict] = None class Registry(BaseSchema): @@ -112,15 +116,165 @@ class Registry(BaseSchema): helm_config: Optional[HelmConfig] = None helm_app_config: Optional[HelmAppConfig] = None + def resolve_auth(self, env_creds: Optional[dict] = None) -> Optional[dict]: + """Returns auth headers dict for V1 registries (basic auth). + Returns None if no credentials configured.""" + if not self.credentials_id or not env_creds: + return None + cred_data = env_creds.get(self.credentials_id, {}).get("data", {}) + username = cred_data.get("username") + password = cred_data.get("password") + if username and password: + token = base64.b64encode(f"{username}:{password}".encode()).decode() + return {"Authorization": f"Basic {token}"} + return None + + +REGDEF_V2_VERSION = "2.0" + + +class Provider(str, Enum): + NEXUS = "nexus" + ARTIFACTORY = "artifactory" + AWS = "aws" + GCP = "gcp" + AZURE = "azure" + + +class GcpOIDC(BaseSchema): + url: str = Field(alias="URL") + custom_params: Optional[list[dict[str, str]]] = None + + +class AuthConfig(BaseSchema): + credentials_id: Optional[str] = None + auth_type: Optional[str] = None + provider: Provider + auth_method: str + aws_region: Optional[str] = None + aws_domain: Optional[str] = None + aws_role_arn: Optional[str] = Field(default=None, alias="awsRoleARN") + aws_role_session_prefix: Optional[str] = None + gcp_oidc: Optional[GcpOIDC] = Field(default=None, alias="gcpOIDC") + gcp_reg_project: Optional[str] = None + gcp_reg_pool_id: Optional[str] = None + gcp_reg_provider_id: Optional[str] = None + gcp_reg_sa_email: Optional[str] = Field(default=None, alias="gcpRegSAEmail") + gcp_region: Optional[str] = None + azure_tenant_id: Optional[str] = None + azure_acr_resource: Optional[str] = Field(default=None, alias="azureACRResource") + azure_acr_name: Optional[str] = Field(default=None, alias="azureACRName") + azure_artifacts_resource: Optional[str] = None + + +class MavenConfigV2(BaseSchema): + auth_config: str + repository_domain_name: str + target_snapshot: Optional[str] = "" + target_staging: Optional[str] = "" + target_release: Optional[str] = "" + snapshot_group: Optional[str] = "" + release_group: Optional[str] = "" + + @field_validator('repository_domain_name') + def ensure_trailing_slash(cls, value): + return value.rstrip("/") + "/" + + +class DockerConfigV2(BaseSchema): + auth_config: str + snapshot_uri: str + staging_uri: str + release_uri: str + group_uri: str + snapshot_repo_name: str + staging_repo_name: str + release_repo_name: str + group_name: str + + +class GoConfigV2(BaseSchema): + auth_config: str + repository_domain_name: str + go_target_snapshot: str + go_target_release: str + go_proxy_repository: str + + +class RawConfigV2(BaseSchema): + auth_config: str + repository_domain_name: str + raw_target_snapshot: str + raw_target_release: str + raw_target_staging: str + raw_target_proxy: str + + +class NpmConfigV2(BaseSchema): + auth_config: str + repository_domain_name: str + npm_target_snapshot: str + npm_target_release: str + + +class HelmConfigV2(BaseSchema): + auth_config: str + repository_domain_name: str + helm_target_staging: str + helm_target_release: str + + +class HelmAppConfigV2(BaseSchema): + auth_config: str + repository_domain_name: str + helm_staging_repo_name: str + helm_release_repo_name: str + helm_group_repo_name: str + helm_dev_repo_name: str + + +class RegistryV2(BaseSchema): + name: str + version: str = REGDEF_V2_VERSION + auth_config: dict[str, AuthConfig] = {} + maven_config: MavenConfigV2 + docker_config: Optional[DockerConfigV2] = None + go_config: Optional[GoConfigV2] = None + raw_config: Optional[RawConfigV2] = None + npm_config: Optional[NpmConfigV2] = None + helm_config: Optional[HelmConfigV2] = None + helm_app_config: Optional[HelmAppConfigV2] = None + + def resolve_auth(self, env_creds: Optional[dict] = None) -> Optional[dict]: + """Returns auth headers dict for V2 registries (unified API with V1). + Returns None if anonymous or no credentials configured.""" + from artifact_searcher.auth_resolver import resolve_v2_auth_headers + return resolve_v2_auth_headers(self, env_creds or {}) + + +def parse_registry(data: dict) -> Registry | RegistryV2: + if data.get("version") == REGDEF_V2_VERSION or "authConfig" in data: + schema = get_regdef_v2_schema() + jsonschema.validate(instance=data, schema=schema) + return RegistryV2.model_validate(data) + return Registry.model_validate(data) + # artifact definition class Application(BaseSchema): name: str artifact_id: str group_id: str - registry: Registry + registry: Registry | RegistryV2 solution_descriptor: bool = False + @field_validator('registry', mode='before') + @classmethod + def parse_registry_field(cls, v): + if isinstance(v, dict): + return parse_registry(v) + return v + class FileExtension(str, Enum): ZIP = 'zip' diff --git a/python/artifact-searcher/pyproject.toml b/python/artifact-searcher/pyproject.toml index 131ac4b7c..5a11558ec 100644 --- a/python/artifact-searcher/pyproject.toml +++ b/python/artifact-searcher/pyproject.toml @@ -7,16 +7,20 @@ name = "artifact_searcher" version = "0.0.1" requires-python = "~=3.12" dependencies = [ - "pydantic~=2.10.6", - "requests~=2.32.3", - "deepdiff~=8.0.1", - "PyYAML~=6.0.2", - "responses~=0.25.7", - "aiohttp~=3.11.18", - "asyncio~=3.4.3", - "aioresponses~=0.7.8", - "pytest-asyncio~=1.0.0", - "pytest-aiohttp~=1.1.0" + "pydantic==2.10.6", + "requests==2.32.3", + "deepdiff==8.0.1", + "PyYAML==6.0.2", + "responses==0.25.7", + "aiohttp==3.11.18", + "asyncio==3.4.3", + "aioresponses==0.7.8", + "pytest-asyncio==1.0.0", + "pytest-aiohttp==1.1.0", + "boto3==1.39.4", + "google-auth==2.34.0", + "qubership-pipelines-common-library==2.0.3", + "jsonschema==4.24.1" ] [project.optional-dependencies] diff --git a/python/envgene/envgenehelper/__init__.py b/python/envgene/envgenehelper/__init__.py index 8adea78a9..cace6f169 100644 --- a/python/envgene/envgenehelper/__init__.py +++ b/python/envgene/envgenehelper/__init__.py @@ -2,7 +2,7 @@ from .yaml_helper import * from .file_helper import * from .business_helper import * -from .config_helper import get_envgene_config_yaml +from .config_helper import get_envgene_config_yaml, get_regdef_schema, get_regdef_v2_schema, validate_regdef_or_fail, get_regdef_schema_for_content from .json_helper import * from .collections_helper import * from .logger import logger diff --git a/python/envgene/envgenehelper/config_helper.py b/python/envgene/envgenehelper/config_helper.py index 716494539..46f795be5 100644 --- a/python/envgene/envgenehelper/config_helper.py +++ b/python/envgene/envgenehelper/config_helper.py @@ -1,8 +1,10 @@ +from importlib.resources import files from os import getenv, path import json from pathlib import Path from envgenehelper import openYaml, get_empty_yaml, getenv_with_error +from envgenehelper.yaml_helper import validate_yaml_by_scheme_or_fail import jsonschema from .logger import logger @@ -14,6 +16,31 @@ FERNET_ID = "Fernet" SOPS_ID = "SOPS" +REGDEF_V2_VERSION = "2.0" + +def get_regdef_schema() -> dict: + """Load RegDef V1 schema from package resources""" + return json.loads(files("envgenehelper").joinpath("schemas/regdef.schema.json").read_text(encoding="utf-8")) + + +def get_regdef_v2_schema() -> dict: + """Load RegDef V2 schema from package resources""" + return json.loads(files("envgenehelper").joinpath("schemas/regdef-v2.schema.json").read_text(encoding="utf-8")) + + +def get_regdef_schema_for_content(content: dict) -> dict: + """Get the appropriate schema (V1 or V2) based on registry definition content""" + if content.get("version") == REGDEF_V2_VERSION or "authConfig" in content: + return get_regdef_v2_schema() + return get_regdef_schema() + + +def validate_regdef_or_fail(yaml_file_path: str): + """Validate a registry definition YAML file (V1 or V2) by path""" + content = openYaml(yaml_file_path) + schema = get_regdef_schema_for_content(content) + validate_yaml_by_scheme_or_fail(yaml_file_path=yaml_file_path, input_schema_content=schema) + def get_schema(schema_name): schemas_folder = "schemas" diff --git a/schemas/regdef-v2.schema.json b/python/envgene/envgenehelper/schemas/regdef-v2.schema.json similarity index 96% rename from schemas/regdef-v2.schema.json rename to python/envgene/envgenehelper/schemas/regdef-v2.schema.json index ff671b696..7fcd62784 100644 --- a/schemas/regdef-v2.schema.json +++ b/python/envgene/envgenehelper/schemas/regdef-v2.schema.json @@ -162,6 +162,22 @@ "required": ["credentialsId"] } }, + { + "if": { + "properties": { + "provider": { + "const": "aws" + }, + "authMethod": { + "const": "secret" + } + }, + "required": ["provider", "authMethod"] + }, + "then": { + "required": ["awsRegion"] + } + }, { "if": { "properties": { diff --git a/python/envgene/envgenehelper/schemas/regdef.schema.json b/python/envgene/envgenehelper/schemas/regdef.schema.json new file mode 100644 index 000000000..7b20d25af --- /dev/null +++ b/python/envgene/envgenehelper/schemas/regdef.schema.json @@ -0,0 +1,224 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "credentialsId": { + "type": "string" + }, + "mavenConfig": { + "$ref": "#/definitions/MavenConfig" + }, + "dockerConfig": { + "$ref": "#/definitions/DockerConfig" + }, + "goConfig": { + "$ref": "#/definitions/GoConfig" + }, + "rawConfig": { + "$ref": "#/definitions/RawConfig" + }, + "npmConfig": { + "$ref": "#/definitions/NpmConfig" + }, + "helmConfig": { + "$ref": "#/definitions/HelmConfig" + }, + "helmAppConfig": { + "$ref": "#/definitions/HelmAppConfig" + } + }, + "required": [ + "name", + "credentialsId", + "mavenConfig", + "dockerConfig" + ], + "definitions": { + "mapString": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "DockerConfig": { + "type": "object", + "additionalProperties": false, + "properties": { + "snapshotUri": { + "type": "string" + }, + "stagingUri": { + "type": "string" + }, + "releaseUri": { + "type": "string" + }, + "groupUri": { + "type": "string" + }, + "snapshotRepoName": { + "type": "string" + }, + "stagingRepoName": { + "type": "string" + }, + "releaseRepoName": { + "type": "string" + }, + "groupName": { + "type": "string" + } + }, + "required": [ + "groupName", + "groupUri", + "releaseRepoName", + "releaseUri", + "snapshotRepoName", + "snapshotUri", + "stagingRepoName", + "stagingUri" + ] + }, + "MavenConfig": { + "type": "object", + "additionalProperties": false, + "properties": { + "repositoryDomainName": { + "type": "string" + }, + "fullRepositoryUrl": { + "type": "string" + }, + "targetSnapshot": { + "type": "string" + }, + "targetStaging": { + "type": "string" + }, + "targetRelease": { + "type": "string" + }, + "snapshotGroup": { + "type": "string" + }, + "releaseGroup": { + "type": "string" + } + }, + "required": [ + "fullRepositoryUrl", + "releaseGroup", + "repositoryDomainName", + "snapshotGroup", + "targetRelease", + "targetSnapshot", + "targetStaging" + ] + }, + "GoConfig": { + "type": "object", + "additionalProperties": false, + "properties": { + "goTargetSnapshot": { + "type": "string" + }, + "goTargetRelease": { + "type": "string" + }, + "goProxyRepository": { + "type": "string" + } + }, + "required": [ + "goTargetSnapshot", + "goTargetRelease", + "goProxyRepository" + ] + }, + "RawConfig": { + "type": "object", + "additionalProperties": false, + "properties": { + "rawTargetSnapshot": { + "type": "string" + }, + "rawTargetRelease": { + "type": "string" + }, + "rawTargetStaging": { + "type": "string" + }, + "rawTargetProxy": { + "type": "string" + } + }, + "required": [ + "rawTargetSnapshot", + "rawTargetRelease", + "rawTargetStaging", + "rawTargetProxy" + ] + }, + "NpmConfig": { + "type": "object", + "additionalProperties": false, + "properties": { + "npmTargetSnapshot": { + "type": "string" + }, + "npmTargetRelease": { + "type": "string" + } + }, + "required": [ + "npmTargetSnapshot", + "npmTargetRelease" + ] + }, + "HelmConfig": { + "type": "object", + "additionalProperties": false, + "properties": { + "helmTargetStaging": { + "type": "string" + }, + "helmTargetRelease": { + "type": "string" + } + }, + "required": [ + "helmTargetStaging", + "helmTargetRelease" + ] + }, + "HelmAppConfig": { + "type": "object", + "additionalProperties": false, + "properties": { + "helmStagingRepoName": { + "type": "string" + }, + "helmReleaseRepoName": { + "type": "string" + }, + "helmGroupRepoName": { + "type": "string" + }, + "helmDevRepoName": { + "type": "string" + } + }, + "required": [ + "helmStagingRepoName", + "helmReleaseRepoName", + "helmGroupRepoName", + "helmDevRepoName" + ] + } + } +} diff --git a/python/envgene/pyproject.toml b/python/envgene/pyproject.toml index 7d325f5f1..adef7cbe0 100644 --- a/python/envgene/pyproject.toml +++ b/python/envgene/pyproject.toml @@ -13,6 +13,9 @@ dependencies = [ ] +[tool.setuptools.package-data] +envgenehelper = ["schemas/*.json"] + [tool.black] line-length = 120 skip-string-normalization = true diff --git a/scripts/build_env/env_template/process_env_template.py b/scripts/build_env/env_template/process_env_template.py index 4c47339ac..e1314cc37 100644 --- a/scripts/build_env/env_template/process_env_template.py +++ b/scripts/build_env/env_template/process_env_template.py @@ -63,11 +63,11 @@ def validate_url(url, group_id, artifact_id, version): # logic resolving template by artifact definition -async def resolve_artifact_new_logic(app_def: Application, app_version: str, template_dest: str, cred: Credentials) -> str: +async def resolve_artifact_new_logic(app_def: Application, app_version: str, template_dest: str, auth_headers: dict = None) -> str: template_url = None resolved_version = app_version - dd_artifact_info = await artifact.check_artifact_async(app_def, FileExtension.JSON, app_version, cred) + dd_artifact_info = await artifact.check_artifact_async(app_def, FileExtension.JSON, app_version, auth_headers=auth_headers) if dd_artifact_info: logger.info("Loading environment template artifact info from deployment descriptor...") dd_url, (repo_name, _) = dd_artifact_info @@ -75,7 +75,7 @@ async def resolve_artifact_new_logic(app_def: Application, app_version: str, tem if "-SNAPSHOT" in app_version: resolved_version = extract_snapshot_version(dd_url, app_version) - dd_config = artifact.download_json_content(dd_url, cred) + dd_config = artifact.download_json_content(dd_url, auth_headers=auth_headers) group_id, artifact_id, version = parse_maven_coord_from_dd(dd_config) logger.info( f"Parsed maven coordinates: group_id={group_id}, artifact_id={artifact_id}, version={version} from dd") @@ -87,12 +87,13 @@ async def resolve_artifact_new_logic(app_def: Application, app_version: str, tem if not repo_url: repo_url = f"{app_def.registry.maven_config.repository_domain_name}{repo_name}" logger.info(f"building repo url from the repo name : {repo_url}") - template_url = artifact.check_artifact(repo_url, group_id, artifact_id, version, FileExtension.ZIP, cred) + template_url = artifact.check_artifact(repo_url, group_id, artifact_id, version, FileExtension.ZIP, + auth_headers=auth_headers) validate_url(template_url, group_id, artifact_id, version) else: logger.info("Loading environment template artifact from zip directly...") group_id, artifact_id, version = app_def.group_id, app_def.artifact_id, app_version - artifact_info = await artifact.check_artifact_async(app_def, FileExtension.ZIP, app_version, cred) + artifact_info = await artifact.check_artifact_async(app_def, FileExtension.ZIP, app_version, auth_headers=auth_headers) if artifact_info: template_url, _ = artifact_info validate_url(template_url, group_id, artifact_id, version) @@ -100,7 +101,7 @@ async def resolve_artifact_new_logic(app_def: Application, app_version: str, tem resolved_version = extract_snapshot_version(template_url, app_version) logger.info(f"Environment template url has been resolved: {template_url}") artifact_dest = tempfile.mkstemp(suffix='.zip')[1] - artifact.download(template_url, artifact_dest, cred) + artifact.download(template_url, artifact_dest, auth_headers=auth_headers) unpack_archive(artifact_dest, template_dest) return resolved_version @@ -133,32 +134,33 @@ async def resolve_artifact_old_logic(env_definition: dict, template_dest: str, c repository_username = fetch_cred_value(registry.get("username"), cred_config) repository_password = fetch_cred_value(registry.get("password"), cred_config) cred = Credentials(username=repository_username, password=repository_password) + auth_headers = artifact.credentials_to_headers(cred) if cred.username and cred.password else None template_url = None resolved_version = dd_version - dd_url = artifact.check_artifact(dd_repo_url, group_id, artifact_id, dd_version, FileExtension.JSON, cred) + dd_url = artifact.check_artifact(dd_repo_url, group_id, artifact_id, dd_version, FileExtension.JSON, auth_headers=auth_headers) if dd_url: logger.info(f"Deployment descriptor url for environment template has been resolved: {dd_url}") if "-SNAPSHOT" in dd_version: resolved_version = extract_snapshot_version(dd_url, dd_version) - dd_config = artifact.download_json_content(dd_url, cred) + dd_config = artifact.download_json_content(dd_url, auth_headers=auth_headers) group_id, artifact_id, version = parse_maven_coord_from_dd(dd_config) logger.info( f"Parsed maven coordinates from dd: group_id={group_id}, artifact_id={artifact_id}, version={version}") if not all([group_id, artifact_id, version]): raise ValueError(f"Invalid maven coordinates from deployment descriptor {dd_url}") - template_url = artifact.check_artifact(repo_url, group_id, artifact_id, version, FileExtension.ZIP, cred) + template_url = artifact.check_artifact(repo_url, group_id, artifact_id, version, FileExtension.ZIP, auth_headers=auth_headers) validate_url(template_url, group_id, artifact_id, version) else: logger.info("Loading environment template artifact from zip directly...") - template_url = artifact.check_artifact(repo_url, group_id, artifact_id, dd_version, FileExtension.ZIP, cred) + template_url = artifact.check_artifact(repo_url, group_id, artifact_id, dd_version, FileExtension.ZIP, auth_headers=auth_headers) validate_url(template_url, group_id, artifact_id, dd_version) if "-SNAPSHOT" in dd_version: resolved_version = extract_snapshot_version(template_url, dd_version) logger.info(f"Environment template url has been resolved: {template_url}") artifact_dest = tempfile.mkstemp(suffix='.zip')[1] - artifact.download(template_url, artifact_dest, cred) + artifact.download(template_url, artifact_dest, auth_headers=auth_headers) unpack_archive(artifact_dest, template_dest) return resolved_version @@ -200,10 +202,11 @@ def process_env_template() -> dict: if not artifact_path: raise FileNotFoundError(f"No artifact definition file found for {app_name}") app_def = Application.model_validate(openYaml(artifact_path)) - cred = get_registry_creds(app_def.registry, cred_config) + + auth_headers = app_def.registry.resolve_auth(cred_config) logger.info(f'Use template resolving new logic for {appver}') - tasks[template_type] = resolve_artifact_new_logic(app_def, app_version, template_dest, cred) + tasks[template_type] = resolve_artifact_new_logic(app_def, app_version, template_dest, auth_headers) async def resolve_all(): results = await asyncio.gather(*tasks.values()) diff --git a/scripts/build_env/process_sd.py b/scripts/build_env/process_sd.py index 8ff6c207d..47c0d103a 100644 --- a/scripts/build_env/process_sd.py +++ b/scripts/build_env/process_sd.py @@ -301,12 +301,17 @@ def download_sd_by_appver(app_name: str, version: str, plugins: PluginEngine) -> # TODO: check if job would fail without plugins app_def = get_appdef_for_app(f"{app_name}:{version}", app_name, plugins) - artifact_info = asyncio.run(artifact.check_artifact_async(app_def, artifact.FileExtension.JSON, version)) + env_creds = helper.get_cred_config() + auth_headers = app_def.registry.resolve_auth(env_creds) + + artifact_info = asyncio.run( + artifact.check_artifact_async(app_def, artifact.FileExtension.JSON, version, + auth_headers=auth_headers)) if not artifact_info: raise ValueError( f'Solution descriptor content was not received for {app_name}:{version}') sd_url, _ = artifact_info - return artifact.download_json_content(sd_url) + return artifact.download_json_content(sd_url, auth_headers=auth_headers) def get_appdef_for_app(appver: str, app_name: str, plugins: PluginEngine) -> artifact_models.Application: @@ -317,7 +322,7 @@ def get_appdef_for_app(appver: str, app_name: str, plugins: PluginEngine) -> art app_def_path = identify_yaml_extension(f"{APP_DEFS_PATH}/{app_name}") app_dict = helper.openYaml(app_def_path) reg_def_path = identify_yaml_extension(f"{REG_DEFS_PATH}/{app_dict['registryName']}") - app_dict['registry'] = artifact_models.Registry.model_validate(helper.openYaml(reg_def_path)) + app_dict['registry'] = artifact_models.parse_registry(helper.openYaml(reg_def_path)) app_def = artifact_models.Application.model_validate(app_dict) return app_def diff --git a/scripts/build_env/render_config_env.py b/scripts/build_env/render_config_env.py index 94aced3d0..8ea5383ef 100644 --- a/scripts/build_env/render_config_env.py +++ b/scripts/build_env/render_config_env.py @@ -14,6 +14,7 @@ from jinja.replace_ansible_stuff import replace_ansible_stuff, escaping_quotation SCHEMAS_DIR = Path(__file__).resolve().parents[2] / "schemas" +APPDEF_SCHEMA = str(SCHEMAS_DIR / "appdef.schema.json") TD_SCHEMA = str(SCHEMAS_DIR / "template-descriptor.schema.json") yml = create_yaml_processor() @@ -556,7 +557,7 @@ def validate_appregdefs(self): logger.warning(f"No AppDef YAMLs found in {appdef_dir}") for file in appdef_files: logger.info(f"AppDef file: {file}") - validate_yaml_by_scheme_or_fail(file, str(SCHEMAS_DIR / "appdef.schema.json")) + validate_yaml_by_scheme_or_fail(file, APPDEF_SCHEMA) if os.path.exists(regdef_dir): regdef_files = findAllYamlsInDir(regdef_dir) @@ -564,7 +565,7 @@ def validate_appregdefs(self): logger.warning(f"No RegDef YAMLs found in {regdef_dir}") for file in regdef_files: logger.info(f"RegDef file: {file}") - validate_yaml_by_scheme_or_fail(file, str(SCHEMAS_DIR / "regdef.schema.json")) + validate_regdef_or_fail(file) def process_app_reg_defs(self, env_name: str, extra_env: dict): logger.info( From 781d40523455cac51fec66a4907cb118c0516e45 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Wed, 15 Apr 2026 14:05:47 +0000 Subject: [PATCH 143/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 6a4b60956..deb0e70e5 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -75,9 +75,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.31.25" - DOCKER_IMAGE_TAG_ENVGENE: "1.31.25" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.31.25" + DOCKER_IMAGE_TAG_PIPEGENE: "1.32.0" + DOCKER_IMAGE_TAG_ENVGENE: "1.32.0" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.32.0" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 476e33492..f16dbae84 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.31.25 +version: 1.32.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 9f400260b..9933a8655 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.31.25 +version: 1.32.0 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 79b3b2f51..529a9eebd 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.31.25", + "envgene_version": "1.32.0", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From e8dd1bef2657c2c6abbcb461d773b915138012d1 Mon Sep 17 00:00:00 2001 From: Geetha Gadde Date: Thu, 16 Apr 2026 13:31:58 +0530 Subject: [PATCH 144/161] feat: Enhance Test data for Calculator CLI (#1234) --- .../monitoring-origin/namespace.yml | 8 + .../cleanup/monitoring-origin/parameters.yaml | 28 ++++ .../effective-set/cleanup/pg/parameters.yaml | 20 +++ .../values/deployment-parameters.yaml | 146 ++++++++++++------ .../values/deployment-parameters.yaml | 50 +++++- .../environments/cluster-01/pl-01/tenant.yml | 14 +- 6 files changed, 209 insertions(+), 57 deletions(-) diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/Namespaces/monitoring-origin/namespace.yml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/Namespaces/monitoring-origin/namespace.yml index 660474d64..b2f546b47 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/Namespaces/monitoring-origin/namespace.yml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/Namespaces/monitoring-origin/namespace.yml @@ -13,6 +13,14 @@ profile: name: "dev_bss_override" baseline: "dev" deployParameters: + server_port: 8080 + app_version: "3.0" + ssl_enabled: true + debug_mode_test: "true" + api_port: ${server_port} + service_version: ${app_version} + use_ssl: ${ssl_enabled} + log_level: ${debug_mode_test} ENVGENE_CONFIG_REF_NAME: "branch_name" ENVGENE_CONFIG_TAG: "No Ref tag" KMS_CERT_IN_BASE64: "${creds.get( \"kms-cert\" ).secret}" # paramset: test-deploy-creds version: 1 source: template diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/monitoring-origin/parameters.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/monitoring-origin/parameters.yaml index ee9dbe1bb..d7f81b496 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/monitoring-origin/parameters.yaml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/monitoring-origin/parameters.yaml @@ -73,9 +73,37 @@ TRACING_HOST: tracing-agent TRACING_UI_URL: https://cluster-01.qubership.org ZOOKEEPER_ADDRESS: zookeeper.zookeeper:2181 ZOOKEEPER_URL: zookeeper.zookeeper:2181 +api_config: + connection: + host: db.example.com + port: 5432 +api_port: 8080 +app_version: '3.0' bss-app-exist: false core: apps: volumes: outputs: capacity: 20Gi +database_config: + connection: + host: db.example.com + port: 5432 +debug_mode_test: 'true' +log_level: 'true' +rendered_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 +server_port: 8080 +service_version: '3.0' +ssl_enabled: true +use_ssl: true +yaml_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/pg/parameters.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/pg/parameters.yaml index f1613b417..6499c0b7d 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/pg/parameters.yaml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/cleanup/pg/parameters.yaml @@ -68,3 +68,23 @@ TRACING_HOST: tracing-agent TRACING_UI_URL: https://cluster-01.qubership.org ZOOKEEPER_ADDRESS: zookeeper.zookeeper:2181 ZOOKEEPER_URL: zookeeper.zookeeper:2181 +api_config: + connection: + host: db.example.com + port: 5432 +database_config: + connection: + host: db.example.com + port: 5432 +rendered_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 +yaml_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/monitoring-origin/MONITORING/values/deployment-parameters.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/monitoring-origin/MONITORING/values/deployment-parameters.yaml index e900f09b6..32aa31035 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/monitoring-origin/MONITORING/values/deployment-parameters.yaml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/monitoring-origin/MONITORING/values/deployment-parameters.yaml @@ -74,13 +74,41 @@ TRACING_HOST: tracing-agent TRACING_UI_URL: https://cluster-01.qubership.org ZOOKEEPER_ADDRESS: zookeeper.zookeeper:2181 ZOOKEEPER_URL: zookeeper.zookeeper:2181 +api_config: &id001 + connection: + host: db.example.com + port: 5432 +api_port: 8080 +app_version: '3.0' bss-app-exist: false -core: &id001 +core: &id002 apps: volumes: outputs: capacity: 20Gi -global: &id002 +database_config: &id003 + connection: + host: db.example.com + port: 5432 +debug_mode_test: 'true' +log_level: 'true' +rendered_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 +server_port: 8080 +service_version: '3.0' +ssl_enabled: true +use_ssl: true +yaml_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 +global: &id004 API_DBAAS_ADDRESS: http://dbaas.dbaas:8080 APPLICATION_NAME: MONITORING ARTIFACT_DESCRIPTOR_ARTIFACT_ID: prod.platform.system.monitoring_monitoring-operator @@ -157,50 +185,72 @@ global: &id002 TRACING_UI_URL: https://cluster-01.qubership.org ZOOKEEPER_ADDRESS: zookeeper.zookeeper:2181 ZOOKEEPER_URL: zookeeper.zookeeper:2181 + api_config: *id001 + api_port: 8080 + app_version: '3.0' bss-app-exist: false - core: *id001 -alertmanager: *id002 -blackbox-exporter: *id002 -cert-exporter: *id002 -cloud-events-exporter: *id002 -cloud-events-reader: *id002 -cloudwatch-exporter: *id002 -common-dashboards: *id002 -configmap-reload: *id002 -configurations-streamer: *id002 -grafana: *id002 -grafana-image-renderer: *id002 -grafana-operator: *id002 -grafana-plugins-init: *id002 -graphite-remote-adapter: *id002 -json-exporter: *id002 -kube-rbac-proxy: *id002 -kube-state-metrics: *id002 -monitoring-operator: *id002 -network-latency-exporter: *id002 -node-exporter: *id002 -oauth2-proxy: *id002 -platform_monitoring_tests: *id002 -prometheus: *id002 -prometheus-adapter: *id002 -prometheus-adapter-converter: *id002 -prometheus-adapter-operator: *id002 -prometheus-config-reloader: *id002 -prometheus-operator: *id002 -promitor-agent-resource-discovery: *id002 -promitor-agent-scraper: *id002 -promxy: *id002 -pushgateway: *id002 -stackdriver-exporter: *id002 -version-exporter: *id002 -victoriametrics-operator: *id002 -vmagent: *id002 -vmalert: *id002 -vmauth: *id002 -vmcleanup: *id002 -vminsert: *id002 -vmoperator: *id002 -vmoperator_config_reloader: *id002 -vmselect: *id002 -vmsingle: *id002 -vmstorage: *id002 + core: *id002 + database_config: *id003 + debug_mode_test: 'true' + log_level: 'true' + rendered_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 + server_port: 8080 + service_version: '3.0' + ssl_enabled: true + use_ssl: true + yaml_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 +alertmanager: *id004 +blackbox-exporter: *id004 +cert-exporter: *id004 +cloud-events-exporter: *id004 +cloud-events-reader: *id004 +cloudwatch-exporter: *id004 +common-dashboards: *id004 +configmap-reload: *id004 +configurations-streamer: *id004 +grafana: *id004 +grafana-image-renderer: *id004 +grafana-operator: *id004 +grafana-plugins-init: *id004 +graphite-remote-adapter: *id004 +json-exporter: *id004 +kube-rbac-proxy: *id004 +kube-state-metrics: *id004 +monitoring-operator: *id004 +network-latency-exporter: *id004 +node-exporter: *id004 +oauth2-proxy: *id004 +platform_monitoring_tests: *id004 +prometheus: *id004 +prometheus-adapter: *id004 +prometheus-adapter-converter: *id004 +prometheus-adapter-operator: *id004 +prometheus-config-reloader: *id004 +prometheus-operator: *id004 +promitor-agent-resource-discovery: *id004 +promitor-agent-scraper: *id004 +promxy: *id004 +pushgateway: *id004 +stackdriver-exporter: *id004 +version-exporter: *id004 +victoriametrics-operator: *id004 +vmagent: *id004 +vmalert: *id004 +vmauth: *id004 +vmcleanup: *id004 +vminsert: *id004 +vmoperator: *id004 +vmoperator_config_reloader: *id004 +vmselect: *id004 +vmsingle: *id004 +vmstorage: *id004 diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/pg/postgres/values/deployment-parameters.yaml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/pg/postgres/values/deployment-parameters.yaml index 006a49a9e..a58486edd 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/pg/postgres/values/deployment-parameters.yaml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/effective-set/deployment/pg/postgres/values/deployment-parameters.yaml @@ -69,7 +69,27 @@ TRACING_HOST: tracing-agent TRACING_UI_URL: https://cluster-01.qubership.org ZOOKEEPER_ADDRESS: zookeeper.zookeeper:2181 ZOOKEEPER_URL: zookeeper.zookeeper:2181 -global: &id001 +api_config: &id001 + connection: + host: db.example.com + port: 5432 +database_config: &id002 + connection: + host: db.example.com + port: 5432 +rendered_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 +yaml_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 +global: &id003 API_DBAAS_ADDRESS: http://dbaas.dbaas:8080 APPLICATION_NAME: postgres ARTIFACT_DESCRIPTOR_ARTIFACT_ID: prod.platform.ha.postgres @@ -141,10 +161,24 @@ global: &id001 TRACING_UI_URL: https://cluster-01.qubership.org ZOOKEEPER_ADDRESS: zookeeper.zookeeper:2181 ZOOKEEPER_URL: zookeeper.zookeeper:2181 -patroni-core: *id001 -pg_patroni: *id001 -pg_upgrade: *id001 -pgbackrest_sidecar: *id001 -postgres_operator_init: *id001 -postgres_operator_tests: *id001 -vault_env: *id001 + api_config: *id001 + database_config: *id002 + rendered_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 + yaml_template: |- + services: + api: + image: api:latest + ports: + - 8080:8080 +patroni-core: *id003 +pg_patroni: *id003 +pg_upgrade: *id003 +pgbackrest_sidecar: *id003 +postgres_operator_init: *id003 +postgres_operator_tests: *id003 +vault_env: *id003 diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/tenant.yml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/tenant.yml index c526990a0..d267792c6 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/tenant.yml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/tenant.yml @@ -15,4 +15,16 @@ globalE2EParameters: mergeTenantsAndE2EParameters: false environmentParameters: {} deployParameters: - ESCAPE_SEQUENCE: "true" \ No newline at end of file + ESCAPE_SEQUENCE: "true" + api_config: ${database_config} + database_config: + connection: + host: db.example.com + port: 5432 + yaml_template: | + services: + api: + image: api:latest + ports: + - 8080:8080 + rendered_template: ${yaml_template} From ff6f47c61aa0760ee725baed42d9d89162cea960 Mon Sep 17 00:00:00 2001 From: dysmon <120464230+dysmon@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:57:43 +0500 Subject: [PATCH 145/161] fix: git commit job optimization (#1238) --- build_envgene/scripts/git_commit.sh | 4 ++++ build_pipegene/scripts/env_build_jobs.py | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/build_envgene/scripts/git_commit.sh b/build_envgene/scripts/git_commit.sh index e5db60920..8234e82a8 100755 --- a/build_envgene/scripts/git_commit.sh +++ b/build_envgene/scripts/git_commit.sh @@ -1,6 +1,8 @@ #!/bin/bash set -e +echo "===== SCRIPT START: $(date '+%H:%M:%S') =====" + retries=0 exit_code=0 @@ -331,4 +333,6 @@ if [ "$exit_code" -ne 0 ]; then fi fi +echo "===== SCRIPT END: $(date '+%H:%M:%S') =====" + exit $exit_code diff --git a/build_pipegene/scripts/env_build_jobs.py b/build_pipegene/scripts/env_build_jobs.py index 5a0274abf..1db80647d 100644 --- a/build_pipegene/scripts/env_build_jobs.py +++ b/build_pipegene/scripts/env_build_jobs.py @@ -64,7 +64,6 @@ def prepare_git_commit_job(pipeline, full_env, enviroment_name, cluster_name, de "export env_name=$(echo $ENV_NAME | awk -F '/' '{print $NF}')", 'env_path=$(sudo find $CI_PROJECT_DIR/environments -type d -name "$env_name")', 'for path in $env_path; do if [ -d "$path/Credentials" ]; then sudo chmod ugo+rw $path/Credentials/*; fi; done', - 'cp -rf $CI_PROJECT_DIR/environments $CI_PROJECT_DIR/git_envs', ], } @@ -82,7 +81,6 @@ def prepare_git_commit_job(pipeline, full_env, enviroment_name, cluster_name, de } git_commit_job = job_instance(params=git_commit_params, vars=git_commit_vars) git_commit_job.artifacts.add_paths("${CI_PROJECT_DIR}/environments/" + f"{full_env}") - git_commit_job.artifacts.add_paths("${CI_PROJECT_DIR}/git_envs") git_commit_job.artifacts.add_paths('${CI_PROJECT_DIR}/sboms') git_commit_job.artifacts.when = WhenStatement.ALWAYS if (credential_rotation_job is not None): From 0dedd586eea308f1025204ea24c7d552ec2d0e3e Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Thu, 16 Apr 2026 15:00:28 +0000 Subject: [PATCH 146/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index deb0e70e5..083891818 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -75,9 +75,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.32.0" - DOCKER_IMAGE_TAG_ENVGENE: "1.32.0" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.32.0" + DOCKER_IMAGE_TAG_PIPEGENE: "1.32.1" + DOCKER_IMAGE_TAG_ENVGENE: "1.32.1" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.32.1" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index f16dbae84..5179be479 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.32.0 +version: 1.32.1 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 9933a8655..ad02a7c05 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.32.0 +version: 1.32.1 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 529a9eebd..707d522b8 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.32.0", + "envgene_version": "1.32.1", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 152eb10d242014a087478e576ba43b032adb8369 Mon Sep 17 00:00:00 2001 From: dysmon <120464230+dysmon@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:29:01 +0500 Subject: [PATCH 147/161] fix: shared creds (#1240) * fix: update shared creds * fix: update shared creds * fix: add log' * fix: add additional dir * fix: make search case sensitive --- python/envgene/envgenehelper/yaml_helper.py | 23 +++++++++++++++------ scripts/build_env/create_credentials.py | 21 ++++++++++++------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/python/envgene/envgenehelper/yaml_helper.py b/python/envgene/envgenehelper/yaml_helper.py index 01147e15e..345abb5f5 100644 --- a/python/envgene/envgenehelper/yaml_helper.py +++ b/python/envgene/envgenehelper/yaml_helper.py @@ -294,12 +294,23 @@ def beautifyYaml(file_path, schema_path="", header_text="", allign_comments=Fals alignYamlFileComments(file_path) -def find_yaml_file(dir_path: Path, search_name: str) -> Path | None: - for ext in (".yml", ".yaml"): - f = dir_path / f"{search_name}{ext}" - if f.is_file(): - logger.info(f"Found {search_name} in: {f}") - return f +def find_yaml_file(dir_path: Path, search_name: str, recursively: bool = False) -> Path | None: + + if not dir_path.exists(): + return None + + if recursively: + for root, _, files in os.walk(dir_path): + for f in files: + if f.endswith((".yml", ".yaml")): + if Path(f).stem == search_name: + return Path(root) / f + else: + for entry in os.scandir(dir_path): + if entry.is_file() and entry.name.endswith((".yml", ".yaml")): + if Path(entry.name).stem == search_name: + return Path(entry.path) + return None diff --git a/scripts/build_env/create_credentials.py b/scripts/build_env/create_credentials.py index 3bf986810..441e2a550 100644 --- a/scripts/build_env/create_credentials.py +++ b/scripts/build_env/create_credentials.py @@ -158,18 +158,23 @@ def mergeAndSaveYaml(yamlPath, newCreds) : logger.info("%s credentials created" % count) writeYamlToFile(yamlPath, credsYaml) + def findSharedCredentials(cred_name, env_dir, instances_dir) -> Path: - env_level = Path(env_dir) / "Inventory" / "credentials" - cluster_level = Path(env_dir).parent / "credentials" - site_level = Path(instances_dir) / "credentials" - - shared_cred_paths = [env_level, cluster_level, site_level] + levels = [ + Path(env_dir) / "Inventory", + Path(env_dir).parent, + Path(instances_dir), + ] - logger.debug(f"Searching for '{cred_name}' in paths: {shared_cred_paths}") + cred_dir_names = ["credentials", "Credentials", "shared-credentials"] + + shared_cred_paths = [level / name for level in levels for name in cred_dir_names] + for p in shared_cred_paths: - found_path = find_yaml_file(p, cred_name) + found_path = find_yaml_file(p, cred_name, recursively=True) if found_path: - return found_path + logger.info(f"Shared credentials with key '{cred_name}' found in '{found_path}'") + return found_path raise FileNotFoundError(f"Shared credentials with key '{cred_name}' not found.") From 2951b0e65c982952a813c43fcdf1b7065d90adb6 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Tue, 21 Apr 2026 13:32:04 +0000 Subject: [PATCH 148/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 083891818..3c977adbf 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -75,9 +75,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.32.1" - DOCKER_IMAGE_TAG_ENVGENE: "1.32.1" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.32.1" + DOCKER_IMAGE_TAG_PIPEGENE: "1.32.2" + DOCKER_IMAGE_TAG_ENVGENE: "1.32.2" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.32.2" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 5179be479..ec2fa5efe 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.32.1 +version: 1.32.2 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index ad02a7c05..80c2f048c 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.32.1 +version: 1.32.2 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 707d522b8..0552d892a 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.32.1", + "envgene_version": "1.32.2", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 954be890203a2fdda0664e7bbf540d7f5aecfb09 Mon Sep 17 00:00:00 2001 From: ismglvd-hub Date: Thu, 23 Apr 2026 12:42:11 +0500 Subject: [PATCH 149/161] docs: add use cases for DevCI (#1241) * docs: add Effective Set use cases * docs: add extra US * docs: add use-cases * docs: add Effective Set use cases * docs: add extra US * docs: add use-cases * docs: add SSL sert use-cases * docs: update ssl use cases * docs: update GSF Use Cases * docs: remove UC 2,3,4 * docs: add cred rotation UC * docs: add UC-SC-NEX-1 to Environment Template Artifact Download and remove system-certificate UC * docs: added auto-environment-name use cases * docs: fixed links * docs: fix markdown system-certificate use case * docs: fixed ssl use cases --- docs/use-cases/artifact-downloading.md | 34 ++ docs/use-cases/auto-environment-name.md | 206 ++++++++ docs/use-cases/credential-rotation.md | 441 ++++++++++++++++++ .../environment-instance-generation.md | 121 +++++ docs/use-cases/gsf-repository-maintenance.md | 258 ++++++++++ docs/use-cases/system-certificate.md | 3 + docs/use-cases/template-inheritance.md | 210 +++++++++ 7 files changed, 1273 insertions(+) create mode 100644 docs/use-cases/auto-environment-name.md create mode 100644 docs/use-cases/credential-rotation.md create mode 100644 docs/use-cases/gsf-repository-maintenance.md create mode 100644 docs/use-cases/system-certificate.md create mode 100644 docs/use-cases/template-inheritance.md diff --git a/docs/use-cases/artifact-downloading.md b/docs/use-cases/artifact-downloading.md index 5a30adf78..2c2121baf 100644 --- a/docs/use-cases/artifact-downloading.md +++ b/docs/use-cases/artifact-downloading.md @@ -21,6 +21,7 @@ - [UC-AD-ENV-9: Download Template from Artifactory with GAV notation](#uc-ad-env-9-download-template-from-artifactory-with-gav-notation) - [UC-AD-ENV-10: Download Template from Artifactory with GAV notation and Anonymous Access](#uc-ad-env-10-download-template-from-artifactory-with-gav-notation-and-anonymous-access) - [UC-AD-ENV-11: Download Template from Nexus with GAV notation](#uc-ad-env-11-download-template-from-nexus-with-gav-notation) + - [UC-SC-NEX-1: Download template artifact from Nexus with custom CA certificate](#uc-sc-nex-1-download-template-artifact-from-nexus-with-custom-ca-certificate) - [UC-AD-ENV-12: Download Template from Nexus with GAV notation and Anonymous Access](#uc-ad-env-12-download-template-from-nexus-with-gav-notation-and-anonymous-access) - [UC-AD-ENV-13: Download Template with app ver notation from Artifactory (ArtDef v1)](#uc-ad-env-13-download-template-with-app-ver-notation-from-artifactory-artdef-v1) - [UC-AD-ENV-14: Download Template with app ver notation from Artifactory and Anonymous Access (ArtDef v1)](#uc-ad-env-14-download-template-with-app-ver-notation-from-artifactory-and-anonymous-access-artdef-v1) @@ -593,6 +594,39 @@ Instance pipeline (GitLab or GitHub) is started with parameters: 1. Template artifact is downloaded successfully 2. Template is available for Environment Instance generation +### UC-SC-NEX-1: Download template artifact from Nexus with custom CA certificate + +**Pre-requisites:** + +1. Template artifact is uploaded to Nexus and available for download. +2. Environment Inventory exists and specifies template with GAV notation. +3. `registry.yml` exists with Nexus configuration and credentials. +4. Nexus endpoint uses certificate chain signed by private or internal CA. +5. Instance repository contains CA certificate chain file in `configuration/certs/`. + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: ` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `app_reg_def_process` job runs in the pipeline: + 1. Reads Environment Inventory. + 2. Parses GAV coordinates from `templateArtifact` section. + 3. Resolves registry from `registry.yml`. + 4. Loads CA certificates from `configuration/certs/` into runner trust. + 5. Authenticates using credentials. + 6. Connects to Nexus over TLS and downloads template artifact. + +**Results:** + +1. TLS connection to Nexus is established successfully. +2. Template artifact is downloaded successfully. +3. No `CERTIFICATE_VERIFY_FAILED` or trust errors appear in logs. + ### UC-AD-ENV-12: Download Template from Nexus with GAV notation and Anonymous Access **Pre-requisites:** diff --git a/docs/use-cases/auto-environment-name.md b/docs/use-cases/auto-environment-name.md new file mode 100644 index 000000000..63d88d355 --- /dev/null +++ b/docs/use-cases/auto-environment-name.md @@ -0,0 +1,206 @@ +# Automatic Environment Name Derivation Use Cases + +- [Automatic Environment Name Derivation Use Cases](#automatic-environment-name-derivation-use-cases) + - [Overview](#overview) + - [Environment Name Derivation Scenarios](#environment-name-derivation-scenarios) + - [UC-AEN-END-1: Environment with no explicit environmentName defined](#uc-aen-end-1-environment-with-no-explicit-environmentname-defined) + - [UC-AEN-END-2: Environment with explicit environmentName defined](#uc-aen-end-2-environment-with-explicit-environmentname-defined) + - [UC-AEN-END-3: Environment with explicit environmentName different from folder name](#uc-aen-end-3-environment-with-explicit-environmentname-different-from-folder-name) + - [UC-AEN-END-4: Invalid folder structure for environment](#uc-aen-end-4-invalid-folder-structure-for-environment) + - [UC-AEN-END-5: Template rendering with derived environment name](#uc-aen-end-5-template-rendering-with-derived-environment-name) + +## Overview + +This document describes use cases for automatic environment name derivation. + +Feature reference: [Automatic Environment Name Derivation](/docs/features/auto-env-name-derivation.md). + +## Environment Name Derivation Scenarios + +### UC-AEN-END-1: Environment with no explicit environmentName defined + +**Pre-requisites:** + +1. Environment definition file exists at path `/environments///Inventory/env_definition.yml`. +2. The `environmentName` attribute is not defined in `env_definition.yml`: + + ```yaml + inventory: + # environmentName is not defined + tenantName: "Applications" + cloudName: "cluster01" + envTemplate: + name: "simple" + artifact: "project-env-template:master_20231024-080204" + ``` + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: cluster01/env01` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `env_build` job runs in the pipeline. +2. EnvGene reads environment path from `ENV_NAMES`. +3. EnvGene loads `env_definition.yml` and determines environment name. + +**Results:** + +1. The environment is successfully created. +2. The environment name is derived from the folder name `env01`. +3. The environment context contains `current_env.name` with the value `env01`. +4. The generated environment instance references the correct environment name. + +### UC-AEN-END-2: Environment with explicit environmentName defined + +**Pre-requisites:** + +1. Environment definition file exists at path `/environments///Inventory/env_definition.yml`. +2. The `environmentName` attribute is explicitly defined in `env_definition.yml`: + + ```yaml + inventory: + environmentName: "env02" + tenantName: "Applications" + cloudName: "cluster01" + envTemplate: + name: "simple" + artifact: "project-env-template:master_20231024-080204" + ``` + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: cluster01/env02` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `env_build` job runs in the pipeline. +2. EnvGene reads environment path from `ENV_NAMES`. +3. EnvGene loads `env_definition.yml` and uses explicit `environmentName`. + +**Results:** + +1. The environment is successfully created. +2. The explicitly defined environment name `env02` is used. +3. The environment context contains `current_env.name` with the value `env02`. +4. The generated environment instance references the correct environment name. + +### UC-AEN-END-3: Environment with explicit environmentName different from folder name + +**Pre-requisites:** + +1. Environment definition file exists at path `/environments///Inventory/env_definition.yml`. +2. The `environmentName` attribute is explicitly defined with a value different from folder name: + + ```yaml + inventory: + environmentName: "custom-env" + tenantName: "Applications" + cloudName: "cluster01" + envTemplate: + name: "simple" + artifact: "project-env-template:master_20231024-080204" + ``` + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: cluster01/env03` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `env_build` job runs in the pipeline. +2. EnvGene reads environment path from `ENV_NAMES`. +3. EnvGene loads `env_definition.yml` and uses explicit `environmentName`. + +**Results:** + +1. The environment is successfully created. +2. The explicitly defined environment name `custom-env` is used instead of folder name. +3. The environment context contains `current_env.name` with the value `custom-env`. +4. The generated environment instance references the correct environment name. + +### UC-AEN-END-4: Invalid folder structure for environment + +**Pre-requisites:** + +1. Environment definition file exists at invalid path that does not follow expected structure. +2. The `environmentName` attribute is not defined in `env_definition.yml`: + + ```yaml + inventory: + # environmentName is not defined + tenantName: "Applications" + cloudName: "cluster01" + envTemplate: + name: "simple" + artifact: "project-env-template:master_20231024-080204" + ``` + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: invalid-structure` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `env_build` job runs in the pipeline. +2. EnvGene attempts to determine environment name from path. +3. EnvGene detects invalid folder structure. + +**Results:** + +1. Environment creation fails with an appropriate error message. +2. The error message indicates that environment name could not be determined from path. +3. The error message suggests checking folder structure. + +### UC-AEN-END-5: Template rendering with derived environment name + +**Pre-requisites:** + +1. Environment definition file exists at path `/environments///Inventory/env_definition.yml`. +2. The `environmentName` attribute is not defined in `env_definition.yml`. +3. Environment template contains references to `current_env.name`: + + ```yaml + # cloud.yml.j2 + name: "{{ current_env.name }}-cloud" + description: "Cloud for {{ current_env.name }} environment" + ``` + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: cluster01/env04` +2. `ENV_BUILDER: true` + +**Steps:** + +1. The `env_build` job runs in the pipeline. +2. EnvGene derives environment name from folder path. +3. EnvGene renders template with `current_env.name`. + +**Results:** + +1. The environment is successfully created. +2. The environment name is derived from folder name `env04`. +3. The template is rendered with derived environment name: + + ```yaml + # Rendered cloud.yml + name: "env04-cloud" + description: "Cloud for env04 environment" + ``` + +4. All template variables referencing `current_env.name` are substituted with derived name. diff --git a/docs/use-cases/credential-rotation.md b/docs/use-cases/credential-rotation.md new file mode 100644 index 000000000..29a2f487e --- /dev/null +++ b/docs/use-cases/credential-rotation.md @@ -0,0 +1,441 @@ +# Credential Rotation Use Cases + +- [Credential Rotation Use Cases](#credential-rotation-use-cases) + - [Overview](#overview) + - [UC-CR-TPR-1: Update Credential from Pipeline Parameter](#uc-cr-tpr-1-update-credential-from-pipeline-parameter) + - [UC-CR-TPR-2: Update Credential from Deployment Parameter](#uc-cr-tpr-2-update-credential-from-deployment-parameter) + - [UC-CR-TPR-3: Update Credentials from Multiple rotation_items](#uc-cr-tpr-3-update-credentials-from-multiple-rotation_items) + - [Affected Credential Handling](#affected-credential-handling) + - [UC-CR-LCH-1: Reject Affected Credential Update](#uc-cr-lch-1-reject-affected-credential-update) + - [UC-CR-LCH-2: Update Affected Credentials in Force Mode](#uc-cr-lch-2-update-affected-credentials-in-force-mode) + - [UC-CR-VAL-1: Fail When No Affected Parameters Found](#uc-cr-val-1-fail-when-no-affected-parameters-found) + - [Encryption Processing](#encryption-processing) + - [Successful Update with Encryption Enabled](#successful-update-with-encryption-enabled) + - [UC-CR-ENC-1: Update Credentials with Plaintext Payload when Encryption Is Enabled](#uc-cr-enc-1-update-credentials-with-plaintext-payload-when-encryption-is-enabled) + - [UC-CR-ENC-2: Update Credentials with Encrypted Payload when Encryption Is Enabled](#uc-cr-enc-2-update-credentials-with-encrypted-payload-when-encryption-is-enabled) + - [Successful Update with Encryption Disabled](#successful-update-with-encryption-disabled) + +## Overview + +This document contains use cases for [Credential Rotation](/docs/features/cred-rotation.md). + +It describes parameter targeting, affected-credentials handling with force mode, and encryption processing for `credential_rotation`. + +### Successful Update without Affected Credentials + +This group covers successful rotation when the target credential is not linked to other parameters. + +### UC-CR-TPR-1: Update Credential from Pipeline Parameter + +**Pre-requisites:** + +1. `env_inventory_generation_job` must be launched in the pipeline run. +2. Environment Instance contains a Namespace with `name` matching ``. +3. The Namespace contains a sensitive parameter in `e2eParameters` linked via the `cred` macro. +4. The referenced Credential exists in the Environment Credentials file or in a Shared Credentials file. +5. No other parameters reference the same `cred-id` and credential field. +6. `CRED_ROTATION_PAYLOAD` contains a valid `rotation_items` entry for Namespace-level pipeline context: + + ```json + { + "rotation_items": [ + { + "namespace": "", + "context": "pipeline", + "parameter_key": "", + "parameter_value": "" + } + ] + } + ``` + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: /` +2. `CRED_ROTATION_PAYLOAD: ` +3. `CRED_ROTATION_FORCE: false` + +**Steps:** + +1. The `credential_rotation` job runs in the pipeline: + 1. Finds the target Namespace from the payload. + 2. Uses the `pipeline` context to look for the parameter in `e2eParameters`. + 3. Determines which credential field is linked to the target parameter. + 4. Searches for affected credentials linked to the same `cred-id` and credential field. + 5. Finds no affected credentials and continues the flow. + 6. Updates credential value for the target parameter. + +**Results:** + +1. The credential value is updated successfully. +2. The job completes with success status. + +### UC-CR-TPR-2: Update Credential from Deployment Parameter + +**Pre-requisites:** + +1. `env_inventory_generation_job` must be launched in the pipeline run. +2. Environment Instance contains a Namespace with `name` matching ``. +3. That Namespace contains an Application with `name` matching ``. +4. The Application contains a sensitive parameter in `deployParameters`. +5. The parameter is linked via the `cred` macro to an existing credential. +6. No other parameters reference the same `cred-id` and credential field. +7. `CRED_ROTATION_PAYLOAD` contains a valid `rotation_items` entry for Application-level deployment context: + + ```json + { + "rotation_items": [ + { + "namespace": "", + "application": "", + "context": "deployment", + "parameter_key": "db.connection.password", + "parameter_value": "" + } + ] + } + ``` + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: /` +2. `CRED_ROTATION_PAYLOAD: ` +3. `CRED_ROTATION_FORCE: false` + +**Steps:** + +1. The `credential_rotation` job runs in the pipeline: + 1. Finds the target Namespace and the Application specified in the payload. + 2. Uses the `deployment` context to look for the parameter in `deployParameters`. + 3. Determines which credential field is linked to the target parameter. + 4. Searches for affected credentials linked to the same `cred-id` and credential field. + 5. Finds no affected credentials and continues the flow. + 6. Updates credential value for the target parameter. + +**Results:** + +1. The credential value is updated successfully. +2. The job completes with success status. + +### UC-CR-TPR-3: Update Credentials from Multiple rotation_items + +**Pre-requisites:** + +1. `env_inventory_generation_job` must be launched in the pipeline run. +2. Environment Instance contains all target Namespace and Application objects referenced by the payload. +3. The payload contains multiple `rotation_items`. +4. The payload includes items from different supported contexts: + + - `pipeline` + - `deployment` + - `runtime` + +5. Each payload item references an existing sensitive parameter linked via the `cred` macro. +6. For each payload item, the target credential is not linked to other parameters. +7. Any payload item with `context: pipeline` does not specify `application`, because Application-level pipeline rotation is rejected by the current implementation. + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: /` +2. `CRED_ROTATION_PAYLOAD: ` +3. `CRED_ROTATION_FORCE: false` + +**Steps:** + +1. The `credential_rotation` job runs in the pipeline: + 1. Reads all `rotation_items` from `CRED_ROTATION_PAYLOAD`. + 2. Processes payload items one by one in the order they are provided. + 3. For each item, chooses the parameter section according to the requested context: + 1. `pipeline` uses `e2eParameters` + 2. `deployment` uses `deployParameters` + 3. `runtime` uses `technicalConfigurationParameters` + 4. For each payload item, searches for affected credentials linked to the same `cred-id` and credential field. + 5. Finds no affected credentials for the successful path and continues processing. + 6. Updates credential values for all valid payload items. + 7. Stops the whole job if any payload item is invalid or cannot be processed. + +**Results:** + +1. All target credential values are updated successfully. +2. The job completes with success status. + +## Affected Credential Handling + +This section covers scenarios where the target parameter shares the same credential reference with other sensitive parameters in the same or different environments. This is the main currently implemented execution path. + +### Affected Credentials with Non-Force Mode + +This group covers scenarios where dependencies are found and `CRED_ROTATION_FORCE=false`. In these cases, the job fails and generates affected parameters artifact. + +### UC-CR-LCH-1: Reject Affected Credential Update + +**Pre-requisites:** + +1. `env_inventory_generation_job` must be launched in the pipeline run. +2. The target sensitive parameter exists and is linked via the `cred` macro. +3. One or more additional sensitive parameters reference the same `cred-id` and the same credential field (`username`, `password`, or `secret`). +4. The linked parameters may be located in: + + - The same Environment Credentials file + - One or more Shared Credentials files + - Other affected Environment Instances + +5. `CRED_ROTATION_PAYLOAD` contains a valid rotation request. +6. `CRED_ROTATION_FORCE` is not provided or is explicitly set to `false`. + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: /` +2. `CRED_ROTATION_PAYLOAD: ` +3. `CRED_ROTATION_FORCE: false` + +**Steps:** + +1. The `credential_rotation` job runs in the pipeline: + 1. Resolves the target parameter and the credential field linked to it. + 2. Finds all other parameters affected by the same credential change. + 3. Builds the full affected parameters report for the request. + 4. Checks `CRED_ROTATION_FORCE` and sees that force mode is disabled. + 5. Generates `affected-sensitive-parameters.yaml` artifact and finishes the job with error status without writing credential changes. + +**Results:** + +1. The `credential_rotation` job fails with a readable error message explaining that affected parameters exist. +2. No credential values are changed. +3. Repository state remains unchanged for all credential files involved in the request. +4. The `affected-sensitive-parameters.yaml` artifact is generated and lists all detected affected parameters. + +### UC-CR-LCH-2: Update Affected Credentials in Force Mode + +**Pre-requisites:** + +1. `env_inventory_generation_job` must be launched in the pipeline run. +2. The target sensitive parameter exists and is linked via the `cred` macro. +3. One or more additional sensitive parameters reference the same `cred-id` and the same credential field. +4. The linked parameters span at least one of the following storage locations: + + - Environment Credentials file of the target Environment + - Shared Credentials file + - Environment Credentials file of another affected Environment + +5. `CRED_ROTATION_PAYLOAD` contains a valid rotation request. +6. `CRED_ROTATION_FORCE` is set to `true`. + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: /` +2. `CRED_ROTATION_PAYLOAD: ` +3. `CRED_ROTATION_FORCE: true` + +**Steps:** + +1. The `credential_rotation` job runs in the pipeline: + 1. Resolves the target parameter and the credential field linked to it. + 2. Finds all other parameters affected by the same credential change. + 3. Builds the full affected parameters report for the request. + 4. Checks `CRED_ROTATION_FORCE` and allows the rotation to continue. + 5. Updates all matched credential files that contain the affected credential. + +**Results:** + +1. The target credential field is updated to the new value. +2. All linked sensitive parameters now reference the rotated value through the shared credential linkage. +3. The `credential_rotation` job completes successfully. +4. The `affected-sensitive-parameters.yaml` artifact is generated. + +### UC-CR-VAL-1: Fail When No Affected Parameters Found + +**Pre-requisites:** + +1. `env_inventory_generation_job` must be launched in the pipeline run. +2. `CRED_ROTATION_PAYLOAD` contains one or more valid `rotation_items`. +3. Each payload item points to an existing sensitive parameter linked via the `cred` macro. +4. None of the payload items has other affected parameters in the current implementation search scope. +5. `CRED_ROTATION_FORCE` is `true`. + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: /` +2. `CRED_ROTATION_PAYLOAD: ` +3. `CRED_ROTATION_FORCE: true` + +**Steps:** + +1. The `credential_rotation` job runs in the pipeline: + 1. Processes all payload items from `CRED_ROTATION_PAYLOAD`. + 2. Tries to collect affected parameters for each payload item. + 3. Finishes payload processing without collecting any affected parameters. + 4. The job finishes with error status. + +**Results:** + +1. The `credential_rotation` job fails. +2. No credential files are updated. +3. `affected-sensitive-parameters.yaml` is not created. + +## Encryption Processing + +This section covers credential rotation behavior that is confirmed by the current implementation. + +### Successful Update with Encryption Enabled + +This group covers successful scenarios when encryption is enabled in `config.yml`. + +### UC-CR-ENC-1: Update Credentials with Plaintext Payload when Encryption Is Enabled + +**Pre-requisites:** + +1. `env_inventory_generation_job` must be launched in the pipeline run. +2. The configuration file `/configuration/config.yml` contains `crypt: true`. +3. The target sensitive parameter exists and is linked via the `cred` macro. +4. `CRED_ROTATION_PAYLOAD` contains plaintext JSON in string form. +5. `CRED_ROTATION_FORCE=true`. +6. One or more additional sensitive parameters reference the same `cred-id` and the same credential field. + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: /` +2. `CRED_ROTATION_PAYLOAD: ` +3. `CRED_ROTATION_FORCE: true` + +**Steps:** + +1. The `credential_rotation` job runs in the pipeline: + 1. Reads the payload in plaintext form. + 2. Loads credential files for the Environment and decrypts them. + 3. Searches for affected credentials linked to the same `cred-id` and credential field. + 4. Finds no affected credentials for the successful path and continues processing. + 5. Applies the requested credential changes to the matched files. + 6. Re-encrypts updated credential files before finishing the job. + +**Results:** + +1. The plaintext payload is processed successfully. +2. Credential values are updated according to payload input. +3. Updated credential files remain encrypted in the repository. +4. The `credential_rotation` job completes with success status. + +### UC-CR-ENC-2: Update Credentials with Encrypted Payload when Encryption Is Enabled + +**Pre-requisites:** + +1. `env_inventory_generation_job` must be launched in the pipeline run. +2. The configuration file `/configuration/config.yml` contains `crypt: true`, so encryption mode is enabled. +3. The target sensitive parameter exists and is linked via the `cred` macro. +4. `CRED_ROTATION_PAYLOAD` is passed in encrypted form and can be decrypted by EnvGene. +5. `CRED_ROTATION_FORCE=true`. +6. One or more additional sensitive parameters reference the same `cred-id` and the same credential field. + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: /` +2. `CRED_ROTATION_PAYLOAD: ` +3. `CRED_ROTATION_FORCE: true` + +**Steps:** + +1. The `credential_rotation` job runs in the pipeline: + 1. Decrypts the payload for processing. + 2. Loads credential files for the Environment and decrypts them. + 3. Searches for affected credentials linked to the same `cred-id` and credential field. + 4. Finds no affected credentials for the successful path and continues processing. + 5. Applies the requested credential changes to the matched files. + 6. Re-encrypts updated credential files before finishing the job. + +**Results:** + +1. The encrypted payload is processed successfully. +2. Credential values are updated according to payload input. +3. Updated credential files remain encrypted in the repository. +4. The `credential_rotation` job completes with success status. + +### Successful Update with Encryption Disabled + +This group covers successful scenarios when encryption is disabled in `config.yml`. + +### UC-CR-ENC-3: Update Credentials with Plaintext Payload when Encryption Is Disabled + +**Pre-requisites:** + +1. `env_inventory_generation_job` must be launched in the pipeline run. +2. The configuration file `/configuration/config.yml` contains `crypt: false`. +3. The target sensitive parameter exists and is linked via the `cred` macro. +4. `CRED_ROTATION_PAYLOAD` contains plaintext JSON in string form. +5. `CRED_ROTATION_FORCE=true`. +6. One or more additional sensitive parameters reference the same `cred-id` and the same credential field. + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: /` +2. `CRED_ROTATION_PAYLOAD: ` +3. `CRED_ROTATION_FORCE: true` + +**Steps:** + +1. The `credential_rotation` job runs in the pipeline: + 1. Reads the payload in plaintext form. + 2. Loads credential files for the Environment without repository decryption. + 3. Searches for affected credentials linked to the same `cred-id` and credential field. + 4. Finds no affected credentials for the successful path and continues processing. + 5. Applies the requested credential changes to the matched files. + 6. Finishes without credential file re-encryption. + +**Results:** + +1. The plaintext payload is processed successfully. +2. Credential values are updated according to payload input. +3. The `credential_rotation` job completes with success status. + +### UC-CR-ENC-4: Update Credentials with Encrypted Payload when Encryption Is Disabled + +**Pre-requisites:** + +1. `env_inventory_generation_job` must be launched in the pipeline run. +2. The EnvGene configuration file `/configuration/config.yml` contains `crypt: false`, so encryption mode is disabled. +3. The target sensitive parameter exists and is linked via the `cred` macro. +4. `CRED_ROTATION_PAYLOAD` is passed in encrypted form and can be decrypted by EnvGene. + +5. `CRED_ROTATION_FORCE=true`. +6. One or more additional sensitive parameters reference the same `cred-id` and the same credential field. +7. The rotation request is otherwise valid. + +**Trigger:** + +Instance pipeline (GitLab or GitHub) is started with parameters: + +1. `ENV_NAMES: /` +2. `CRED_ROTATION_PAYLOAD: ` + +**Steps:** + +1. The `credential_rotation` job runs in the pipeline: + 1. Reads the payload and decrypts it for further processing. + 2. Loads credential files for the Environment without repository decryption. + 3. Searches for affected credentials linked to the same `cred-id` and credential field. + 4. Finds no affected credentials for the successful path and continues processing. + 5. Applies the requested credential changes to the matched files. + 6. Finishes without credential file re-encryption. + +**Results:** + +1. The encrypted payload is processed successfully. +2. Credential values are updated according to payload input. +3. The `credential_rotation` job completes with success status. diff --git a/docs/use-cases/environment-instance-generation.md b/docs/use-cases/environment-instance-generation.md index f70a039a8..506adae3e 100644 --- a/docs/use-cases/environment-instance-generation.md +++ b/docs/use-cases/environment-instance-generation.md @@ -15,6 +15,11 @@ - [UC-EIG-TA-1: Environment Instance Generation with `artifact` only](#uc-eig-ta-1-environment-instance-generation-with-artifact-only) - [UC-EIG-TA-2: Environment Instance Generation with `artifact` and `bgNsArtifacts` and BG Domain](#uc-eig-ta-2-environment-instance-generation-with-artifact-and-bgnsartifacts-and-bg-domain) - [UC-EIG-TA-3: Environment Instance Generation with `artifact` and `bgNsArtifacts` and without BG Domain](#uc-eig-ta-3-environment-instance-generation-with-artifact-and-bgnsartifacts-and-without-bg-domain) + - [Effective Set Generation in Instance Pipeline](#effective-set-generation-in-instance-pipeline) + - [UC-EIG-ES-1: Generate Effective Set without `SD_DATA` or `SD_VERSION`](#uc-eig-es-1-generate-effective-set-without-sd_data-or-sd_version) + - [UC-EIG-ES-2: Generate Effective Set with `SD_DATA` or `SD_VERSION`](#uc-eig-es-2-generate-effective-set-with-sd_data-or-sd_version) + - [UC-EIG-ES-3: Apply `CUSTOM_PARAMS` when `GENERATE_EFFECTIVE_SET` is true](#uc-eig-es-3-apply-custom_params-when-generate_effective_set-is-true) + - [UC-EIG-ES-4: Ignore `CUSTOM_PARAMS` when `GENERATE_EFFECTIVE_SET` is false](#uc-eig-es-4-ignore-custom_params-when-generate_effective_set-is-false) - [Multiple Environments Processing](#multiple-environments-processing) - [UC-EIG-ME-1: Parallel Environment Instance Generation for Multiple Environments](#uc-eig-me-1-parallel-environment-instance-generation-for-multiple-environments) @@ -450,6 +455,122 @@ Instance pipeline (GitLab or GitHub) is started with parameters: 2. All other objects (Tenant, Cloud, Applications, etc.) are rendered using `project-env-template:v1.2.3` 3. `bgNsArtifacts` are ignored since BG Domain is absent +## Effective Set Generation in Instance Pipeline + +This section describes Effective Set generation scenarios executed from the Instance pipeline. + +### UC-EIG-ES-1: Generate Effective Set without `SD_DATA` or `SD_VERSION` + +**Pre-requisites:** + +1. Environment Inventory exists and can be processed by the Instance pipeline. +2. Pipeline parameters include: + 1. `ENV_BUILDER: true` + 2. `GENERATE_EFFECTIVE_SET: true` + 3. `SD_DATA` is empty or not set + 4. `SD_VERSION` is empty or not set + +**Trigger:** + +Instance build pipeline with Effective Set enabled is started for one or more environments. + +**Steps:** + +1. The `env_builder` job runs and generates Environment Instance objects. +2. The `process_sd` job is skipped because neither `SD_DATA` nor `SD_VERSION` is provided. +3. The `generate_effective_set` job runs without SD input parameters. + +**Results:** + +1. `env_builder` job finishes successfully. +2. `generate_effective_set` job finishes successfully. +3. Generated Effective Set does not include data merged from SD input. + +### UC-EIG-ES-2: Generate Effective Set with `SD_DATA` or `SD_VERSION` + +**Pre-requisites:** + +1. Environment Inventory exists and can be processed by the Instance pipeline. +2. Pipeline parameters include: + 1. `ENV_BUILDER: true` + 2. `GENERATE_EFFECTIVE_SET: true` + 3. At least one SD input is set: + - `SD_DATA`, or + - `SD_VERSION` + +**Trigger:** + +Instance build pipeline with Effective Set enabled is started for one or more environments. + +**Steps:** + +1. The `env_builder` job runs and generates Environment Instance objects. +2. The `process_sd` job runs when SD input matches `SD_SOURCE_TYPE`: + 1. `SD_SOURCE_TYPE=json` and `SD_DATA` is provided, or + 2. `SD_SOURCE_TYPE=artifact` and `SD_VERSION` is provided. +3. The `generate_effective_set` job runs with processed SD input. + +**Results:** + +1. `env_builder` job finishes successfully. +2. `process_sd` job runs and finishes successfully. +3. `generate_effective_set` job finishes successfully. +4. Generated Effective Set includes data resolved from provided SD input. + +### UC-EIG-ES-3: Apply `CUSTOM_PARAMS` when `GENERATE_EFFECTIVE_SET` is true + +**Pre-requisites:** + +1. Environment Inventory exists and can be processed by the Instance pipeline. +2. Pipeline parameters include: + 1. `ENV_BUILDER: true` + 2. `GENERATE_EFFECTIVE_SET: true` + 3. `CUSTOM_PARAMS` contains one or more key-value pairs + +**Trigger:** + +Instance build pipeline with Effective Set enabled is started for one or more environments. + +**Steps:** + +1. The `env_builder` job runs and generates Environment Instance objects. +2. The `generate_effective_set` job runs. +3. The `generate_effective_set` job applies values from `CUSTOM_PARAMS`. + +**Results:** + +1. `env_builder` job finishes successfully. +2. `generate_effective_set` job finishes successfully. +3. Values from `CUSTOM_PARAMS` are applied in generated Effective Set according to merge rules. + +### UC-EIG-ES-4: Ignore `CUSTOM_PARAMS` when `GENERATE_EFFECTIVE_SET` is false + +**Pre-requisites:** + +1. Environment Inventory exists and can be processed by the Instance pipeline. +2. Pipeline parameters include: + 1. `ENV_BUILDER: true` + 2. `GENERATE_EFFECTIVE_SET: false` + 3. `CUSTOM_PARAMS` contains one or more key-value pairs + +**Trigger:** + +Instance build pipeline is started for one or more environments with: + +1. `GENERATE_EFFECTIVE_SET: false` + +**Steps:** + +1. The `env_builder` job runs and generates Environment Instance objects. +2. The `generate_effective_set` job is skipped because `GENERATE_EFFECTIVE_SET` is false. + +**Results:** + +1. `env_builder` job finishes successfully. +2. `generate_effective_set` job is not created. +3. Environment Instance generation completes without Effective Set output. +4. `CUSTOM_PARAMS` are ignored. + ## Multiple Environments Processing When multiple environments are specified in `ENV_NAMES`, the pipeline processes them in parallel. Each environment triggers an independent pipeline flow with the same set of pipeline parameters. This section describes how Environment Instance generation works for multiple environments. diff --git a/docs/use-cases/gsf-repository-maintenance.md b/docs/use-cases/gsf-repository-maintenance.md new file mode 100644 index 000000000..763356218 --- /dev/null +++ b/docs/use-cases/gsf-repository-maintenance.md @@ -0,0 +1,258 @@ +# GSF Repository Maintenance Use Cases + +- [GSF Repository Maintenance Use Cases](#gsf-repository-maintenance-use-cases) + - [Overview](#overview) + - [Template Repository Maintenance via GSF](#template-repository-maintenance-via-gsf) + - [UC-GSF-TMP-1: Initialize Template Repository via GSF](#uc-gsf-tmp-1-initialize-template-repository-via-gsf) + - [UC-GSF-TMP-2: Upgrade Template Repository via GSF](#uc-gsf-tmp-2-upgrade-template-repository-via-gsf) + - [UC-GSF-TMP-3: Downgrade Template Repository via GSF](#uc-gsf-tmp-3-downgrade-template-repository-via-gsf) + - [Instance Repository Maintenance via GSF](#instance-repository-maintenance-via-gsf) + - [UC-GSF-INST-1: Initialize Instance Repository via GSF](#uc-gsf-inst-1-initialize-instance-repository-via-gsf) + - [UC-GSF-INST-2: Upgrade Instance Repository via GSF](#uc-gsf-inst-2-upgrade-instance-repository-via-gsf) + - [UC-GSF-INST-3: Downgrade Instance Repository via GSF](#uc-gsf-inst-3-downgrade-instance-repository-via-gsf) + +## Overview + +This document defines use cases for maintaining EnvGene Template and Instance repositories with the Git-System-Follower (GSF) package manager. + +For each repository type, it covers three maintenance scenarios: + +- Initial installation (init) +- Upgrade to a new EnvGene package version +- Downgrade to an older EnvGene package version + +For every scenario, repository contents after GSF execution are validated against a reference structure (golden state) to confirm that managed files are correctly added, updated, or removed. + +For detailed installation and maintenance steps, see: + +- Template Repository: [Maintain Template Repository via GSF] +- Instance Repository: [Environment Instance Repository Installation Guide](/docs/how-to/envgene-maitanance.md) + +## Template Repository Maintenance via GSF + +### UC-GSF-TMP-1: Initialize Template Repository via GSF + +**Pre-requisites:** + +1. A new Git repository for the Environment Template exists in the project Git group and does not yet contain EnvGene-specific files. +2. GitLab technical user and access token with required permissions are available. +3. GSF package manager is installed and working on the local machine. +4. Template package image path for the desired EnvGene version is known. +5. A reference (target) Template Repository structure for this version is defined. + +**Trigger:** + +User runs GSF on the local machine to initialize the Template Repository: + +```bash +git-system-follower install \ + -r \ + -b \ + -t \ + --extra env_template_artifact_name no-masked +``` + +**Steps:** + +1. Run GSF with repository URL, branch, token, and package image. +2. GSF applies the selected package to the Template Repository. +3. GSF adds required files from the selected version and removes obsolete managed files (if any). + +**Results:** + +1. Template Repository is initialized. +2. Required files from the selected version are present. +3. Old managed files are removed or replaced. +4. Repository matches the reference structure. + +### UC-GSF-TMP-2: Upgrade Template Repository via GSF + +**Pre-requisites:** + +1. Template Repository already exists and contains a previous EnvGene template package version. +2. GitLab technical user, token, and required CI/CD variables are available. +3. GSF package manager is installed and working on the local machine. +4. Target EnvGene template package image path is known. +5. A reference Template Repository structure for the target EnvGene version is defined. + +**Trigger:** + +User runs GSF on the local machine to upgrade the Template Repository to a new EnvGene version: + +```bash +git-system-follower install \ + -r \ + -b \ + -t \ + --extra env_template_artifact_name no-masked +``` + +**Steps:** + +1. Run GSF with repository URL, branch, token, and target package image. +2. GSF updates the Template Repository to the target version. +3. GSF updates changed files, adds new files, and removes outdated managed files. +4. Verify restricted files for Template Repository: + - `pipeline_vars.yml` or `pipeline_vars.yaml` + - `build_vars.sh` + - `description_template.yml` or `description_template.yaml` +5. Verify restricted file behavior: + - `build_vars.sh` and `description_template.*` preserve user-defined values after upgrade + - `pipeline_vars.*` preserves user-defined values, except allowed structural alignment with current package structure + +**Results:** + +1. Template Repository is upgraded to the target version. +2. Repository matches the reference structure. +3. Restricted files are preserved according to policy: + - `build_vars.sh` and `description_template.*` are not replaced with package defaults + - `pipeline_vars.*` is preserved, with structural alignment allowed when required +4. No regressions related to repository upgrade are observed. + +### UC-GSF-TMP-3: Downgrade Template Repository via GSF + +**Pre-requisites:** + +1. Template Repository already exists and has a later version. +2. GitLab token and required variables are available. +3. GSF is installed on the local machine. +4. Path to an older template package image is known. +5. Reference structure for the older version is available. + +**Trigger:** + +User runs GSF on the local machine to install an older template package version: + +```bash +git-system-follower install \ + -r \ + -b \ + -t \ + --extra env_template_artifact_name no-masked +``` + +**Steps:** + +1. Run GSF with repository URL, branch, token, and older package image. +2. GSF applies the older package to the Template Repository. +3. GSF replaces current managed files with older-version files and removes extra files. + +**Results:** + +1. Template Repository is switched to the older version. +2. Required files for the older version are present. +3. Files from the later version are removed. +4. Repository matches the reference structure. + +## Instance Repository Maintenance via GSF + +### UC-GSF-INST-1: Initialize Instance Repository via GSF + +**Pre-requisites:** + +1. A new Git repository for the Environment Instance exists in the project Git group. +2. GitLab project access token with required scopes is available. +3. GSF package manager is installed and working on the local machine. +4. Instance package image path for the chosen EnvGene version is known. +5. A reference Instance Repository structure for this version is defined. + +**Trigger:** + +User runs GSF on the local machine to initialize the Instance Repository: + +```bash +git-system-follower install \ + -r \ + -b \ + -t +``` + +**Steps:** + +1. Run GSF with repository URL, branch, token, and package image. +2. GSF applies the selected package to the Instance Repository. +3. GSF creates the required CI/CD and configuration files for the selected version. + +**Results:** + +1. Instance Repository is initialized. +2. Required files from the selected version are present. +3. Repository matches the reference structure. + +### UC-GSF-INST-2: Upgrade Instance Repository via GSF + +**Pre-requisites:** + +1. Instance Repository already exists and contains a previous EnvGene instance package version. +2. GitLab token and required CI/CD variables are available. +3. GSF package manager is installed and working on the operator's machine. +4. Target EnvGene instance package image path is known. +5. A reference Instance Repository structure for the target EnvGene version is defined. + +**Trigger:** + +User runs GSF on the local machine to upgrade the Instance Repository: + +```bash +git-system-follower install \ + -r \ + -b \ + -t +``` + +**Steps:** + +1. Run GSF with repository URL, branch, token, and target package image. +2. GSF updates the Instance Repository to the target version. +3. GSF updates changed files, adds new files, and removes outdated managed files. +4. Verify `configuration/credentials/credentials.yml` contains `self-token-cred`: + - `type: secret` + - `data.secret` is present. +5. Verify `configuration/integration.yml` contains: + - `self_token: "${creds.get('self-token-cred').secret}"`. +6. Verify legacy `self_token` definition is absent in `configuration/config.yml` or ignored by the target version. +7. Verify placeholder file `configuration/.gitkeep` is present. + +**Results:** + +1. Instance Repository is upgraded to the target version. +2. Repository matches the reference structure. +3. Token configuration is migrated and valid: + - `configuration/credentials/credentials.yml` contains `self-token-cred` with non-empty secret data, + - `configuration/integration.yml` references `${creds.get('self-token-cred').secret}` in `self_token`, + - runtime execution does not fail with missing `self_token` or missing `self-token-cred`. +4. Legacy token definition in `configuration/config.yml` is absent or ignored by the target version, and does not affect runtime behavior. + +### UC-GSF-INST-3: Downgrade Instance Repository via GSF + +**Pre-requisites:** + +1. Instance Repository already exists and has a later version. +2. GitLab token and required variables are available. +3. GSF is installed on the local machine. +4. Path to an older instance package image is known. +5. Reference structure for the older version is available. + +**Trigger:** + +User runs GSF on the local machine to install an older instance package version: + +```bash +git-system-follower install \ + -r \ + -b \ + -t +``` + +**Steps:** + +1. Run GSF with repository URL, branch, token, and older package image. +2. GSF applies the older package to the Instance Repository. +3. GSF replaces current managed files with older-version files and removes extra files. + +**Results:** + +1. Instance Repository is switched to the older version. +2. Required files for the older version are present. +3. Files from the later version are removed. +4. Repository matches the reference structure. diff --git a/docs/use-cases/system-certificate.md b/docs/use-cases/system-certificate.md new file mode 100644 index 000000000..4aaff192c --- /dev/null +++ b/docs/use-cases/system-certificate.md @@ -0,0 +1,3 @@ +# SSL Certificate Processing Use Cases + +This document describes use cases for SSL certificate processing. diff --git a/docs/use-cases/template-inheritance.md b/docs/use-cases/template-inheritance.md new file mode 100644 index 000000000..24524d497 --- /dev/null +++ b/docs/use-cases/template-inheritance.md @@ -0,0 +1,210 @@ +# Template Inheritance (Template Composition) Use Cases + +- [Template Inheritance (Template Composition) Use Cases](#template-inheritance-template-composition-use-cases) + - [Overview](#overview) + - [Parent Templates Download and Selection](#parent-templates-download-and-selection) + - [UC-TI-PT-1: Build child template using a single parent template](#uc-ti-pt-1-build-child-template-using-a-single-parent-template) + - [UC-TI-PT-2: Build child template composed from multiple parent templates](#uc-ti-pt-2-build-child-template-composed-from-multiple-parent-templates) + - [Composite Structure Selection](#composite-structure-selection) + - [UC-TI-CS-1: Use explicit `composite_structure` from child Template Descriptor](#uc-ti-cs-1-use-explicit-composite_structure-from-child-template-descriptor) + - [Overrides in Child Template](#overrides-in-child-template) + - [UC-TI-OV-1: Override parent parameters for Cloud template](#uc-ti-ov-1-override-parent-parameters-for-cloud-template) + - [UC-TI-OV-2: Override parent parameters for Namespace template](#uc-ti-ov-2-override-parent-parameters-for-namespace-template) + +## Overview + +This document describes use cases for [Template Inheritance] +The child template can inherit data from one or more parent templates. +The child template can also override selected parent values. + +Important: + +- `overrides-parent` is part of Template Inheritance logic. +- `template_override` is a different feature and is described in [Template Override](/docs/features/template-override.md). + +## Parent Templates Download and Selection + +This section explains how `parent-templates` and `parent` links are resolved when the child template is built. + +### UC-TI-PT-1: Build child template using a single parent template + +**Pre-requisites:** + +1. A child Template Repository exists and is configured to build an Environment Template artifact. +2. The child repository contains a Template Descriptor with a single parent template reference: + + ```yaml + parent-templates: + basic-cloud: basic-product-template:10.1.3 + tenant: + parent: basic-cloud + cloud: + parent: basic-cloud + namespaces: + - name: "{env}-billing" + parent: basic-cloud + ``` + +**Trigger:** + +Template repository pipeline is started to build the child template artifact. + +**Steps:** + +1. The build pipeline starts Template Inheritance processing. +2. The pipeline resolves `tenant.parent`, `cloud.parent`, and `namespaces[].parent` using alias `basic-cloud` from `parent-templates`. +3. The pipeline publishes a regular EnvGene child template artifact. + +**Results:** + +1. Child template artifact is created successfully. +2. Inherited components are taken from `basic-cloud` according to descriptor references. +3. Parent templates are resolved from `parent-templates` entries (`application:version`). + +### UC-TI-PT-2: Build child template composed from multiple parent templates + +**Pre-requisites:** + +1. Child Template Repository contains a Template Descriptor that references multiple parent templates and composes namespaces from them: + + ```yaml + parent-templates: + basic-cloud: basic-product-template:10.1.3 + default-oss: core-product-templates:1.5.3 + default-billing: billing-product-templates:3.7.12 + default-bss: bss-product-templates:2.0.0 + tenant: + parent: basic-cloud + cloud: "{{ templates_dir }}/env_templates/composite/cloud.yml.j2" + composite_structure: "{{ templates_dir }}/env_templates/composite/composite_structure.yml.j2" + namespaces: + - name: "{env}-oss" + parent: default-oss + - name: "{env}-billing" + parent: default-billing + - name: "{env}-bss" + parent: default-bss + ``` + +2. All referenced parent template artifacts are available and accessible to the pipeline. + +**Trigger:** + +Template repository pipeline is started to build the child template artifact. + +**Steps:** + +1. The build pipeline starts Template Inheritance processing. +2. The pipeline resolves `tenant.parent` and each `namespaces[].parent` by alias from `parent-templates`. +3. `cloud` and `composite_structure` are taken from child template paths defined in descriptor. +4. The pipeline publishes a regular EnvGene child template artifact. + +**Results:** + +1. Child template artifact is created successfully. +2. Tenant and namespaces are composed from referenced parent templates. +3. Cloud and composite structure are taken from child repository sources. + +## Composite Structure Selection + +This section explains how explicit `composite_structure` is selected and used. + +### UC-TI-CS-1: Use explicit `composite_structure` from child Template Descriptor + +**Pre-requisites:** + +1. Child Template Descriptor specifies `composite_structure` explicitly: + + ```yaml + composite_structure: "{{ templates_dir }}/env_templates/composite/composite_structure.yml.j2" + ``` + +2. The referenced file exists in the child repository template sources. + +**Trigger:** + +Template repository pipeline is started to build the child template artifact. + +**Steps:** + +1. The build pipeline reads `composite_structure` path from child descriptor. +2. The path value is saved in built template artifact and later used during instance generation to render `composite_structure.yml`. + +**Results:** + +1. Built artifact contains the explicit child `composite_structure` reference. +2. User can verify generated instance contains `composite_structure.yml` rendered from that path. + +## Overrides in Child Template + +This section explains override logic in Template Inheritance. + +- Use `overrides-parent` when a child template inherits from a parent and changes selected fields. +- Do not mix this with `template_override`. `template_override` is applied in instance generation and is documented separately. + +### UC-TI-OV-1: Override parent parameters for Cloud template + +**Pre-requisites:** + +1. Child Template Descriptor defines `cloud.parent` and includes `cloud.overrides-parent` section: + + ```yaml + cloud: + parent: basic-template + overrides-parent: + deployParameters: + DEPLOY_PARAM1: "DEPLOY_PARAM1_VALUE" + e2eParameters: + E2E_PARAM1: "E2E_PARAM1_VALUE" + ``` + +2. The parent template `basic-template` exists and contains a Cloud template. + +**Trigger:** + +Template repository pipeline is started to build the child template artifact. + +**Steps:** + +1. The pipeline loads Cloud template from parent `basic-template`. +2. The pipeline applies `cloud.overrides-parent` over inherited Cloud values. +3. The pipeline publishes a child artifact with merged Cloud template. + +**Results:** + +1. Child Cloud template is inherited from parent and then updated by `cloud.overrides-parent`. +2. User can verify only supported override sections are used: `profile`, parameter maps, and parameter sets. + +### UC-TI-OV-2: Override parent parameters for Namespace template + +**Pre-requisites:** + +1. Child Template Descriptor defines a namespace with `parent` and includes `overrides-parent` section: + + ```yaml + namespaces: + - name: "{env}-bss" + parent: default-bss + overrides-parent: + technicalConfigurationParameters: + TECH_CONF_PARAM1: "TECH_CONF_PARAM1_VALUE" + deployParameterSets: + - "bss-overrides" + ``` + +2. The parent template `default-bss` exists and contains the referenced namespace template. + +**Trigger:** + +Template repository pipeline is started to build the child template artifact. + +**Steps:** + +1. The pipeline loads Namespace template from parent `default-bss`. +2. The pipeline applies `namespaces[].overrides-parent` over inherited Namespace values. +3. The pipeline publishes a child artifact with merged Namespace template. + +**Results:** + +1. Child Namespace template is inherited from parent and then updated by `namespaces[].overrides-parent`. +2. User can verify overridden parameter maps and parameter sets are present in the produced template. From b6fc58b28642111b337a3247612f976de012ad54 Mon Sep 17 00:00:00 2001 From: dysmon <120464230+dysmon@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:03:03 +0500 Subject: [PATCH 150/161] fix: binary parsing (#1242) --- .../cloud/devops/commons/utils/HelmNameNormalizer.java | 6 +++--- .../main/java/org/qubership/cloud/devops/cli/CmdbCli.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build_effective_set_generator/commons/src/main/java/org/qubership/cloud/devops/commons/utils/HelmNameNormalizer.java b/build_effective_set_generator/commons/src/main/java/org/qubership/cloud/devops/commons/utils/HelmNameNormalizer.java index 74daedc55..8046c1e58 100644 --- a/build_effective_set_generator/commons/src/main/java/org/qubership/cloud/devops/commons/utils/HelmNameNormalizer.java +++ b/build_effective_set_generator/commons/src/main/java/org/qubership/cloud/devops/commons/utils/HelmNameNormalizer.java @@ -44,7 +44,7 @@ private static String normalizeNameForHelm(String name, int limit) { } // Convert binary mask to decimal - int decimalMask = Integer.parseInt(mask.toString(), 2); + long decimalMask = Long.parseLong(mask.toString(), 2); // Encode decimal mask in base-36 using custom symbols List base36Digits = numberToBase(decimalMask, 36); @@ -62,14 +62,14 @@ private static String normalizeNameForHelm(String name, int limit) { return finalName; } - private static List numberToBase(int number, int base) { + private static List numberToBase(long number, int base) { List digits = new ArrayList<>(); if (number == 0) { digits.add(0); return digits; } while (number > 0) { - digits.add(0, number % base); + digits.add(0, (int)(number % base)); number /= base; } return digits; diff --git a/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/CmdbCli.java b/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/CmdbCli.java index feb8f9907..42b7dd5e8 100644 --- a/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/CmdbCli.java +++ b/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/CmdbCli.java @@ -72,7 +72,7 @@ public Integer call() { return 0; } catch (Exception e) { logError(String.format(EFFECTIVE_SET_FAILED, e.getMessage())); - logDebug(String.format("Stack trace: %s", ExceptionUtils.getStackTrace(e))); + logError(String.format("Stack trace: %s", ExceptionUtils.getStackTrace(e))); return 1; } } From 79e59ad6aa6423b5bbcd1ff254864847c15b8b58 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Mon, 27 Apr 2026 09:05:54 +0000 Subject: [PATCH 151/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 3c977adbf..f8c82a5f5 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -75,9 +75,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.32.2" - DOCKER_IMAGE_TAG_ENVGENE: "1.32.2" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.32.2" + DOCKER_IMAGE_TAG_PIPEGENE: "1.32.3" + DOCKER_IMAGE_TAG_ENVGENE: "1.32.3" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.32.3" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index ec2fa5efe..c9ee4d085 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.32.2 +version: 1.32.3 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 80c2f048c..21d755a30 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.32.2 +version: 1.32.3 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 0552d892a..c5e387616 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.32.2", + "envgene_version": "1.32.3", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 141822caeb32fad661ffe60189c7e32992f2616a Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Mon, 27 Apr 2026 18:13:27 +0300 Subject: [PATCH 152/161] fix: Updated the values in repo to pass grand report check (#1264) --- .qubership/grand-report.json | 24 +++++++++++++++++++ .../cloud-passport/cluster-01-creds.yml | 15 ------------ 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/.qubership/grand-report.json b/.qubership/grand-report.json index f216aab40..18b464c93 100644 --- a/.qubership/grand-report.json +++ b/.qubership/grand-report.json @@ -8,5 +8,29 @@ }, { "t-hash" : "244f28ce3685167745ad3a7f1760fd4483bbbb3fd150b9087b95442d4d6fd905", "f-hash" : "f81b4f0ec55a8c76b2006d0d8fb8cf55ba73f8f24db12cd9e737c06e50f8010c" + }, { + "t-hash" : "f454bc9163706b52dc68c37847db48967e15b2f18a80c1d85f4387e9970cd299", + "f-hash" : "4a097b2592a091a5e9c9898fb01b957287f62d282e37abdc3f64f5397ddf7790" + }, { + "t-hash" : "f454bc9163706b52dc68c37847db48967e15b2f18a80c1d85f4387e9970cd299", + "f-hash" : "1b81a1c8c26e93bbbc06c67a3aeebffaa9fb32e4adc12ea43c546bd8ba6b0db2" + }, { + "t-hash" : "f454bc9163706b52dc68c37847db48967e15b2f18a80c1d85f4387e9970cd299", + "f-hash" : "266ceec1c40da5d4664b1fd8b680fb11ecab4bc58c28ca4b83c95c2cfc87da42" + }, { + "t-hash" : "f454bc9163706b52dc68c37847db48967e15b2f18a80c1d85f4387e9970cd299", + "f-hash" : "12358bb89894a3b900c43b8fbaf976baaecfd1151976ade3a7528cdc47728c52" + }, { + "t-hash" : "f454bc9163706b52dc68c37847db48967e15b2f18a80c1d85f4387e9970cd299", + "f-hash" : "81364271645a7f08f26b6d896bc26221e14ae46b670d66a2990bd3b805c18da3" + }, { + "t-hash" : "f454bc9163706b52dc68c37847db48967e15b2f18a80c1d85f4387e9970cd299", + "f-hash" : "633b0a67c34213aeba550c218df84e75f1839993ec7fef1c8f6c4f9d924f507f" + }, { + "t-hash" : "f454bc9163706b52dc68c37847db48967e15b2f18a80c1d85f4387e9970cd299", + "f-hash" : "490876a57b0af0ebbb422281ec7b178483e8ea4462281533b2a34abbf3725222" + }, { + "t-hash" : "f454bc9163706b52dc68c37847db48967e15b2f18a80c1d85f4387e9970cd299", + "f-hash" : "fe68378b7ff1c467cce4d2579b704fcac1b1cd7bae254f7b5225586ca59233d0" } ] } diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/cloud-passport/cluster-01-creds.yml b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/cloud-passport/cluster-01-creds.yml index 175bb1c18..e4dee4f23 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/cloud-passport/cluster-01-creds.yml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/{{ cookiecutter.gsf_repository_name }}/example/environments/cluster-01/cloud-passport/cluster-01-creds.yml @@ -6,11 +6,6 @@ consul-admin-cred: type: "secret" data: secret: "token-placeholder-123" -cse-graylog-cred: - type: "usernamePassword" - data: - username: "user-placeholder-123" - password: "pass-placeholder-123" dbaas-cluster-dba-cred: type: "usernamePassword" data: @@ -22,16 +17,6 @@ maas-cred: username: "user-placeholder-123" password: "pass-placeholder-123" minio-storage-cred: - type: "usernamePassword" - data: - username: "user-placeholder-123" - password: "pass-placeholder-123" -oss-streaming-cred: - type: "usernamePassword" - data: - username: "user-placeholder-123" - password: "pass-placeholder-123" -ssm-cmdb-cred: type: "usernamePassword" data: username: "user-placeholder-123" From 790b5f0d5599321951155c806c3a036c4d72644b Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Mon, 27 Apr 2026 15:24:42 +0000 Subject: [PATCH 153/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index f8c82a5f5..f2de244e5 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -75,9 +75,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.32.3" - DOCKER_IMAGE_TAG_ENVGENE: "1.32.3" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.32.3" + DOCKER_IMAGE_TAG_PIPEGENE: "1.32.4" + DOCKER_IMAGE_TAG_ENVGENE: "1.32.4" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.32.4" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index c9ee4d085..b60490e61 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.32.3 +version: 1.32.4 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 21d755a30..222803f3b 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.32.3 +version: 1.32.4 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index c5e387616..58b33f2c2 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.32.3", + "envgene_version": "1.32.4", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 927c57c73ec913b908058a47e0b126865baeeb4f Mon Sep 17 00:00:00 2001 From: chethana-shastry-p Date: Tue, 28 Apr 2026 11:58:13 +0530 Subject: [PATCH 154/161] fix: Disable YAML anchors on excessive aliasing (#1263) --- .../effective-set-generator/pom.xml | 5 + .../implementation/FileDataConverterImpl.java | 17 ++- .../devops/cli/utils/yaml/AdaptiveYaml.java | 114 ++++++++++++++++++ .../cli/utils/yaml/AdaptiveYamlTest.java | 64 ++++++++++ .../parameter-calculator-bom/pom.xml | 2 +- 5 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/utils/yaml/AdaptiveYaml.java create mode 100644 build_effective_set_generator/effective-set-generator/src/test/java/org/qubership/cloud/devops/cli/utils/yaml/AdaptiveYamlTest.java diff --git a/build_effective_set_generator/effective-set-generator/pom.xml b/build_effective_set_generator/effective-set-generator/pom.xml index cf787ce87..83773a1bb 100644 --- a/build_effective_set_generator/effective-set-generator/pom.xml +++ b/build_effective_set_generator/effective-set-generator/pom.xml @@ -45,6 +45,11 @@ + + org.yaml + snakeyaml + 2.3 + io.quarkus quarkus-picocli diff --git a/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/repository/implementation/FileDataConverterImpl.java b/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/repository/implementation/FileDataConverterImpl.java index 214b92d46..06007809e 100644 --- a/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/repository/implementation/FileDataConverterImpl.java +++ b/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/repository/implementation/FileDataConverterImpl.java @@ -34,13 +34,14 @@ import org.yaml.snakeyaml.nodes.Node; import org.yaml.snakeyaml.nodes.Tag; import org.yaml.snakeyaml.representer.Representer; +import org.qubership.cloud.devops.cli.utils.yaml.AdaptiveYaml; import java.io.*; import java.util.Base64; import java.util.Map; import java.util.TreeMap; -import static org.qubership.cloud.devops.commons.utils.ConsoleLogger.logError; +import static org.qubership.cloud.devops.commons.utils.ConsoleLogger.*; @ApplicationScoped @@ -96,9 +97,16 @@ public T parseInputFile(TypeReference typeReference, File file) { @Override public void writeToFile(Map params, String... args) throws IOException { File file = fileSystemUtils.getFileFromGivenPath(args); + + boolean expand = params != null && !params.isEmpty() + && AdaptiveYaml.shouldExpand(params); + if (expand) { + logInfo("removing anchors and aliases for file: " + file.getAbsolutePath()); + } + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { if (params != null && !params.isEmpty()) { - getYamlObject().dump(params, writer); + getYamlObject(expand).dump(params, writer); } } } @@ -112,11 +120,14 @@ public Map getObjectMap(T inputObject) { } - private static Yaml getYamlObject() { + private static Yaml getYamlObject(boolean expand) { DumperOptions options = new DumperOptions(); options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); options.setDefaultScalarStyle(DumperOptions.ScalarStyle.PLAIN); options.setPrettyFlow(false); + if (expand) { + options.setDereferenceAliases(true); + } Representer representer = new Representer(options) { @Override protected Node representScalar(Tag tag, String value, DumperOptions.ScalarStyle style) { diff --git a/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/utils/yaml/AdaptiveYaml.java b/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/utils/yaml/AdaptiveYaml.java new file mode 100644 index 000000000..61ca36c56 --- /dev/null +++ b/build_effective_set_generator/effective-set-generator/src/main/java/org/qubership/cloud/devops/cli/utils/yaml/AdaptiveYaml.java @@ -0,0 +1,114 @@ +/* + * Copyright 2024-2025 NetCracker Technology Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.qubership.cloud.devops.cli.utils.yaml; + +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +public class AdaptiveYaml { + private static final int ALIAS_RATIO_RANGE_LOW = 400000; + private static final int ALIAS_RATIO_RANGE_HIGH = 4000000; + private static final double ALIAS_RATIO_RANGE = + (double) (ALIAS_RATIO_RANGE_HIGH - ALIAS_RATIO_RANGE_LOW); + + private static class Statistic { + int repeats = 0; + int complexity = 0; + } + + private static class Decoder { + // For more code details, check https://github.com/go-yaml/yaml/blob/v3/decode.go + + private final IdentityHashMap unique = new IdentityHashMap<>(); + private int decodeCount = 0; + private int aliasCount = 0; + + public int unmarshal(Object node) { + if (!(node instanceof Map || node instanceof List)) { + decodeCount++; + return 1; + } + + + if (unique.containsKey(node)) { + return alias(node); + } + + decodeCount++; + Statistic stat = new Statistic(); + stat.complexity = 1; + unique.put(node, stat); + + int childComplexity = 0; + + if (node instanceof Map map) { + for (Map.Entry e : map.entrySet()) { + childComplexity += unmarshal(e.getKey()); + childComplexity += unmarshal(e.getValue()); + } + } else if (node instanceof List list) { + for (Object item : list) { + childComplexity += unmarshal(item); + } + } + + stat.complexity += childComplexity; + + if (isLimitExceeded(aliasCount, decodeCount, node)) { + throw new RuntimeException("Excessive aliasing"); + } + + return stat.complexity; + } + + private int alias(Object node) { + Statistic stat = unique.get(node); + stat.repeats++; + decodeCount += stat.complexity; + aliasCount += stat.complexity; + return stat.complexity; + } + + private boolean isLimitExceeded(int alias, int decode, Object node ) { + double rt = allowedAliasRatio(decode); + double ad = ((double) alias / decode); + + return alias > 100 && + decode > 1000 && + ((double) alias / decode) > allowedAliasRatio(decode); + } + + private double allowedAliasRatio(int count) { + if (count <= ALIAS_RATIO_RANGE_LOW) return 0.99; + if (count >= ALIAS_RATIO_RANGE_HIGH) return 0.1; + + return 0.99 - 0.89 * ((double)(count - ALIAS_RATIO_RANGE_LOW) / ALIAS_RATIO_RANGE); + } + } + + public static boolean shouldExpand(Object data) { + Decoder decoder = new Decoder(); + try { + decoder.unmarshal(data); + } catch (RuntimeException e) { + return true; + } + return false; + } + +} diff --git a/build_effective_set_generator/effective-set-generator/src/test/java/org/qubership/cloud/devops/cli/utils/yaml/AdaptiveYamlTest.java b/build_effective_set_generator/effective-set-generator/src/test/java/org/qubership/cloud/devops/cli/utils/yaml/AdaptiveYamlTest.java new file mode 100644 index 000000000..69b19acb0 --- /dev/null +++ b/build_effective_set_generator/effective-set-generator/src/test/java/org/qubership/cloud/devops/cli/utils/yaml/AdaptiveYamlTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2024-2025 NetCracker Technology Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.qubership.cloud.devops.cli.utils.yaml; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AdaptiveYamlTest { + @Test + void testNoAliasing() { + Map root = new HashMap<>(); + root.put("a", Map.of("x", 1)); + root.put("b", Map.of("y", 2)); + + assertFalse(AdaptiveYaml.shouldExpand(root)); + } + + @Test + void testSimpleAlias() { + Map shared = new HashMap<>(); + shared.put("x", 1); + + Map root = new HashMap<>(); + root.put("a", shared); + root.put("b", shared); + + assertFalse(AdaptiveYaml.shouldExpand(root)); + } + + @Test + void testExcessiveAliasing() { + Map shared = new HashMap<>(); + shared.put("x", 1); + + List list = new ArrayList<>(); + + for (int i = 0; i < 2000; i++) { + list.add(shared); + } + + assertTrue(AdaptiveYaml.shouldExpand(list)); + } +} diff --git a/build_effective_set_generator/parameter-calculator-bom/pom.xml b/build_effective_set_generator/parameter-calculator-bom/pom.xml index 3c095505d..1e4f6b5bf 100644 --- a/build_effective_set_generator/parameter-calculator-bom/pom.xml +++ b/build_effective_set_generator/parameter-calculator-bom/pom.xml @@ -55,7 +55,7 @@ org.yaml snakeyaml - 2.2 + 2.3 org.eclipse.jgit From 50bd4331754f66a20c0e8114c2f1a5b61153d9f1 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Tue, 28 Apr 2026 06:36:19 +0000 Subject: [PATCH 155/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index f2de244e5..5a1aec7be 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -75,9 +75,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.32.4" - DOCKER_IMAGE_TAG_ENVGENE: "1.32.4" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.32.4" + DOCKER_IMAGE_TAG_PIPEGENE: "1.32.5" + DOCKER_IMAGE_TAG_ENVGENE: "1.32.5" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.32.5" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index b60490e61..4cfd27337 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.32.4 +version: 1.32.5 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index 222803f3b..c4e31240b 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.32.4 +version: 1.32.5 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 58b33f2c2..3fdf1e787 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.32.4", + "envgene_version": "1.32.5", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 376c213bb18ccdaf614393701d3b088ee4ea66a3 Mon Sep 17 00:00:00 2001 From: Nurlybek Kamelov <79522742+GlimmerCape@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:19:59 +0500 Subject: [PATCH 156/161] test: add tests in pipegene for custom params (#1223) --- .github/actions/run-tests/action.yml | 31 ++++++--- .gitignore | 1 + Makefile | 4 +- build_pipegene/scripts/test_gitlab_ci.py | 69 +++++++++++++------ devtools/tests/Dockerfile | 10 +-- devtools/tests/run.sh | 38 ++++++---- devtools/tests/up.sh | 2 + .../pipegene_ci_instance/GAV_coordinates.yaml | 6 ++ .../configuration/integration.yml | 6 ++ .../environments/cluster-01 | 1 + .../env_templates/composite-full.yml | 3 + 11 files changed, 122 insertions(+), 49 deletions(-) create mode 100644 test_data/pipegene_ci_instance/GAV_coordinates.yaml create mode 100644 test_data/pipegene_ci_instance/configuration/integration.yml create mode 120000 test_data/pipegene_ci_instance/environments/cluster-01 create mode 100644 test_data/pipegene_ci_instance/templates/env_templates/composite-full.yml diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index 21b873e36..4aa24289d 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -44,6 +44,11 @@ runs: mv sops-v3.9.0.linux.amd64 /usr/local/bin/sops chmod +x /usr/local/bin/sops + - name: Configure test environment + shell: bash + run: | + echo "PYTHONPATH=${GITHUB_WORKSPACE}" >> "${GITHUB_ENV}" + - name: ENVGENE HELPER test shell: bash run: | @@ -52,6 +57,14 @@ runs: cd ../../.. mv junit.xml junit_envgenehelper.xml + - name: PIPEGENE test + shell: bash + run: | + cd build_pipegene/scripts + pytest --capture=no -W ignore::DeprecationWarning --junitxml=../../junit.xml + cd ../.. + mv junit.xml junit_pipegene.xml + - name: ARTIFACT SEARCHER test shell: bash run: | @@ -88,10 +101,18 @@ runs: cd ../.. mv junit.xml junit_cred_rotation.xml + - name: SBOMS RETENTION POLICY test + shell: bash + run: | + cd build_effective_set_generator/scripts + pytest --capture=no -W ignore::DeprecationWarning --junitxml=../../junit.xml + cd ../.. + mv junit.xml junit_sbom_retention.xml + - name: Merge test results shell: bash run: | - junitparser merge junit_build_env.xml junit_envgenehelper.xml junit.xml + junitparser merge junit_*.xml junit.xml - name: Upload test results if: always() @@ -106,11 +127,3 @@ runs: with: name: test_artifact path: tmp/ - - - name: SBOMS RETENTION POLICY test - shell: bash - run: | - cd build_effective_set_generator/scripts - pytest --capture=no -W ignore::DeprecationWarning --junitxml=../../junit.xml - cd ../.. - mv junit.xml junit_build_env.xml diff --git a/.gitignore b/.gitignore index 82f61d8db..566f689ec 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ __pycache__ .vscode .DS_Store pyrightconfig.json +junit* diff --git a/Makefile b/Makefile index 4270b9941..e933632ff 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ build-%: up-%: $(compose) up -d $* - @if [ -f devtools/$*/up.sh ]; then $(compose) exec $* sh /workspace/devtools/$*/up.sh; fi + @if [ -f devtools/$*/up.sh ]; then $(compose) exec $* bash /workspace/devtools/$*/up.sh; fi bash-%: $(compose) exec $* bash @@ -20,7 +20,7 @@ rm-%: $(compose) rm $* run-%: - @if [ -f devtools/$*/run.sh ]; then $(compose) exec $* sh /workspace/devtools/$*/run.sh; \ + @if [ -f devtools/$*/run.sh ]; then $(compose) exec $* bash /workspace/devtools/$*/run.sh; \ else echo "No run script for $*"; fi edit: diff --git a/build_pipegene/scripts/test_gitlab_ci.py b/build_pipegene/scripts/test_gitlab_ci.py index 7ce2301d4..28b551c97 100644 --- a/build_pipegene/scripts/test_gitlab_ci.py +++ b/build_pipegene/scripts/test_gitlab_ci.py @@ -1,12 +1,23 @@ -import pytest -from main import perform_generation -from envgenehelper import getAbsPath, openYaml, dump_as_yaml_format import os from dataclasses import dataclass, asdict +from pathlib import Path + +import pytest + +# validations / gitlab_ci capture CI_PROJECT_DIR at import time; set it before loading main. +_REPO_ROOT = Path(__file__).resolve().parents[2] +os.environ["CI_PROJECT_DIR"] = str(_REPO_ROOT / "test_data" / "pipegene_ci_instance") +os.environ["CI_JOB_NAME"] = 'JOB_NAME_PLACEHOLDER' +os.environ["CI_COMMIT_REF_SLUG"] = 'PLACEHOLDER' +os.environ.setdefault("JSON_SCHEMAS_DIR", str(_REPO_ROOT / "schemas")) + +from main import perform_generation +from envgenehelper import openYaml, dump_as_yaml_format + @dataclass class PipelineVars: - env_names: str = "sample-cloud-name/composite-full" + env_names: str = "cluster-01/env-01" env_template_version: str = "new-version:app_def" get_passport: str = "true" env_builder: str = "true" @@ -19,34 +30,53 @@ class PipelineVars: sd_version: str = "" env_template_name: str = "" env_specific_params: str = "" + custom_params: str = "" -def convert_keys_to_uppercase(dictionary): - return {k.upper(): v for k, v in dictionary} +def convert_keys_to_uppercase(pairs): + return {k.upper(): v for k, v in pairs} build_pipeline_test_data = [ - ( # with all jobs + ( PipelineVars(env_specific_params='{"params": "value"}'), - ["trigger", "process_passport", "env_inventory_generation", "env_builder", "generate_effective_set", "git_commit", "cmdb_import" ] + [ + "trigger", + "process_passport", + "env_inventory_generation", + "app_reg_def_render", + "env_builder", + "generate_effective_set", + "git_commit", + ], ), - ( # new version template test + ( PipelineVars(env_template_test="true", env_inventory_init="true"), - ["trigger", "process_passport", "env_builder", "generate_effective_set", "cmdb_import" ] + [ + "trigger", + "process_passport", + "app_reg_def_render", + "env_builder", + "generate_effective_set", + ], ), - ( # wihtout passport discovery + ( PipelineVars(get_passport="false"), - ["env_builder", "generate_effective_set", "git_commit", "cmdb_import" ] + ["app_reg_def_render", "env_builder", "generate_effective_set", "git_commit"], ), - ( # effective set only + ( PipelineVars(get_passport="false", env_builder="false", cmdb_import="false"), ["generate_effective_set", "git_commit"] ), - ( # without passport and effective set + ( PipelineVars(get_passport="false", generate_effective_set="false"), - ["env_builder", "git_commit", "cmdb_import" ] + ["app_reg_def_render", "env_builder", "git_commit"], ), - ( # with inventory generation and without env_build, passport discovery and effective set - PipelineVars(get_passport="false", env_builder="false", generate_effective_set="false", sd_data='{"params": "value"}'), - ["env_inventory_generation", "git_commit", "cmdb_import" ] + ( + PipelineVars(get_passport="false", custom_params='{"params": "value"}'), + ["app_reg_def_render", "env_builder", "generate_effective_set", "git_commit"], + ), + ( + PipelineVars(get_passport="false", generate_effective_set="false", custom_params='{"params": "value"}'), + ["app_reg_def_render", "env_builder", "git_commit"], ), ] @@ -56,9 +86,6 @@ def change_test_dir(request, monkeypatch): @pytest.mark.parametrize("pipeline_vars, expected_sequence", build_pipeline_test_data) def test_build_pipeline(pipeline_vars, expected_sequence): - os.environ["CI_PROJECT_DIR"] = getAbsPath("samples") - os.environ["JSON_SCHEMAS_DIR"] = getAbsPath("schemas") - ci_commit_ref_name = "feature/test-generate" os.environ["CI_COMMIT_REF_NAME"] = ci_commit_ref_name pipeline_vars = asdict(pipeline_vars, dict_factory=convert_keys_to_uppercase) diff --git a/devtools/tests/Dockerfile b/devtools/tests/Dockerfile index f9c98e30c..c71e3e1d9 100644 --- a/devtools/tests/Dockerfile +++ b/devtools/tests/Dockerfile @@ -16,7 +16,9 @@ RUN curl -LO https://github.com/getsops/sops/releases/download/v3.9.0/sops-v3.9. COPY dependencies/pip.conf /etc/pip.conf COPY dependencies/sources.list /etc/apt/sources.list -# Copy dependencies and install them -COPY dependencies/tests_requirements.txt /tmp/ -RUN pip install --no-cache-dir "uv>=0.9.5" && uv pip install --system --no-cache-dir -r /tmp/tests_requirements.txt - +# PyPI test dependencies only (repo packages are installed editable at container start via up.sh) +COPY dependencies/tests_requirements.txt /tmp/tests_requirements.txt +# hadolint ignore=DL3013,DL3042 +RUN pip install --no-cache-dir "uv>=0.9.5" && \ + uv pip install --system --upgrade pip "setuptools<82" wheel && \ + uv pip install --system --no-cache-dir -r /tmp/tests_requirements.txt diff --git a/devtools/tests/run.sh b/devtools/tests/run.sh index 5a12db405..e936e4720 100755 --- a/devtools/tests/run.sh +++ b/devtools/tests/run.sh @@ -1,18 +1,30 @@ #!/bin/bash -set -euxo +set -euxo pipefail -cd "$CI_PROJECT_DIR" +cd "${CI_PROJECT_DIR}" -# Run tests -cd python/envgene/envgenehelper -pytest --capture=no -W ignore::DeprecationWarning --junitxml=../../../junit.xml -cd ../../.. -mv junit.xml junit_envgenehelper.xml +export PYTHONPATH=${CI_PROJECT_DIR} +export FULL_ENV_NAME="sdp-dev/env-1" +export BG_STATE="" -cd scripts/build_env -pytest --capture=no -W ignore::DeprecationWarning --junitxml=../../junit.xml -cd ../.. -mv junit.xml junit_build_env.xml +rm -f junit.xml junit_*.xml -# Merge results -python -m junitparser merge junit_build_env.xml junit_envgenehelper.xml junit.xml +run_pytest_suite() { + local name="$1" + local dir="$2" + ( + cd "${CI_PROJECT_DIR}/${dir}" + pytest --capture=no -W ignore::DeprecationWarning --junitxml="${CI_PROJECT_DIR}/junit.xml" + ) + mv "${CI_PROJECT_DIR}/junit.xml" "${CI_PROJECT_DIR}/junit_${name}.xml" +} + +run_pytest_suite envgenehelper python/envgene/envgenehelper +run_pytest_suite pipegene build_pipegene/scripts +run_pytest_suite artifact_searcher python/artifact-searcher/artifact_searcher +run_pytest_suite bg_manage scripts/bg_manage +run_pytest_suite build_env scripts/build_env +run_pytest_suite cred_rotation creds_rotation/scripts +run_pytest_suite sbom_retention build_effective_set_generator/scripts + +junitparser merge junit_*.xml junit.xml diff --git a/devtools/tests/up.sh b/devtools/tests/up.sh index f57ba0a22..d80946ada 100755 --- a/devtools/tests/up.sh +++ b/devtools/tests/up.sh @@ -1,3 +1,5 @@ #!/bin/bash +set -euo pipefail + chmod +x /workspace/python/build_modules.sh /workspace/python/build_modules.sh diff --git a/test_data/pipegene_ci_instance/GAV_coordinates.yaml b/test_data/pipegene_ci_instance/GAV_coordinates.yaml new file mode 100644 index 000000000..8ba62720e --- /dev/null +++ b/test_data/pipegene_ci_instance/GAV_coordinates.yaml @@ -0,0 +1,6 @@ +--- +# Template-test fixture for pipegene (see pipeline_helper.get_gav_coordinates_from_build). +artifact: + group_id: org.qubership.test + artifact_id: app_def + version: new-version diff --git a/test_data/pipegene_ci_instance/configuration/integration.yml b/test_data/pipegene_ci_instance/configuration/integration.yml new file mode 100644 index 000000000..c82a4034d --- /dev/null +++ b/test_data/pipegene_ci_instance/configuration/integration.yml @@ -0,0 +1,6 @@ +cp_discovery: + gitlab: + project: "value" + branch: master + token: somevalue.secret +self_token: somevalue.secret diff --git a/test_data/pipegene_ci_instance/environments/cluster-01 b/test_data/pipegene_ci_instance/environments/cluster-01 new file mode 120000 index 000000000..ec4338dde --- /dev/null +++ b/test_data/pipegene_ci_instance/environments/cluster-01 @@ -0,0 +1 @@ +../../test_environments/cluster-01 \ No newline at end of file diff --git a/test_data/pipegene_ci_instance/templates/env_templates/composite-full.yml b/test_data/pipegene_ci_instance/templates/env_templates/composite-full.yml new file mode 100644 index 000000000..1b64520ae --- /dev/null +++ b/test_data/pipegene_ci_instance/templates/env_templates/composite-full.yml @@ -0,0 +1,3 @@ +--- +# Placeholder for ENV_TEMPLATE_TEST pipeline generation tests. +placeholder: true From 61c9b0aa4a8be4c3130ccf98937a982f5b49c2eb Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:59:58 +0300 Subject: [PATCH 157/161] docs: Added how-to for git hooks related to CyberFerret (#1270) --- .qubership/grand-report.json | 3 + docs/how-to/global-pre-commit-hooks.md | 109 +++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 docs/how-to/global-pre-commit-hooks.md diff --git a/.qubership/grand-report.json b/.qubership/grand-report.json index 18b464c93..9da304c8c 100644 --- a/.qubership/grand-report.json +++ b/.qubership/grand-report.json @@ -1,5 +1,8 @@ { "exclusions" : [ { + "t-hash" : "00000000", + "f-hash" : "519368c1daa626a147f2a8f67e9ceec568378a9b094fd3f6ecd625faa2a1d564" + }, { "t-hash" : "823c4eb3e895adc925a755d89cea1c6c46954c999d23604e0091788b75496159", "f-hash" : "680348e409a5a7a1ccae4a38eea6315f78eab8d683e599d12bf03bc1405b1a75" }, { diff --git a/docs/how-to/global-pre-commit-hooks.md b/docs/how-to/global-pre-commit-hooks.md new file mode 100644 index 000000000..64f949314 --- /dev/null +++ b/docs/how-to/global-pre-commit-hooks.md @@ -0,0 +1,109 @@ +# Global pre-commit hooks + +- [Global pre-commit hooks](#global-pre-commit-hooks) + - [Description](#description) + - [Prerequisites](#prerequisites) + - [Step 1: Clone pre-commit-global](#step-1-clone-pre-commit-global) + - [Step 2: Point Git at the global hooks directory](#step-2-point-git-at-the-global-hooks-directory) + - [Step 3: Configure repositories you work on](#step-3-configure-repositories-you-work-on) + - [What runs on commit](#what-runs-on-commit) + - [Disable global hooks](#disable-global-hooks) + - [References](#references) + +## Description + +This guide shows how to register [pre-commit-global](https://github.com/exadmin/pre-commit-global) hooks globally on your machine so every Git repository can use shared hook logic before your normal pre-commit runs. Hook scripts live in this repository under `hooks-global/`. Full install details and upstream updates are documented in the [pre-commit-global readme](https://github.com/exadmin/pre-commit-global). + +## Prerequisites + +| Requirement | Purpose | +|-----------------------------|------------------------------------------------------------------------------------------------------------| +| Git | Required. See [git-scm.com](https://git-scm.com/install/). | +| Java (JDK or JRE) | Required by the hook toolchain (upstream tests with a recent JDK). | +| `CYBER_FERRET_PASSWORD` | Only if CyberFerret runs; see the note below. | + +> [!NOTE] +> For the `CYBER_FERRET_PASSWORD` value or questions about it, contact **Andrei Rudchenko**. + +## Step 1: Clone pre-commit-global + +Choose a directory you intend to keep (for example `~/tools/global-git-hooks` on Linux or macOS, or `C:\Tools\global-git-hooks` on Windows). Later, if you move or delete this folder you must repeat [Step 2](#step-2-point-git-at-the-global-hooks-directory). + +**Clone into the directory root:** + +```bash +mkdir -p ~/tools/global-git-hooks +cd ~/tools/global-git-hooks +git clone https://github.com/exadmin/pre-commit-global . +``` + +**Or clone into a named subfolder:** + +```bash +mkdir -p ~/tools +cd ~/tools +git clone https://github.com/exadmin/pre-commit-global my-global-hooks +cd my-global-hooks +``` + +On Windows Command Prompt, `mkdir`, `cd` to your chosen folder, run the same `git clone`, then `cd` into the clone. + +Stay in this clone directory when running the commands in the next step. + +## Step 2: Point Git at the global hooks directory + +Configure `core.hooksPath` to the `hooks-global` directory inside your clone. + +**Linux and macOS:** + +```bash +git config --global core.hooksPath "$(pwd)/hooks-global" +git config --global core.hooksPath +``` + +**Windows (cmd):** + +```bat +git config --global core.hooksPath "%cd%\hooks-global" +git config --global core.hooksPath +``` + +The second command prints the value Git stored so you can confirm the path. + +> [!TIP] +> Alternatively, run `linux_register_this_folder_as_global_hooks.sh` (Linux or macOS) or `win_register_*.cmd` (Windows) from your clone root so `core.hooksPath` points at this clone's `hooks-global` folder, instead of typing the `git config` commands above. + +## Step 3: Configure repositories you work on + +You **do not** need `.pre-commit-config.yaml` or any other file from this step for global hooks to run. Registration in [Step 2](#step-2-point-git-at-the-global-hooks-directory) applies to **every** repository on your machine. See [What runs on commit](#what-runs-on-commit) below for the branching logic. + +Optional, depending on what you want in **this** repository: + +1. **`[pre-commit](https://pre-commit.com/)` checks** - Add `.pre-commit-config.yaml` only if this repository should run the Python `pre-commit` CLI with that config. Without this file that step is skipped; global hooks still run their other behavior. +2. **Install the `pre-commit` CLI** - Only needed if you added `.pre-commit-config.yaml` (for example `pip install pre-commit`, or whatever your team uses). +3. **CyberFerret** - Add **`.qubership/grand-report.json`** only if you want CyberFerret invoked on commits in this repository (an empty JSON object is valid as a marker; exclusions live there later). Needs `CYBER_FERRET_PASSWORD` from [Prerequisites](#prerequisites). + +## What runs on commit + +When you run `git commit -m "your message"`: + +1. Global hooks run (including an online hook-update check). +2. If `.pre-commit-config.yaml` exists, **pre-commit** runs with that config. +3. If pre-commit passes or there is no config, the repository's **`.git/hooks/pre-commit`** runs if present. + +If any check fails, the commit stops until you fix the issue or adjust configuration and exclusions. + +## Disable global hooks + +To stop using global hooks machine-wide: + +```bash +git config --global --unset core.hooksPath +``` + +## References + +| Resource | Link | +|-----------------------------------------|-----------------------------------------------------------------------------------------------------------------------| +| pre-commit-global overview and readme | [github.com/exadmin/pre-commit-global](https://github.com/exadmin/pre-commit-global) | +| pre-commit framework | [pre-commit.com](https://pre-commit.com/) | From 1c7eddba776049b1481a45ca1db5ecbbb0b860c7 Mon Sep 17 00:00:00 2001 From: dysmon <120464230+dysmon@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:30:25 +0500 Subject: [PATCH 158/161] fix: unify env params (#1216) --- .../scripts/appregdef_render_job.py | 15 +---- build_pipegene/scripts/bg_manage_job.py | 4 +- .../scripts/credential_rotation_job.py | 6 +- build_pipegene/scripts/effective_set_job.py | 8 --- build_pipegene/scripts/env_build_jobs.py | 26 +------- build_pipegene/scripts/gitlab_ci.py | 30 +++++---- .../scripts/inventory_generation_job.py | 12 +--- build_pipegene/scripts/main.py | 2 +- build_pipegene/scripts/passport_jobs.py | 10 +-- build_pipegene/scripts/pipeline_helper.py | 4 +- build_pipegene/scripts/process_sd_job.py | 15 ++--- .../.github/workflows/Envgene.yml | 3 - scripts/utils/pipeline_parameters.py | 63 +++++++++++-------- 13 files changed, 68 insertions(+), 130 deletions(-) diff --git a/build_pipegene/scripts/appregdef_render_job.py b/build_pipegene/scripts/appregdef_render_job.py index 200a88c71..7b8b36e24 100644 --- a/build_pipegene/scripts/appregdef_render_job.py +++ b/build_pipegene/scripts/appregdef_render_job.py @@ -5,14 +5,11 @@ def prepare_appregdef_render_job(pipeline, params, full_env, environment_name, cluster_name, group_id, artifact_id, - artifact_url, tags): + artifact_url): logger.info(f'Prepare appregdef render job for {full_env}') - env_template_version = params.get('ENV_TEMPLATE_VERSION') - is_template_test = params.get('IS_TEMPLATE_TEST') - env_tmp_ver_update_mode = params.get('ENV_TEMPLATE_VERSION_UPDATE_MODE') script = [] - if env_template_version and not is_template_test: + if params.get('ENV_TEMPLATE_VERSION') and not params.get('IS_TEMPLATE_TEST'): script.append('python3 /build_env/scripts/build_env/env_template/set_template_version.py') script.append('python3 /build_env/scripts/build_env/appregdef_render.py') @@ -29,22 +26,14 @@ def prepare_appregdef_render_job(pipeline, params, full_env, environment_name, c "FULL_ENV_NAME": full_env, "CLUSTER_NAME": cluster_name, "ENVIRONMENT_NAME": environment_name, - "ENV_TEMPLATE_TEST": "true" if is_template_test else "false", - "ENV_TEMPLATE_VERSION": env_template_version, "INSTANCES_DIR": "${CI_PROJECT_DIR}/environments", "GROUP_ID": group_id, "ARTIFACT_ID": artifact_id, "ARTIFACT_URL": artifact_url, - "GITLAB_RUNNER_TAG_NAME": tags, - "ENV_TEMPLATE_VERSION_UPDATE_MODE": env_tmp_ver_update_mode } appregdef_render_job = job_instance(params=appregdef_render_params, vars=appregdef_render_vars) - - appregdef_render_job.artifacts.add_paths("${CI_PROJECT_DIR}/environments/" + full_env) - appregdef_render_job.artifacts.add_paths("${CI_PROJECT_DIR}/configuration") appregdef_render_job.artifacts.when = WhenStatement.ALWAYS - pipeline.add_children(appregdef_render_job) return appregdef_render_job diff --git a/build_pipegene/scripts/bg_manage_job.py b/build_pipegene/scripts/bg_manage_job.py index 664d855e1..150d939d1 100644 --- a/build_pipegene/scripts/bg_manage_job.py +++ b/build_pipegene/scripts/bg_manage_job.py @@ -2,7 +2,7 @@ from envgenehelper import logger from pipeline_helper import job_instance -def prepare_bg_manage_job(pipeline, full_env, tags): +def prepare_bg_manage_job(pipeline, full_env): logger.info(f'prepare_bg manage job for {full_env}') job_params = { @@ -13,11 +13,9 @@ def prepare_bg_manage_job(pipeline, full_env, tags): } job_vars = { "FULL_ENV_NAME": full_env, - "GITLAB_RUNNER_TAG_NAME" : tags } job = job_instance(params=job_params, vars=job_vars) - job.artifacts.add_paths(f"$CI_PROJECT_DIR/environments/{full_env}") job.artifacts.when = WhenStatement.ALWAYS pipeline.add_children(job) return job diff --git a/build_pipegene/scripts/credential_rotation_job.py b/build_pipegene/scripts/credential_rotation_job.py index 940d0ebb8..ddda46c1e 100644 --- a/build_pipegene/scripts/credential_rotation_job.py +++ b/build_pipegene/scripts/credential_rotation_job.py @@ -2,7 +2,7 @@ from envgenehelper import logger from pipeline_helper import job_instance -def prepare_credential_rotation_job(pipeline, full_env, environment_name, cluster_name, tags): +def prepare_credential_rotation_job(pipeline, full_env, environment_name, cluster_name): logger.info(f'Prepare credential_rotation_job job for {full_env}.') credential_rotation_params = { "name": f'credential_rotation.{full_env}', @@ -16,12 +16,8 @@ def prepare_credential_rotation_job(pipeline, full_env, environment_name, cluste credential_rotation_vars = { "CLUSTER_NAME": cluster_name, "ENV_NAME": environment_name, - "envgen_args": " -vv", - "envgen_debug": "true", - "GITLAB_RUNNER_TAG_NAME" : tags } credential_rotation_job = job_instance(params=credential_rotation_params, vars=credential_rotation_vars) - credential_rotation_job.artifacts.add_paths("${CI_PROJECT_DIR}/environments") credential_rotation_job.artifacts.add_paths("${CI_PROJECT_DIR}/affected-sensitive-parameters.yaml") credential_rotation_job.artifacts.when = WhenStatement.ALWAYS pipeline.add_children(credential_rotation_job) diff --git a/build_pipegene/scripts/effective_set_job.py b/build_pipegene/scripts/effective_set_job.py index 0516a5d57..2db54fecf 100644 --- a/build_pipegene/scripts/effective_set_job.py +++ b/build_pipegene/scripts/effective_set_job.py @@ -20,7 +20,6 @@ def prepare_generate_effective_set_job(pipeline, full_env_name, env_name, cluste sd_data = params["SD_DATA"] deployment_id = params["DEPLOYMENT_SESSION_ID"] effective_set_config = params["EFFECTIVE_SET_CONFIG"] - tags = params['GITLAB_RUNNER_TAG_NAME'] if "CUSTOM_PARAMS" in params: custom_params = params["CUSTOM_PARAMS"] @@ -102,10 +101,6 @@ def prepare_generate_effective_set_job(pipeline, full_env_name, env_name, cluste "ENV_NAME": env_name, "INSTANCES_DIR": "${CI_PROJECT_DIR}/environments", "effective_set_generator_image": "$effective_set_generator_image", - "envgen_args": " -vv", - "envgen_debug": "true", - "module_config_default": "/module/templates/defaults.yaml", - "GITLAB_RUNNER_TAG_NAME": tags, "EXCLUDE_CLEANUP_TARGETS": " ".join(cleanup_targets) } @@ -120,9 +115,6 @@ def prepare_generate_effective_set_job(pipeline, full_env_name, env_name, cluste environ['CI_PIPELINE_ID'] = real_ci_pipe_id generate_effective_set_job = job_instance(params=generate_effective_set_params, needs=needs, vars=generate_effective_set_vars) - generate_effective_set_job.artifacts.add_paths("${CI_PROJECT_DIR}/environments/" + f"{full_env_name}") - generate_effective_set_job.artifacts.add_paths('${CI_PROJECT_DIR}/sboms') - generate_effective_set_job.artifacts.add_paths('${CI_PROJECT_DIR}/configuration/registry.y*ml') effective_set_expiry = effective_set_config_dict.get("effective_set_expiry") or "1 hour" logger.info(f"effective set expiry value '{effective_set_expiry}'") diff --git a/build_pipegene/scripts/env_build_jobs.py b/build_pipegene/scripts/env_build_jobs.py index 1db80647d..369c08291 100644 --- a/build_pipegene/scripts/env_build_jobs.py +++ b/build_pipegene/scripts/env_build_jobs.py @@ -3,8 +3,7 @@ from pipeline_helper import job_instance -def prepare_env_build_job(pipeline, is_template_test, full_env, enviroment_name, cluster_name, group_id, artifact_id, - tags): +def prepare_env_build_job(pipeline, is_template_test, full_env, enviroment_name, cluster_name, group_id, artifact_id): logger.info(f'prepare env_build job for {full_env}') script = [ @@ -30,31 +29,20 @@ def prepare_env_build_job(pipeline, is_template_test, full_env, enviroment_name, "ENVIRONMENT_NAME": enviroment_name, "GROUP_ID": group_id, "ARTIFACT_ID": artifact_id, - "ENV_TEMPLATE_TEST": "true" if is_template_test else "false", "INSTANCES_DIR": "${CI_PROJECT_DIR}/environments", - "envgen_image": "$envgen_image", - "envgen_args": " -vvv", - "envgen_debug": "true", - "module_config_default": "/module/templates/defaults.yaml", - "GITLAB_RUNNER_TAG_NAME": tags, } env_build_job = job_instance(params=env_build_params, vars=env_build_vars) if is_template_test: - env_build_job.artifacts.add_paths("${CI_PROJECT_DIR}/environments") env_build_job.artifacts.add_paths("${CI_PROJECT_DIR}/set_variable.txt") - else: - env_build_job.artifacts.add_paths("${CI_PROJECT_DIR}/environments/" + f"{full_env}") - env_build_job.artifacts.add_paths("${CI_PROJECT_DIR}/configuration") + env_build_job.artifacts.when = WhenStatement.ALWAYS pipeline.add_children(env_build_job) return env_build_job -def prepare_git_commit_job(pipeline, full_env, enviroment_name, cluster_name, deployment_session_id, tags, - credential_rotation_job: object = None): +def prepare_git_commit_job(pipeline, full_env, enviroment_name, cluster_name, credential_rotation_job: object = None): logger.info(f'prepare git_commit job for {full_env}.') - logger.info(f'Deployment session id is {deployment_session_id}.') git_commit_params = { "name": f'git_commit.{full_env}', "image": '${envgen_image}', @@ -71,17 +59,9 @@ def prepare_git_commit_job(pipeline, full_env, enviroment_name, cluster_name, de "ENV_NAME": full_env, "CLUSTER_NAME": cluster_name, "ENVIRONMENT_NAME": enviroment_name, - "envgen_image": "$envgen_image", - "envgen_args": " -vv", - "envgen_debug": "true", - "module_config_default": "/module/templates/defaults.yaml", "COMMIT_ENV": "true", - "GITLAB_RUNNER_TAG_NAME": tags, - "DEPLOY_SESSION_ID": deployment_session_id } git_commit_job = job_instance(params=git_commit_params, vars=git_commit_vars) - git_commit_job.artifacts.add_paths("${CI_PROJECT_DIR}/environments/" + f"{full_env}") - git_commit_job.artifacts.add_paths('${CI_PROJECT_DIR}/sboms') git_commit_job.artifacts.when = WhenStatement.ALWAYS if (credential_rotation_job is not None): git_commit_job.add_needs(credential_rotation_job) diff --git a/build_pipegene/scripts/gitlab_ci.py b/build_pipegene/scripts/gitlab_ci.py index 4c3ea90aa..866716c14 100644 --- a/build_pipegene/scripts/gitlab_ci.py +++ b/build_pipegene/scripts/gitlab_ci.py @@ -25,9 +25,7 @@ logger.info(f"Detected environment - GitLab: {IS_GITLAB}, GitHub: {IS_GITHUB}") -def build_pipeline(params: dict) -> None: - tags = params['GITLAB_RUNNER_TAG_NAME'] - +def build_pipeline(params: dict, sensitive_params: list) -> None: artifact_url = None if params['IS_TEMPLATE_TEST']: logger.info("Generating jobs in template test mode.") @@ -96,8 +94,7 @@ def build_pipeline(params: dict) -> None: # get passport job if it is not already added for cluster if params['GET_PASSPORT'] and cluster_name not in get_passport_jobs: jobs_map["trigger_passport_job"] = prepare_trigger_passport_job(pipeline, full_env_name) - jobs_map["get_passport_job"] = prepare_passport_job(pipeline, full_env_name, - environment_name, cluster_name, tags) + jobs_map["get_passport_job"] = prepare_passport_job(pipeline, full_env_name, environment_name, cluster_name) get_passport_jobs[cluster_name] = True else: logger.info(f"Generation of cloud passport for environment '{full_env_name}' is skipped") @@ -105,12 +102,11 @@ def build_pipeline(params: dict) -> None: if not params.get('BG_MANAGE', None): logger.info(f'Preparing of bg_manage job for environment {full_env_name} is skipped.') else: - jobs_map['bg_manage_job'] = prepare_bg_manage_job(pipeline, full_env_name, tags) + jobs_map['bg_manage_job'] = prepare_bg_manage_job(pipeline, full_env_name) if is_inventory_generation_needed(params['IS_TEMPLATE_TEST'], params): jobs_map["env_inventory_generation_job"] = prepare_inventory_generation_job(pipeline, full_env_name, - environment_name, cluster_name, - params, tags) + environment_name, cluster_name) else: logger.info( f'Preparing of full_env_name inventory generation job for {full_env_name} ' @@ -119,7 +115,7 @@ def build_pipeline(params: dict) -> None: credential_rotation_job = None if params['CRED_ROTATION_PAYLOAD']: credential_rotation_job = prepare_credential_rotation_job(pipeline, full_env_name, environment_name, - cluster_name, tags) + cluster_name) jobs_map["credential_rotation_job"] = credential_rotation_job else: logger.info( @@ -128,7 +124,7 @@ def build_pipeline(params: dict) -> None: if params['ENV_BUILD']: jobs_map["appregdef_render_job"] = prepare_appregdef_render_job(pipeline, params, full_env_name, environment_name, cluster_name, group_id, - artifact_id, artifact_url, tags) + artifact_id, artifact_url) else: logger.info(f'Preparing of appregdef_render_job {full_env_name} is skipped.') @@ -137,15 +133,13 @@ def build_pipeline(params: dict) -> None: (source_type == "json" and params.get("SD_DATA")) or (source_type == "artifact" and params.get("SD_VERSION")) ): - jobs_map["process_sd_job"] = prepare_process_sd(pipeline, full_env_name, environment_name, cluster_name, - params["APP_DEFS_PATH"], params["REG_DEFS_PATH"], tags) + jobs_map["process_sd_job"] = prepare_process_sd(pipeline, full_env_name, environment_name, cluster_name) else: logger.info(f'Preparing of process_sd_job for {full_env_name} is skipped') if params['ENV_BUILD']: jobs_map["env_build_job"] = prepare_env_build_job(pipeline, params['IS_TEMPLATE_TEST'], full_env_name, - environment_name, cluster_name, group_id, artifact_id, - tags) + environment_name, cluster_name, group_id, artifact_id) else: logger.info(f'Preparing of env_build job for {full_env_name} is skipped.') @@ -165,7 +159,7 @@ def build_pipeline(params: dict) -> None: "generate_effective_set_job", "env_inventory_generation_job", "credential_rotation_job", "bg_manage_job"] - plugin_params = params + plugin_params = params.copy() plugin_params['jobs_map'] = jobs_map plugin_params['job_sequence'] = job_sequence plugin_params['jobs_requiring_git_commit'] = jobs_requiring_git_commit @@ -177,7 +171,6 @@ def build_pipeline(params: dict) -> None: if (any(job in jobs_map for job in plugin_params['jobs_requiring_git_commit']) and not params['IS_TEMPLATE_TEST']): jobs_map["git_commit_job"] = prepare_git_commit_job(pipeline, full_env_name, environment_name, cluster_name, - params['DEPLOYMENT_SESSION_ID'], tags, credential_rotation_job) else: logger.info(f'Preparing of git commit job for {full_env_name} is skipped.') @@ -193,6 +186,11 @@ def build_pipeline(params: dict) -> None: job_instance.add_needs(*find_predecessor_job(job, jobs_map, job_sequence)) logger.info(f'----------------end processing for {full_env_name}---------------------') + + for key, value in params.items(): + if key not in sensitive_params and value is not None and value != '': + sorted_pipeline.add_variables(**{key: value}) + sorted_pipeline.add_tags(params["GITLAB_RUNNER_TAG_NAME"]) # check out repo only once in the first job of the generated pipeline, later jobs get it through artifacts from each other # purpose: avoid later jobs restoring files that were removed by previous jobs, so git commit job can commit those deletions diff --git a/build_pipegene/scripts/inventory_generation_job.py b/build_pipegene/scripts/inventory_generation_job.py index ac1fabb5d..d3d3d30b7 100644 --- a/build_pipegene/scripts/inventory_generation_job.py +++ b/build_pipegene/scripts/inventory_generation_job.py @@ -10,7 +10,7 @@ def is_inventory_generation_needed(is_template_test, inventory_params): if is_template_test: return False - env_inventory_init = inventory_params.get('ENV_INVENTORY_INIT') == 'true' + env_inventory_init = inventory_params.get('ENV_INVENTORY_INIT') env_specific_parameters = inventory_params.get('ENV_SPECIFIC_PARAMS') env_template_name = inventory_params.get('ENV_TEMPLATE_NAME') env_inventory_content = inventory_params.get('ENV_INVENTORY_CONTENT') @@ -28,8 +28,7 @@ def is_inventory_generation_needed(is_template_test, inventory_params): return env_inventory_content or env_inventory_init or bool(env_specific_parameters) or bool(env_template_name) -def prepare_inventory_generation_job(pipeline, full_env_name, environment_name, cluster_name, env_generation_params, - tags): +def prepare_inventory_generation_job(pipeline, full_env_name, environment_name, cluster_name): logger.info(f"prepare env_generation job for {full_env_name}") params = { "name": f"env_inventory_generation.{full_env_name}", @@ -43,15 +42,8 @@ def prepare_inventory_generation_job(pipeline, full_env_name, environment_name, "ENV_NAME": environment_name, "CLUSTER_NAME": cluster_name, "FULL_ENV_NAME": full_env_name, - "envgen_image": "$envgen_image", - "envgen_args": " -vv", - "envgen_debug": "true", - "module_config_default": "/module/templates/defaults.yaml", - "GITLAB_RUNNER_TAG_NAME": tags, - **env_generation_params } job = job_instance(params=params, vars=vars) - job.artifacts.add_paths("${CI_PROJECT_DIR}/environments/") job.artifacts.when = WhenStatement.ALWAYS pipeline.add_children(job) return job diff --git a/build_pipegene/scripts/main.py b/build_pipegene/scripts/main.py index e124be622..b4e5a10b1 100644 --- a/build_pipegene/scripts/main.py +++ b/build_pipegene/scripts/main.py @@ -16,7 +16,7 @@ def perform_generation(): handler = PipelineParametersHandler() handler.log_pipeline_params() validate_pipeline(handler.params) - build_pipeline(handler.params) + build_pipeline(handler.params, handler.sensitive_params) if __name__ == "__main__": gcip() diff --git a/build_pipegene/scripts/passport_jobs.py b/build_pipegene/scripts/passport_jobs.py index 532d3f638..48ebca656 100644 --- a/build_pipegene/scripts/passport_jobs.py +++ b/build_pipegene/scripts/passport_jobs.py @@ -31,7 +31,7 @@ def prepare_trigger_passport_job(pipeline, full_env): return trigger_job -def prepare_passport_job(pipeline, full_env, enviroment_name, cluster_name, tags): +def prepare_passport_job(pipeline, full_env, enviroment_name, cluster_name): logger.info(f'prepare get_passport job for {full_env}') get_passport_params = { @@ -50,17 +50,9 @@ def prepare_passport_job(pipeline, full_env, enviroment_name, cluster_name, tags "ENV_NAME": full_env, "CLUSTER_NAME": cluster_name, "ENVIRONMENT_NAME": enviroment_name, - "envgen_image": "$envgen_image", - "envgen_args": " -vv", - "envgen_debug": "true", - "module_config_default": "/module/templates/defaults.yaml", - "COMMIT_ENV": "false", - "COMMIT_MESSAGE": f"[ci_skip] update cloud passport for {cluster_name}", - "GITLAB_RUNNER_TAG_NAME": tags } get_passport_job = job_instance(params=get_passport_params, vars=get_passport_vars) base = "${CI_PROJECT_DIR}/environments" - get_passport_job.artifacts.add_paths(f"{base}/{full_env}") get_passport_job.artifacts.add_paths(f"{base}/{cluster_name}/cloud-passport") get_passport_job.artifacts.when = WhenStatement.ALWAYS pipeline.add_children(get_passport_job) diff --git a/build_pipegene/scripts/pipeline_helper.py b/build_pipegene/scripts/pipeline_helper.py index e72b5c935..6c7011dd0 100644 --- a/build_pipegene/scripts/pipeline_helper.py +++ b/build_pipegene/scripts/pipeline_helper.py @@ -32,7 +32,7 @@ def render(self) -> Dict[str, Any]: def job_instance(params, vars, needs=None, rules=None): timeout = getenv("RUNNER_SCRIPT_TIMEOUT") or "10m" - gitlab_runner_tag = vars.get('GITLAB_RUNNER_TAG_NAME') + job = JobExtended( name=params['name'], image=params['image'], @@ -56,7 +56,7 @@ def job_instance(params, vars, needs=None, rules=None): if needs is None: needs = [] job.set_needs(needs) - job.add_tags(gitlab_runner_tag) + if rules: job.rules.extend(rules) return job diff --git a/build_pipegene/scripts/process_sd_job.py b/build_pipegene/scripts/process_sd_job.py index bf43335bf..641671f19 100644 --- a/build_pipegene/scripts/process_sd_job.py +++ b/build_pipegene/scripts/process_sd_job.py @@ -6,15 +6,15 @@ from pipeline_helper import job_instance -def prepare_process_sd(pipeline, full_env, environment_name, cluster_name, artifact_app_defs_path, artifact_reg_defs_path, tags): +def prepare_process_sd(pipeline, full_env, environment_name, cluster_name): logger.info(f'Prepare process_sd job for {full_env}') script = [ f'base_env_path="$CI_PROJECT_DIR/environments/{full_env}";', 'app_defs_path="$base_env_path/AppDefs";', 'reg_defs_path="$base_env_path/RegDefs";', - f'[ -n "$APP_REG_DEFS_JOB" ] && [ -n "$APP_DEFS_PATH" ] && mkdir -p $app_defs_path && cp -rf {artifact_app_defs_path}/* $app_defs_path', - f'[ -n "$APP_REG_DEFS_JOB" ] && [ -n "$REG_DEFS_PATH" ] && mkdir -p $reg_defs_path && cp -fr {artifact_reg_defs_path}/* $reg_defs_path', + f'[ -n "$APP_REG_DEFS_JOB" ] && [ -n "$APP_DEFS_PATH" ] && mkdir -p $app_defs_path && cp -rf $APP_DEFS_PATH/* $app_defs_path', + f'[ -n "$APP_REG_DEFS_JOB" ] && [ -n "$REG_DEFS_PATH" ] && mkdir -p $reg_defs_path && cp -fr $REG_DEFS_PATH/* $reg_defs_path', 'python3 /build_env/scripts/build_env/process_sd.py', ] @@ -30,17 +30,10 @@ def prepare_process_sd(pipeline, full_env, environment_name, cluster_name, artif "ENVIRONMENT_NAME": environment_name, "ENV_NAME": environment_name, "INSTANCES_DIR": "${CI_PROJECT_DIR}/environments", - "envgen_image": "$envgen_image", - "envgen_args": " -vv", - "envgen_debug": "true", - "GITLAB_RUNNER_TAG_NAME": tags } process_sd_job = job_instance(params=process_sd_set_params, vars=process_sd_set_vars) - - process_sd_job.artifacts.add_paths("${CI_PROJECT_DIR}/environments/" + full_env) - process_sd_job.artifacts.when = WhenStatement.ALWAYS - + process_sd_job.artifacts.when = WhenStatement.ALWAYS pipeline.add_children(process_sd_job) return process_sd_job \ No newline at end of file diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 5a1aec7be..8b9a00b49 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -464,8 +464,6 @@ jobs: chmod ugo+rw \"\$path/Credentials/\"* fi done - - cp -rf \${CI_PROJECT_DIR}/environments \${CI_PROJECT_DIR}/git_envs " env > .env.container @@ -475,7 +473,6 @@ jobs: name: git_commit_${{ env.PACKAGE_NAME }} path: | environments/${{ matrix.environment }} - git_envs sboms include-hidden-files: true ### GIT COMMIT - END ### diff --git a/scripts/utils/pipeline_parameters.py b/scripts/utils/pipeline_parameters.py index 9b90c006d..05b21a3f6 100644 --- a/scripts/utils/pipeline_parameters.py +++ b/scripts/utils/pipeline_parameters.py @@ -1,5 +1,6 @@ import json import uuid +import copy from os import getenv from envgenehelper import logger @@ -10,32 +11,32 @@ def get_pipeline_parameters() -> dict: return { 'ENV_NAMES': getenv("ENV_NAMES", ""), - 'ENV_BUILD': getenv("ENV_BUILDER") == "true", - 'GET_PASSPORT': getenv("GET_PASSPORT") == "true", - 'GENERATE_EFFECTIVE_SET': getenv("GENERATE_EFFECTIVE_SET", "false") == "true", + 'ENV_BUILD': getenv("ENV_BUILDER", "false").lower() == "true", + 'GET_PASSPORT': getenv("GET_PASSPORT", "false").lower() == "true", + 'GENERATE_EFFECTIVE_SET': getenv("GENERATE_EFFECTIVE_SET", "false").lower() == "true", 'ENV_TEMPLATE_VERSION': getenv("ENV_TEMPLATE_VERSION", ""), - 'ENV_TEMPLATE_TEST': getenv("ENV_TEMPLATE_TEST") == "true", - 'IS_TEMPLATE_TEST': getenv("ENV_TEMPLATE_TEST") == "true", + 'ENV_TEMPLATE_TEST': getenv("ENV_TEMPLATE_TEST", "false").lower() == "true", + 'IS_TEMPLATE_TEST': getenv("ENV_TEMPLATE_TEST", "false").lower() == "true", 'CI_COMMIT_REF_NAME': getenv("CI_COMMIT_REF_NAME", ""), 'JSON_SCHEMAS_DIR': getenv("JSON_SCHEMAS_DIR", "/module/schemas"), - "SD_SOURCE_TYPE": getenv("SD_SOURCE_TYPE") or "artifact", + "SD_SOURCE_TYPE": getenv("SD_SOURCE_TYPE", "artifact"), "SD_VERSION": getenv("SD_VERSION"), "SD_DATA": getenv("SD_DATA"), "SD_DELTA": getenv("SD_DELTA"), "SD_REPO_MERGE_MODE": getenv("SD_REPO_MERGE_MODE"), - "ENV_INVENTORY_INIT": getenv("ENV_INVENTORY_INIT"), + "ENV_INVENTORY_INIT": getenv("ENV_INVENTORY_INIT", "false").lower() == "true", "ENV_SPECIFIC_PARAMS": getenv("ENV_SPECIFIC_PARAMS"), "ENV_TEMPLATE_NAME": getenv("ENV_TEMPLATE_NAME"), 'CRED_ROTATION_PAYLOAD': getenv("CRED_ROTATION_PAYLOAD", ""), - 'CRED_ROTATION_FORCE': getenv("CRED_ROTATION_FORCE", ""), + 'CRED_ROTATION_FORCE': getenv("CRED_ROTATION_FORCE", "false"), 'NS_BUILD_FILTER': getenv("NS_BUILD_FILTER", ""), 'GITLAB_RUNNER_TAG_NAME': getenv("GITLAB_RUNNER_TAG_NAME", ""), - 'RUNNER_SCRIPT_TIMEOUT': getenv("RUNNER_SCRIPT_TIMEOUT") or "10m", - 'DEPLOYMENT_SESSION_ID': getenv("DEPLOYMENT_SESSION_ID", "") or str(uuid.uuid4()), - 'ENVGENE_LOG_LEVEL': getenv("ENVGENE_LOG_LEVEL"), + 'RUNNER_SCRIPT_TIMEOUT': getenv("RUNNER_SCRIPT_TIMEOUT", "10m"), + 'DEPLOYMENT_SESSION_ID': getenv("DEPLOYMENT_SESSION_ID", str(uuid.uuid4())), + 'ENVGENE_LOG_LEVEL': getenv("ENVGENE_LOG_LEVEL", "INFO"), 'CALCULATOR_CLI_JAVA_OPTIONS' : getenv("CALCULATOR_CLI_JAVA_OPTIONS", ""), "BG_STATE": getenv("BG_STATE"), - "BG_MANAGE": getenv("BG_MANAGE") == "true", + "BG_MANAGE": getenv("BG_MANAGE", "false").lower() == "true", "APP_DEFS_PATH": getenv("APP_DEFS_PATH"), "REG_DEFS_PATH": getenv("REG_DEFS_PATH"), "APP_REG_DEFS_JOB": getenv("APP_REG_DEFS_JOB"), @@ -43,18 +44,33 @@ def get_pipeline_parameters() -> dict: "ENV_INVENTORY_CONTENT": getenv("ENV_INVENTORY_CONTENT"), "CUSTOM_PARAMS" : getenv("CUSTOM_PARAMS"), "ENV_TEMPLATE_VERSION_UPDATE_MODE": getenv( - "ENV_TEMPLATE_VERSION_UPDATE_MODE") or TemplateVersionUpdateMode.PERSISTENT.value, + "ENV_TEMPLATE_VERSION_UPDATE_MODE", TemplateVersionUpdateMode.PERSISTENT.value), } + +def get_sensitive_param_names() -> list: + return [ + "CRED_ROTATION_PAYLOAD", + "ENV_INVENTORY_CONTENT", + ] class PipelineParametersHandler: def __init__(self, **kwargs): plugins_dir = '/module/scripts/pipegene_plugins/pipe_parameters' self.params = get_pipeline_parameters() + self.sensitive_params = get_sensitive_param_names() pipe_param_plugin = PluginEngine(plugins_dir=plugins_dir) if pipe_param_plugin.modules: pipe_param_plugin.run(pipeline_params=self.params) + + for k, v in self.params.items(): + try: + parsed = json.loads(v) + self.params[k] = json.dumps(parsed, separators=(",", ":")) + + except (TypeError, ValueError): + pass def hide_secrets(self, data): if isinstance(data, dict): @@ -70,22 +86,17 @@ def hide_secrets(self, data): def log_pipeline_params(self): params_str = "Input parameters are: " - params = self.params.copy() + params = copy.deepcopy(self.params) if params.get("CRED_ROTATION_PAYLOAD"): params["CRED_ROTATION_PAYLOAD"] = "***" + env_inventory_content = params.get("ENV_INVENTORY_CONTENT") + if env_inventory_content: + parsed = json.loads(env_inventory_content) + self.hide_secrets(parsed) + params["ENV_INVENTORY_CONTENT"] = json.dumps(parsed, separators=(",", ":")) + for k, v in params.items(): - try: - parsed = json.loads(v) - - if k == "ENV_INVENTORY_CONTENT": - self.hide_secrets(parsed) - - params[k] = json.dumps(parsed, separators=(",", ":")) - - except (TypeError, ValueError): - pass - - params_str += f"\n{k.upper()}: {params[k]}" + params_str += f"\n{k.upper()}: {v}" logger.info(params_str) From a316501af007dde55f5ead01977dcdf170ef9c80 Mon Sep 17 00:00:00 2001 From: "qubership-actions[bot]" Date: Wed, 29 Apr 2026 09:33:05 +0000 Subject: [PATCH 159/161] chore: Update docker image tags and envgene_version for branch main [skip ci] --- .../instance-repo-pipeline/.github/workflows/Envgene.yml | 6 +++--- .../git-system-follower-package/package.yaml | 2 +- .../git-system-follower-package/package.yaml | 2 +- .../scripts/templates/default/cookiecutter.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml index 8b9a00b49..1a9596a7d 100644 --- a/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml +++ b/github_workflows/instance-repo-pipeline/.github/workflows/Envgene.yml @@ -75,9 +75,9 @@ env: DOCKER_IMAGE_NAME_EFFECTIVE_SET_GENERATOR: "${{ vars.DOCKER_REGISTRY || 'ghcr.io/netcracker' }}/qubership-effective-set-generator" #DOCKER_IMAGE_TAGS - DOCKER_IMAGE_TAG_PIPEGENE: "1.32.5" - DOCKER_IMAGE_TAG_ENVGENE: "1.32.5" - DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.32.5" + DOCKER_IMAGE_TAG_PIPEGENE: "1.32.6" + DOCKER_IMAGE_TAG_ENVGENE: "1.32.6" + DOCKER_IMAGE_TAG_EFFECTIVE_SET_GENERATOR: "1.32.6" jobs: process_environment_variables: diff --git a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml index 4cfd27337..06bb4e8c3 100644 --- a/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_discovery_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_discovery_project -version: 1.32.5 +version: 1.32.6 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml index c4e31240b..8fe0e23a5 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/package.yaml @@ -1,5 +1,5 @@ apiVersion: v1 type: gitlab-ci-pipeline name: envgene_instance_project -version: 1.32.5 +version: 1.32.6 dependencies: [] diff --git a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json index 3fdf1e787..f7f4a1136 100644 --- a/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json +++ b/gsf_packages/envgene_instance_project/git-system-follower-package/scripts/templates/default/cookiecutter.json @@ -2,7 +2,7 @@ "gsf_repository_name": "envgene_instance_project", "docker_registry": "ghcr.io", "docker_namespace": "netcracker", - "envgene_version": "1.32.5", + "envgene_version": "1.32.6", "envgen_image": "qubership-envgene", "pipe_image": "qubership-pipegene", "cloud_deploytool_image": "env-generator-deploytool_build_deploytool", From 6939f29ce9ef93aaa1334d00d197772f03e87d84 Mon Sep 17 00:00:00 2001 From: Andrei Rudchenko <104736077+andyroode@users.noreply.github.com> Date: Wed, 29 Apr 2026 23:45:30 +0300 Subject: [PATCH 160/161] docs: Pasted the how-to about hooks to dev (#1276) --- docs/{how-to => dev}/global-pre-commit-hooks.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) rename docs/{how-to => dev}/global-pre-commit-hooks.md (79%) diff --git a/docs/how-to/global-pre-commit-hooks.md b/docs/dev/global-pre-commit-hooks.md similarity index 79% rename from docs/how-to/global-pre-commit-hooks.md rename to docs/dev/global-pre-commit-hooks.md index 64f949314..809ee3be4 100644 --- a/docs/how-to/global-pre-commit-hooks.md +++ b/docs/dev/global-pre-commit-hooks.md @@ -5,7 +5,7 @@ - [Prerequisites](#prerequisites) - [Step 1: Clone pre-commit-global](#step-1-clone-pre-commit-global) - [Step 2: Point Git at the global hooks directory](#step-2-point-git-at-the-global-hooks-directory) - - [Step 3: Configure repositories you work on](#step-3-configure-repositories-you-work-on) + - [Step 3: grand-report.json](#step-3-grand-reportjson) - [What runs on commit](#what-runs-on-commit) - [Disable global hooks](#disable-global-hooks) - [References](#references) @@ -73,15 +73,14 @@ The second command prints the value Git stored so you can confirm the path. > [!TIP] > Alternatively, run `linux_register_this_folder_as_global_hooks.sh` (Linux or macOS) or `win_register_*.cmd` (Windows) from your clone root so `core.hooksPath` points at this clone's `hooks-global` folder, instead of typing the `git config` commands above. -## Step 3: Configure repositories you work on +## Step 3: grand-report.json -You **do not** need `.pre-commit-config.yaml` or any other file from this step for global hooks to run. Registration in [Step 2](#step-2-point-git-at-the-global-hooks-directory) applies to **every** repository on your machine. See [What runs on commit](#what-runs-on-commit) below for the branching logic. +The **`.qubership/grand-report.json`** file at the repository root is **added by Andrei Rudchenko**. It is required on the CyberFerret-related hook path and holds ignores and exclusions for signatures as needed. -Optional, depending on what you want in **this** repository: +Use `CYBER_FERRET_PASSWORD` as in [Prerequisites](#prerequisites). -1. **`[pre-commit](https://pre-commit.com/)` checks** - Add `.pre-commit-config.yaml` only if this repository should run the Python `pre-commit` CLI with that config. Without this file that step is skipped; global hooks still run their other behavior. -2. **Install the `pre-commit` CLI** - Only needed if you added `.pre-commit-config.yaml` (for example `pip install pre-commit`, or whatever your team uses). -3. **CyberFerret** - Add **`.qubership/grand-report.json`** only if you want CyberFerret invoked on commits in this repository (an empty JSON object is valid as a marker; exclusions live there later). Needs `CYBER_FERRET_PASSWORD` from [Prerequisites](#prerequisites). +> [!NOTE] +> For updates to this file or questions about it, contact **Andrei Rudchenko**. ## What runs on commit From 0cd91fe1c359ff5d02bdc172d28cecd14988b9d1 Mon Sep 17 00:00:00 2001 From: GeethaGadde99 Date: Thu, 30 Apr 2026 12:19:11 +0530 Subject: [PATCH 161/161] feat: Enhance Test data for Calculator CLI --- .../src/test/resources/environments/cluster-01/pl-01/tenant.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/tenant.yml b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/tenant.yml index d267792c6..b35359480 100644 --- a/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/tenant.yml +++ b/build_effective_set_generator/effective-set-generator/src/test/resources/environments/cluster-01/pl-01/tenant.yml @@ -15,7 +15,6 @@ globalE2EParameters: mergeTenantsAndE2EParameters: false environmentParameters: {} deployParameters: - ESCAPE_SEQUENCE: "true" api_config: ${database_config} database_config: connection: