Skip to content

Commit ed2e437

Browse files
author
Alex Burdusel
committed
Fix name based exclusion in codefresh.py
Improve image removal pruning logic when excluding an image with deps used by other images.
1 parent 251acb8 commit ed2e437

5 files changed

Lines changed: 159 additions & 27 deletions

File tree

tools/deployment-cli-tools/ch_cli_tools/codefresh.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,24 @@ def clean_step_key(s: str) -> str:
4040
return re.sub(r'[^a-zA-Z0-9_]', '_', s)
4141

4242

43+
def dockerfile_selector_candidates(base_path: str, dockerfile_path: str) -> set[str]:
44+
"""Return stable identifiers that may be used with include/exclude selectors."""
45+
relative_to_base = get_app_relative_to_base_path(base_path, dockerfile_path)
46+
parent_name = relative_to_base.split("/")[0] if relative_to_base else ""
47+
return {
48+
candidate for candidate in (
49+
relative_to_base,
50+
app_name_from_path(relative_to_base),
51+
parent_name,
52+
) if candidate
53+
}
54+
55+
56+
def path_contains_excluded_segment(path: str, excluded_segments) -> bool:
57+
normalized_path = f"/{path.replace(os.path.sep, '/')}/"
58+
return any(f"/{segment}/" in normalized_path for segment in excluded_segments)
59+
60+
4361
def get_main_domain(url):
4462
try:
4563
url = url.split("//")[1].split("/")[0]
@@ -238,20 +256,23 @@ def codefresh_steps_from_base_path(base_path, fixed_context=None, include=build_
238256
for dockerfile_path in find_dockerfiles_paths(base_path):
239257
dockerfile_relative_to_root = relpath(dockerfile_path, '.')
240258
dockerfile_relative_to_base = get_app_relative_to_base_path(base_path, dockerfile_path)
259+
selector_candidates = dockerfile_selector_candidates(base_path, dockerfile_path)
241260
app_name = app_name_from_path(dockerfile_relative_to_base)
242261
app_key = app_name
243262
app_config: ApplicationHarnessConfig = app_key in helm_values.apps and helm_values.apps[app_key].harness
244263
full_image_name = helm_values.apps[app_key].image if app_key in helm_values.apps\
245264
else helm_values[KEY_TASK_IMAGES][app_key] if app_key in helm_values[KEY_TASK_IMAGES]\
246265
else f"{base_name}/{app_name}"
247266

248-
if include and not any(
249-
f"/{inc}/" in os.path.relpath(dockerfile_path, root_path) or dockerfile_path.endswith(f"/{inc}") for inc in include
250-
):
267+
if include and not any(inc in selector_candidates for inc in include):
251268
# Skip not included apps
252269
continue
253270

254-
if any(inc in dockerfile_path for inc in (list(exclude) + EXCLUDE_PATHS)):
271+
if any(ex in selector_candidates for ex in exclude):
272+
# Skip explicitly excluded apps/images
273+
continue
274+
275+
if path_contains_excluded_segment(dockerfile_relative_to_root, EXCLUDE_PATHS):
255276
# Skip excluded apps
256277
continue
257278

tools/deployment-cli-tools/ch_cli_tools/configurationgenerator.py

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -204,23 +204,64 @@ def _init_static_images(self, base_image_name):
204204
)
205205

206206
def _assign_static_build_dependencies(self, helm_values):
207+
static_consumers = {}
207208
for static_img_dockerfile in self.static_images:
208209
key = os.path.basename(static_img_dockerfile)
209210
if key in helm_values[KEY_TASK_IMAGES]:
210211
dependencies = guess_build_dependencies_from_dockerfile(
211212
f"{static_img_dockerfile}")
212213
for dep in dependencies:
214+
if dep in self.exclude and dep in helm_values[KEY_TASK_IMAGES] and key not in self.exclude:
215+
static_consumers.setdefault(dep, set()).add(key)
213216
if dep in self.base_images and dep not in helm_values[KEY_TASK_IMAGES]:
214217
helm_values[KEY_TASK_IMAGES][dep] = self.base_images[dep]
215218
# helm_values.setdefault(KEY_TASK_IMAGES_BUILD, {})[dep] = {
216219
# 'context': os.path.relpath(static_img_dockerfile, self.dest_deployment_path.parent),
217220
# 'dockerfile': 'Dockerfile',
218221
# }
219222

