blob: b23fb2a776e9ad83786f5f57b08a7f5bac126e71 [file] [log] [blame]
garciadeblas61a4c692025-07-17 13:04:13 +02001#######################################################################################
2# Copyright ETSI Contributors and Others.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13# implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#######################################################################################
17
18
19import yaml
20import tempfile
21import os
22
23
24MAP_PROFILE = {
25 "infra_controller_profiles": "infra-controller-profiles",
26 "infra_config_profiles": "infra-config-profiles",
27 "resource_profiles": "managed-resources",
28 "app_profiles": "app-profiles",
29}
30
31
32def merge_model(base, override):
33 """Recursively merge override dictionary into base dictionary."""
34 merge_model = base.copy()
35 for k, v in override.get("spec", {}).items():
36 if k != "ksus":
37 merge_model["spec"][k] = v
38 for ksu_override in override.get("spec", {}).get("ksus", []):
39 for ksu_base in merge_model.get("spec", {}).get("ksus", []):
40 if ksu_base.get("name") == ksu_override.get("name"):
41 for k, v in ksu_override.items():
42 if k != "patterns":
43 ksu_base[k] = v
44 continue
45 for pattern_override in ksu_override.get("patterns", []):
46 for pattern_base in ksu_base.get("patterns", []):
47 if pattern_base.get("name") == pattern_override.get("name"):
48 for kp, vp in pattern_override.items():
49 if kp != "bricks":
50 pattern_base[kp] = vp
51 continue
52 for brick_override in pattern_override.get(
53 "bricks", []
54 ):
55 for brick_base in pattern_base.get(
56 "bricks", []
57 ):
58 if brick_base.get(
59 "name"
60 ) == brick_override.get("name"):
61 for kb, vb in brick_override.items():
62 if kb != "hrset-values":
63 brick_base[kb] = vb
64 continue
65 for (
66 hrset_override
67 ) in brick_override.get(
68 "hrset-values", []
69 ):
70 for (
71 hrset_base
72 ) in brick_base.get(
73 "hrset-values", []
74 ):
75 if hrset_base.get(
76 "name"
77 ) == hrset_override.get(
78 "name"
79 ):
80 hrset_base |= (
81 hrset_override
82 )
83 break
84 else:
85 brick_base[
86 "hrset-values"
87 ].append(hrset_override)
88 break
89 else:
90 pattern_base["bricks"].append(
91 brick_override
92 )
93 break
94 else:
95 ksu_base["patterns"].append(pattern_override)
96 break
97 else:
98 merge_model["spec"]["ksus"].append(ksu_override)
99 return merge_model
100
101
102async def launch_app(self, op_id, op_params, workflow_content, operation_type):
103 self.logger.info(
104 f"launch_app Enter. Operation {op_id}. Operation Type: {operation_type}"
105 )
106 # self.logger.debug(f"Operation Params: {op_params}")
107 # self.logger.debug(f"Content: {workflow_content}")
108
109 db_app = workflow_content["app"]
110 db_profile = workflow_content.get("profile")
111
112 profile_t = db_app.get("profile_type")
113 profile_type = MAP_PROFILE[profile_t]
114 profile_name = db_profile.get("git_name").lower()
115 app_name = db_app["git_name"].lower()
116 app_command = f"app {operation_type} $environment"
117 age_public_key = db_profile.get("age_pubkey")
118
119 sw_catalog_model = workflow_content.get("model")
120 self.logger.debug(f"SW catalog model: {sw_catalog_model}")
121
122 # Update the app model, extending it also with the model from op_params
123 if operation_type == "update":
124 model = op_params.get("model", db_app.get("app_model", {}))
125 else:
126 model = op_params.get("model", {})
127 app_model = merge_model(sw_catalog_model, model)
128
129 app_model["kind"] = "AppInstantiation"
130 app_model["metadata"]["name"] = app_name
131 for ksu in app_model.get("spec", {}).get("ksus", []):
132 for pattern in ksu.get("patterns", []):
133 for brick in pattern.get("bricks", []):
134 brick["public-age-key"] = age_public_key
135 self.logger.debug(f"App model: {app_model}")
136
137 if operation_type == "update":
138 params = op_params.get("params", db_app.get("params", {}))
139 else:
140 params = op_params.get("params", {})
141 params["PROFILE_TYPE"] = profile_type
142 params["PROFILE_NAME"] = profile_name
143 params["APPNAME"] = app_name
144 self.logger.debug(f"Params: {params}")
145
146 if operation_type == "update":
147 secret_params = op_params.get("secret_params", db_app.get("secret_params", {}))
148 else:
149 secret_params = op_params.get("secret_params", {})
150 self.logger.debug(f"Secret Params: {secret_params}")
151
152 # Create temporary folder for the app model and the parameters
153 temp_dir = tempfile.mkdtemp(prefix=f"app-{operation_type}-{op_id}-")
154 self.logger.debug(f"Temporary dir created: {temp_dir}")
155 with open(f"{temp_dir}/app_instance_model.yaml", "w") as f:
156 yaml.safe_dump(
157 app_model, f, indent=2, default_flow_style=False, sort_keys=False
158 )
159
160 os.makedirs(f"{temp_dir}/parameters/clear", exist_ok=True)
161 with open(f"{temp_dir}/parameters/clear/environment.yaml", "w") as f:
162 yaml.safe_dump(params, f, indent=2, default_flow_style=False, sort_keys=False)
163
164 # Create PVC and copy app model and parameters to PVC
165 app_model_pvc = f"temp-pvc-app-{op_id}"
166 src_files = [
167 f"{temp_dir}/app_instance_model.yaml",
168 f"{temp_dir}/parameters/clear/environment.yaml",
169 ]
170 dest_files = [
171 "app_instance_model.yaml",
172 "parameters/clear/environment.yaml",
173 ]
174 self.logger.debug(
175 f"Copying files to PVC {app_model_pvc}: {src_files} -> {dest_files}"
176 )
177 await self._kubectl.create_pvc_with_content(
178 name=app_model_pvc,
179 namespace="osm-workflows",
180 src_files=src_files,
181 dest_files=dest_files,
182 )
183
184 # Create secret with secret_params
185 secret_name = f"secret-app-{op_id}"
186 secret_namespace = "osm-workflows"
187 secret_key = "environment.yaml"
188 secret_value = yaml.safe_dump(
189 secret_params, indent=2, default_flow_style=False, sort_keys=False
190 )
191 try:
192 self.logger.debug(f"Testing kubectl: {self._kubectl}")
193 self.logger.debug(
194 f"Testing kubectl configuration: {self._kubectl.configuration}"
195 )
196 self.logger.debug(
197 f"Testing kubectl configuration Host: {self._kubectl.configuration.host}"
198 )
199 self.logger.debug(
200 f"Creating secret {secret_name} in namespace {secret_namespace}"
201 )
202 await self.create_secret(
203 secret_name,
204 secret_namespace,
205 secret_key,
206 secret_value,
207 )
208 except Exception as e:
209 self.logger.info(
210 f"Cannot create secret {secret_name} in namespace {secret_namespace}: {e}"
211 )
212 return (
213 False,
214 f"Cannot create secret {secret_name} in namespace {secret_namespace}: {e}",
215 )
216
217 # Create workflow to launch the app
218 workflow_template = "launcher-app.j2"
219 workflow_name = f"{operation_type}-app-{op_id}"
220 # Additional params for the workflow
221 osm_project_name = workflow_content.get("project_name", "osm_admin")
222
223 # Render workflow
224 manifest = self.render_jinja_template(
225 workflow_template,
226 output_file=None,
227 workflow_name=workflow_name,
228 app_command=app_command,
229 app_model_pvc=app_model_pvc,
230 app_secret_name=secret_name,
231 git_fleet_url=self._repo_fleet_url,
232 git_sw_catalogs_url=self._repo_sw_catalogs_url,
233 app_name=app_name,
234 profile_name=profile_name,
235 profile_type=profile_type,
236 osm_project_name=osm_project_name,
237 workflow_debug=self._workflow_debug,
238 workflow_dry_run=self._workflow_dry_run,
239 )
240 self.logger.debug(f"Workflow manifest: {manifest}")
241
242 # Submit workflow
243 self._kubectl.create_generic_object(
244 namespace="osm-workflows",
245 manifest_dict=yaml.safe_load(manifest),
246 api_group="argoproj.io",
247 api_plural="workflows",
248 api_version="v1alpha1",
249 )
250 workflow_resources = {
251 "app_model": app_model,
252 "secret_params": secret_params,
253 "params": params,
254 }
255 return True, workflow_name, workflow_resources
256
257
258async def create_app(self, op_id, op_params, content):
259 self.logger.info(f"create_app Enter. Operation {op_id}")
260 # self.logger.debug(f"Operation Params: {op_params}")
261 # self.logger.debug(f"Content: {workflow_content}")
262 return await self.launch_app(op_id, op_params, content, "create")
263
264
265async def update_app(self, op_id, op_params, content):
266 self.logger.info(f"update_app Enter. Operation {op_id}")
267 # self.logger.debug(f"Operation Params: {op_params}")
268 # self.logger.debug(f"Content: {workflow_content}")
269 return await self.launch_app(op_id, op_params, content, "update")
270
271
272async def delete_app(self, op_id, op_params, content):
273 self.logger.info(f"delete_app Enter. Operation {op_id}")
274 # self.logger.debug(f"Operation Params: {op_params}")
275 # self.logger.debug(f"Content: {workflow_content}")
276 return await self.launch_app(op_id, op_params, content, "delete")
277
278
279async def clean_items_app_launch(self, op_id, op_params, workflow_content):
280 self.logger.info(f"clean_items_app_launch Enter. Operation {op_id}")
281 # self.logger.debug(f"Operation Params: {op_params}")
282 # self.logger.debug(f"Content: {workflow_content}")
283 try:
284 secret_name = f"secret-app-{op_id}"
285 volume_name = f"temp-pvc-app-{op_id}"
286 items = {
287 "secrets": [
288 {
289 "name": secret_name,
290 "namespace": "osm-workflows",
291 }
292 ],
293 "pvcs": [
294 {
295 "name": volume_name,
296 "namespace": "osm-workflows",
297 }
298 ],
299 }
300 await self.clean_items(items)
301 return True, "OK"
302 except Exception as e:
303 return False, f"Error while cleaning items: {e}"