Skip to content

Commit 635b719

Browse files
authored
Merge pull request #852 from MetaCell/bugfix/CH-268
CH-268: Fix name based exclusion in codefresh.py
2 parents b9d360f + ed2e437 commit 635b719

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
@@ -62,6 +62,24 @@ def clean_step_key(s: str) -> str:
6262
return re.sub(r'[^a-zA-Z0-9_]', '_', s)
6363

6464

65+
def dockerfile_selector_candidates(base_path: str, dockerfile_path: str) -> set[str]:
66+
"""Return stable identifiers that may be used with include/exclude selectors."""
67+
relative_to_base = get_app_relative_to_base_path(base_path, dockerfile_path)
68+
parent_name = relative_to_base.split("/")[0] if relative_to_base else ""
69+
return {
70+
candidate for candidate in (
71+
relative_to_base,
72+
app_name_from_path(relative_to_base),
73+
parent_name,
74+
) if candidate
75+
}
76+
77+
78+
def path_contains_excluded_segment(path: str, excluded_segments) -> bool:
79+
normalized_path = f"/{path.replace(os.path.sep, '/')}/"
80+
return any(f"/{segment}/" in normalized_path for segment in excluded_segments)
81+
82+
6583
def get_main_domain(url):
6684
try:
6785
url = url.split("//")[1].split("/")[0]
@@ -262,20 +280,23 @@ def codefresh_steps_from_base_path(base_path, fixed_context=None, include=build_
262280
for dockerfile_path in find_dockerfiles_paths(base_path):
263281
dockerfile_relative_to_root = relpath(dockerfile_path, '.')
264282
dockerfile_relative_to_base = get_app_relative_to_base_path(base_path, dockerfile_path)
283+
selector_candidates = dockerfile_selector_candidates(base_path, dockerfile_path)
265284
app_name = app_name_from_path(dockerfile_relative_to_base)
266285
app_key = app_name
267286
app_config: ApplicationHarnessConfig = app_key in helm_values.apps and helm_values.apps[app_key].harness
268287
full_image_name = helm_values.apps[app_key].image if app_key in helm_values.apps\
269288
else helm_values[KEY_TASK_IMAGES][app_key] if app_key in helm_values[KEY_TASK_IMAGES]\
270289
else f"{base_name}/{app_name}"
271290

272-
if include and not any(
273-
f"/{inc}/" in os.path.relpath(dockerfile_path, root_path) or dockerfile_path.endswith(f"/{inc}") for inc in include
274-
):
291+
if include and not any(inc in selector_candidates for inc in include):
275292
# Skip not included apps
276293
continue
277294

278-
if any(inc in dockerfile_path for inc in (list(exclude) + EXCLUDE_PATHS)):
295+
if any(ex in selector_candidates for ex in exclude):
296+
# Skip explicitly excluded apps/images
297+
continue
298+
299+
if path_contains_excluded_segment(dockerfile_relative_to_root, EXCLUDE_PATHS):
279300
# Skip excluded apps
280301
continue
281302

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
@@ -381,6 +381,50 @@ def test_create_codefresh_configuration_nobuild():
381381
assert "publish_myapp-mytask" in l1_steps["publish"]["steps"]
382382

383383

384+
def test_excluding_common_app_does_not_skip_common_images_dependencies():
385+
values = create_helm_chart(
386+
[CLOUDHARNESS_ROOT, RESOURCES],
387+
output_path=OUT,
388+
include=['myapp'],
389+
exclude=['common'],
390+
domain='my.local',
391+
namespace='test',
392+
env='dev',
393+
local=False,
394+
tag=1,
395+
registry='reg'
396+
)
397+
try:
398+
values[KEY_TASK_IMAGES]['my-common'] = 'reg/testprojectname/my-common:1'
399+
400+
root_paths = preprocess_build_overrides(
401+
root_paths=[CLOUDHARNESS_ROOT, RESOURCES],
402+
helm_values=values,
403+
merge_build_path=BUILD_MERGE_DIR
404+
)
405+
406+
build_included = [app['harness']['name']
407+
for app in values['apps'].values() if 'harness' in app]
408+
409+
cf = create_codefresh_deployment_scripts(root_paths, include=build_included,
410+
exclude=['common'],
411+
envs=['dev'],
412+
base_image_name=values['name'],
413+
helm_values=values, save=False)
414+
415+
all_build_steps = {
416+
step_name: step
417+
for build_step_name in [STEP_0, STEP_1, STEP_2, STEP_3]
418+
if build_step_name in cf['steps']
419+
for step_name, step in cf['steps'][build_step_name]['steps'].items()
420+
}
421+
422+
assert 'my-common' in all_build_steps, \
423+
'Excluding app "common" must not skip the my-common image under infrastructure/common-images'
424+
finally:
425+
shutil.rmtree(BUILD_MERGE_DIR, ignore_errors=True)
426+
427+
384428
def test_codefresh_db_connect_string_secret():
385429
"""When an app has database.connect_string set to '', a custom_values entry must be added to the deployment step."""
386430
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)