220-
for image_name in list(helm_values[KEY_TASK_IMAGES].keys()):
221-
if image_name in self.exclude:
222-
del helm_values[KEY_TASK_IMAGES][image_name]
223-
# del helm_values[KEY_TASK_IMAGES_BUILD][image_name]
223+
self._prune_excluded_task_images(helm_values, extra_consumers=static_consumers)
224+
225+
def _get_excluded_task_image_consumers(self, values):
226+
"""Return reverse dependencies for excluded task images from app build deps."""
227+
task_images = values.get(KEY_TASK_IMAGES, {})
228+
apps = values.get(KEY_APPS, {})
229+
consumers = {}
230+
231+
for app_name, app_values in apps.items():
232+
if app_name in self.exclude:
233+
continue
234+
build_dependencies = app_values.get(KEY_HARNESS, {}).get('dependencies', {}).get('build', [])
235+
if not build_dependencies:
236+
continue
237+
for dep in build_dependencies:
238+
if dep in self.exclude and dep in task_images:
239+
consumers.setdefault(dep, set()).add(app_name)
240+
241+
return consumers
242+
243+
def _prune_excluded_task_images(self, values, extra_consumers=None):
244+
"""Exclude task images only when no non-excluded image still depends on them."""
245+
task_images = values.get(KEY_TASK_IMAGES, {})
246+
if not task_images:
247+
return
248+
249+
consumers = self._get_excluded_task_image_consumers(values)
250+
if extra_consumers:
251+
for image_name, image_consumers in extra_consumers.items():
252+
consumers.setdefault(image_name, set()).update(image_consumers)
253+
254+
for image_name in list(task_images.keys()):
255+
if image_name not in self.exclude:
256+
continue
257+
used_by = sorted(consumers.get(image_name, set()))
258+
if used_by:
259+
logging.warning(
260+
"Image %s was excluded but is still required by non-excluded builds: %s. Keeping it.",
261+
image_name, ", ".join(used_by)
262+
)
263+
continue
264+
del task_images[image_name]
224265

225266
def _init_base_images(self, base_image_name):
226267
"""Initialize base images (infrastructure/base-images/) with root context."""
@@ -600,6 +641,7 @@ def validate_dependencies(values):
600641
all_apps = {a for a in values["apps"]}
601642
for app in all_apps:
602643
app_values = values["apps"][app]
644+
app_task_images = set(app_values.get(KEY_TASK_IMAGES, {}))
603645
if 'dependencies' in app_values[KEY_HARNESS]:
604646
soft_dependencies = {
605647
d for d in app_values[KEY_HARNESS]['dependencies']['soft']}
@@ -617,9 +659,9 @@ def validate_dependencies(values):
617659
build_dependencies = {
618660
d for d in app_values[KEY_HARNESS]['dependencies']['build']}
619661

662+
available_builds = set(values.get(KEY_TASK_IMAGES, {})) | all_apps | app_task_images
620663
not_found = {
621-
d for d in build_dependencies if d not in values[KEY_TASK_IMAGES]}
622-
not_found = {d for d in not_found if d not in all_apps}
664+
d for d in build_dependencies if d not in available_builds}
623665
if not_found:
624666
raise ValuesValidationException(
625667
f"Bad build dependencies specified for application {app}: {','.join(not_found)} not found as built image")
@@ -629,8 +671,10 @@ def validate_dependencies(values):
629671

630672
not_found = {d for d in service_dependencies if d not in all_apps}
631673
if not_found:
632-
raise ValuesValidationException(
633-
f"Bad service application dependencies specified for application {app}: {','.join(not_found)}")
674+
logging.warning(
675+
f"Service proxy (use_services) dependencies for application {app} are not deployed: "
676+
f"{','.join(not_found)}. Proxies to these services will not be created."
677+
)
634678

635679

636680
def collect_apps_helm_templates(search_root, dest_helm_chart_path, templates_path=HELM_PATH, exclude=(), include=None, envs=()):

tools/deployment-cli-tools/ch_cli_tools/helm.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ def _aggregate_task_images(self, values):
170170
if image:
171171
values[KEY_TASK_IMAGES][dep_name] = image
172172
app_name = dep_name
173+
elif dep_name in self.base_images:
174+
values[KEY_TASK_IMAGES][dep_name] = self.base_images[dep_name]
173175
elif prefix in apps:
174176
app_name = prefix
175177
values[KEY_TASK_IMAGES][dep_name] = apps[app_name][KEY_TASK_IMAGES][dep_name]
@@ -178,10 +180,7 @@ def _aggregate_task_images(self, values):
178180
if key in included_builds or app_name in self.include:
179181
values[KEY_TASK_IMAGES][key] = apps[app_name][KEY_TASK_IMAGES][key]
180182

181-
for ex in self.exclude:
182-
if ex in values[KEY_TASK_IMAGES]:
183-
logging.info(f"Excluding {ex} from build")
184-
del values[KEY_TASK_IMAGES][ex]
183+
self._prune_excluded_task_images(values)
185184

186185
def __finish_helm_values(self, values, defer_task_images=False):
187186
"""
@@ -279,6 +278,8 @@ def __finish_helm_values(self, values, defer_task_images=False):
279278
if image:
280279
values[KEY_TASK_IMAGES][dep_name] = image
281280
app_name = dep_name
281+
elif dep_name in self.base_images:
282+
values[KEY_TASK_IMAGES][dep_name] = self.base_images[dep_name]
282283
elif prefix in apps: # build dependency within an application that is not part of the deployment
283284
app_name = prefix
284285
values[KEY_TASK_IMAGES][dep_name] = apps[app_name][KEY_TASK_IMAGES][dep_name]
@@ -294,10 +295,7 @@ def __finish_helm_values(self, values, defer_task_images=False):
294295
values[KEY_TASK_IMAGES].update(apps[v][KEY_TASK_IMAGES])
295296

296297
if not defer_task_images:
297-
for ex in self.exclude:
298-
if ex in values[KEY_TASK_IMAGES]:
299-
logging.info(f"Excluding {ex} from build")
300-
del values[KEY_TASK_IMAGES][ex]
298+
self._prune_excluded_task_images(values)
301299
create_env_variables(values)
302300
return values, self.include
303301

tools/deployment-cli-tools/tests/test_codefresh.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,50 @@ def test_create_codefresh_configuration_nobuild():
331331
assert "publish_myapp-mytask" in l1_steps["publish"]["steps"]
332332

333333

334+
def test_excluding_common_app_does_not_skip_common_images_dependencies():
335+
values = create_helm_chart(
336+
[CLOUDHARNESS_ROOT, RESOURCES],
337+
output_path=OUT,
338+
include=['myapp'],
339+
exclude=['common'],
340+
domain='my.local',
341+
namespace='test',
342+
env='dev',
343+
local=False,
344+
tag=1,
345+
registry='reg'
346+
)
347+
try:
348+
values[KEY_TASK_IMAGES]['my-common'] = 'reg/testprojectname/my-common:1'
349+
350+
root_paths = preprocess_build_overrides(
351+
root_paths=[CLOUDHARNESS_ROOT, RESOURCES],
352+
helm_values=values,
353+
merge_build_path=BUILD_MERGE_DIR
354+
)
355+
356+
build_included = [app['harness']['name']
357+
for app in values['apps'].values() if 'harness' in app]
358+
359+
cf = create_codefresh_deployment_scripts(root_paths, include=build_included,
360+
exclude=['common'],
361+
envs=['dev'],
362+
base_image_name=values['name'],
363+
helm_values=values, save=False)
364+
365+
all_build_steps = {
366+
step_name: step
367+
for build_step_name in [STEP_0, STEP_1, STEP_2, STEP_3]
368+
if build_step_name in cf['steps']
369+
for step_name, step in cf['steps'][build_step_name]['steps'].items()
370+
}
371+
372+
assert 'my-common' in all_build_steps, \
373+
'Excluding app "common" must not skip the my-common image under infrastructure/common-images'
374+
finally:
375+
shutil.rmtree(BUILD_MERGE_DIR, ignore_errors=True)
376+
377+
334378
def test_codefresh_db_connect_string_secret():
335379
"""When an app has database.connect_string set to '', a custom_values entry must be added to the deployment step."""
336380
values = create_helm_chart(

tools/deployment-cli-tools/tests/test_helm.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,35 @@ def test_collect_helm_values_wrong_dependencies_validate(tmp_path):
197197
with pytest.raises(ValuesValidationException):
198198
create_helm_chart([CLOUDHARNESS_ROOT, f"{RESOURCES}/wrong-dependencies"], output_path=out_folder, domain="my.local",
199199
namespace='test', env='prod', local=False, tag=1, include=["wrong-build"])
200-
with pytest.raises(ValuesValidationException):
200+
try:
201201
create_helm_chart([CLOUDHARNESS_ROOT, f"{RESOURCES}/wrong-dependencies"], output_path=out_folder, domain="my.local",
202202
namespace='test', env='prod', local=False, tag=1, include=["wrong-services"])
203+
except ValuesValidationException:
204+
pytest.fail("Should not error because of missing use_services dependency")
205+
206+
207+
def test_validate_dependencies_accepts_app_local_build_images():
208+
values = {
209+
KEY_APPS: {
210+
'portal': {
211+
KEY_HARNESS: {
212+
'dependencies': {
213+
'soft': [],
214+
'hard': [],
215+
'build': ['cloudharness-base', 'cloudharness-django'],
216+
},
217+
'use_services': [],
218+
},
219+
KEY_TASK_IMAGES: {
220+
'cloudharness-base': 'reg/project/cloudharness-base:1',
221+
'cloudharness-django': 'reg/project/cloudharness-django:1',
222+
},
223+
}
224+
},
225+
KEY_TASK_IMAGES: {},
226+
}
227+
228+
validate_dependencies(values)
203229

204230

205231
def test_collect_helm_values_build_dependencies(tmp_path):
@@ -441,13 +467,12 @@ def test_exclude_single_task(tmp_path):
441467

442468
assert "myapp-mytask" not in values["task-images"], "myapp-mytask has been excluded, so should not appear in the task images"
443469

444-
try:
445-
values = create_helm_chart([CLOUDHARNESS_ROOT, RESOURCES], output_path=out_folder, domain="my.local",
446-
env='fulldep', local=False, include=["dependantapp"], exclude=["myapp-mytask"])
470+
values = create_helm_chart([CLOUDHARNESS_ROOT, RESOURCES], output_path=out_folder, domain="my.local",
471+
env='fulldep', local=False, include=["dependantapp"], exclude=["myapp-mytask"])
447472

448-
assert False, "myapp-mytask has been excluded, but also declared as a dependency, so should not be excluded"
449-
except ValuesValidationException as e:
450-
pass
473+
assert "myapp-mytask" in values[KEY_TASK_IMAGES], (
474+
"myapp-mytask is excluded but still required by dependantapp, so it should be kept"
475+
)
451476

452477

453478
def test_app_depends_on_app(tmp_path):

0 commit comments

Comments
 (0)