Skip to content

Commit a12595a

Browse files
authored
Merge pull request #1521 from simonbaird/fix-unique-rpms-check
Improve the mismatched rpm versions across multi-arch images check
2 parents a2a6acd + 2c3dbba commit a12595a

File tree

5 files changed

+215
-87
lines changed

5 files changed

+215
-87
lines changed

antora/docs/modules/ROOT/pages/packages/release_rpm_packages.adoc

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@ Rules used to verify different properties of specific RPM packages found in the
1111
[#rpm_packages__unique_version]
1212
=== link:#rpm_packages__unique_version[Unique Version]
1313

14-
Check if there is more than one version of the same RPM installed across different architectures. This check only applies for Image Indexes, aka multi-platform images. Use the `non_unique_rpm_names` rule data key to ignore certain RPMs.
14+
Check if a multi-arch build has the same RPM versions installed across each different architecture. This check only applies for Image Indexes, aka multi-platform images. Use the `non_unique_rpm_names` rule data key to ignore certain RPMs.
1515

1616
* Rule type: [rule-type-indicator failure]#FAILURE#
17-
* FAILURE message: `Multiple versions of the %q RPM were found: %s`
17+
* FAILURE message: `Mismatched versions of the %q RPM were found across different arches. %s`
1818
* Code: `rpm_packages.unique_version`
19-
* Effective from: `2025-10-01T00:00:00Z`
2019
* https://github.com/conforma/policy/blob/{page-origin-refhash}/policy/release/rpm_packages/rpm_packages.rego#L17[Source, window="_blank"]

policy/lib/string_utils.rego

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,12 @@ quoted_values_string(value_list) := result if {
1010

1111
result := concat(", ", quoted_list)
1212
}
13+
14+
pluralize_maybe(set_or_list, singular_word, plural_word) := singular_word if {
15+
# One item, use the singular word
16+
count(set_or_list) == 1
17+
} else := sprintf("%ss", [singular_word]) if {
18+
# No plural word provided, make one by adding an "s"
19+
plural_word in {null, ""}
20+
# Use provided plural word
21+
} else := plural_word

policy/lib/string_utils_test.rego

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,23 @@ test_quoted_values_string if {
88
lib.assert_equal("'a', 'b', 'c'", lib.quoted_values_string(["a", "b", "c"]))
99
lib.assert_equal("'a', 'b', 'c'", lib.quoted_values_string({"a", "b", "c"}))
1010
}
11+
12+
test_pluralize_maybe if {
13+
test_cases := [
14+
{
15+
"singular": "mouse",
16+
"plural": "mice",
17+
"expected": ["mouse", "mice", "mice"],
18+
},
19+
{
20+
"singular": "bug",
21+
"plural": "",
22+
"expected": ["bug", "bugs", "bugs"],
23+
},
24+
]
25+
26+
every t in test_cases {
27+
result := [lib.pluralize_maybe(s, t.singular, t.plural) | some s in [{"a"}, {"a", "b"}, {}]]
28+
lib.assert_equal(t.expected, result)
29+
}
30+
}

policy/release/rpm_packages/rpm_packages.rego

Lines changed: 81 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,64 +17,107 @@ import data.lib.tekton
1717
# METADATA
1818
# title: Unique Version
1919
# description: >-
20-
# Check if there is more than one version of the same RPM installed across different
21-
# architectures. This check only applies for Image Indexes, aka multi-platform images.
20+
# Check if a multi-arch build has the same RPM versions installed across each different
21+
# architecture. This check only applies for Image Indexes, aka multi-platform images.
2222
# Use the `non_unique_rpm_names` rule data key to ignore certain RPMs.
2323
# custom:
2424
# short_name: unique_version
25-
# failure_msg: 'Multiple versions of the %q RPM were found: %s'
25+
# failure_msg: 'Mismatched versions of the %q RPM were found across different arches. %s'
2626
# collections:
2727
# - redhat
28-
# # Pushed back again due to https://issues.redhat.com/browse/EC-1354
29-
# effective_on: 2025-10-01T00:00:00Z
3028
#
3129
deny contains result if {
3230
image.is_image_index(input.image.ref)
3331

34-
# Arguably this is a weird edge case that should be dealt with by changing
35-
# the build to not be multi-arch. But this avoids producing a confusing and
36-
# not useful violation in some cases.
37-
not _is_single_image_index(input.image.ref)
32+
some rpm_name in rpm_names_with_mismatched_nvr_sets
33+
not rpm_name in lib.rule_data("non_unique_rpm_names")
34+
35+
detail_text := concat(" ", sort(rpm_mismatch_details(rpm_name)))
3836

39-
some name, versions in grouped_rpm_purls
40-
count(versions) > 1
41-
not name in lib.rule_data("non_unique_rpm_names")
4237
result := lib.result_helper_with_term(
4338
rego.metadata.chain(),
44-
[name, concat(", ", versions)],
45-
name,
39+
[rpm_name, detail_text],
40+
rpm_name,
4641
)
4742
}
4843

49-
# grouped_rpm_purls groups the found RPMs by name to facilitate detecting different versions. It
50-
# has the following structure:
51-
# {
52-
# "spam-maps": {"1.2.3-0", "1.2.3-9"},
53-
# "bacon": {"7.8.8-8"},
54-
# }
55-
grouped_rpm_purls[name] contains version if {
56-
some rpm_purl in all_rpm_purls
57-
rpm := ec.purl.parse(rpm_purl)
58-
name := rpm.name
44+
# Do some extra work here to make a nice tidy violation message
45+
rpm_mismatch_details(rpm_name) := [detail |
46+
# Get all unique NVR sets for this RPM
47+
some nvr_set in {nvrs |
48+
some platform, nvrs in all_rpms_by_name_and_platform[rpm_name]
49+
}
50+
51+
# Find all platforms that have this NVR set
52+
platforms_with_nvr_set := [platform |
53+
some platform, nvrs in all_rpms_by_name_and_platform[rpm_name]
54+
nvrs == nvr_set
55+
]
5956

60-
# NOTE: This includes both version and release.
61-
version := rpm.version
57+
detail := sprintf("%s %s %s %s.", [
58+
lib.pluralize_maybe(platforms_with_nvr_set, "Platform", ""),
59+
concat(", ", sort(platforms_with_nvr_set)),
60+
lib.pluralize_maybe(platforms_with_nvr_set, "has", "have"),
61+
concat(", ", sort(nvr_set)),
62+
])
63+
]
64+
65+
# Detect RPMs where the set of nvrs differs across platforms.
66+
# Generally the sets of versions are of size one, but in some cases we have more
67+
# than one version of a particular rpm due to multi-stage builds.
68+
rpm_names_with_mismatched_nvr_sets contains rpm_name if {
69+
some rpm_name, platform_sets in all_rpms_by_name_and_platform
70+
nvr_sets := {nvrs | some _platform, nvrs in platform_sets}
71+
72+
# If there are more than one unique set of nvrs, then we have some
73+
# kind of mismatch between the platforms
74+
count(nvr_sets) > 1
6275
}
6376

64-
all_rpm_purls contains rpm.purl if {
77+
# A list of rpms grouped by rpm name and by platform
78+
# Something like this:
79+
# {
80+
# "acl": {
81+
# "linux/arm64": ["acl-2.3.1-4.el9"],
82+
# "linux/ppc64le": ["acl-2.3.1-4.el9"],
83+
# ...
84+
# },
85+
# ...
86+
# }
87+
all_rpms_by_name_and_platform[rpm_name][platform] contains nvr if {
6588
some attestation in lib.pipelinerun_attestations
89+
90+
# We're expecting multiple matrixed build tasks, one
91+
# for each platform
6692
some build_task in tekton.build_tasks(attestation)
93+
94+
# Determine which os/arch was built by this build task.
95+
# Note: We expect this to be present for the Konflux multi-arch builds. If it
96+
# isn't then this check doesn't work and mismatched rpm versions will not be
97+
# detected. If there was somehow a different way to build a multi-arch image,
98+
# we would need to find another way to figure out which platform each SBOM is
99+
# related to.
100+
platform := tekton.task_param(build_task, "PLATFORM")
101+
102+
# Find the SBOM location
67103
some result in tekton.task_results(build_task)
68104
result.name == "SBOM_BLOB_URL"
69-
url := result.value
70-
blob := ec.oci.blob(url)
71-
s := json.unmarshal(blob)
72-
some rpm in sbom.rpms_from_sbom(s)
73-
}
105+
sbom_blob_ref := result.value
74106

75-
# For detecting image indexes with just a single image in them.
76-
# (I don't think there are any valid reasons for these to exist)
77-
_is_single_image_index(ref) if {
78-
index := ec.oci.image_index(ref)
79-
count(index.manifests) == 1
80-
} else := false
107+
# Fetch the SBOM data
108+
sbom_blob := ec.oci.blob(sbom_blob_ref)
109+
s := json.unmarshal(sbom_blob)
110+
111+
# Extract the list of rpm purls from the SBOM and parse out
112+
# the rpm version details
113+
some rpm_info in sbom.rpms_from_sbom(s)
114+
rpm := ec.purl.parse(rpm_info.purl)
115+
rpm_name := rpm.name
116+
rpm_version := rpm.version
117+
118+
# We really only need the version, but it's convenient for
119+
# creating violation messages if we use the full nvr here.
120+
# Note that rpm.version is actually the version and the release in
121+
# RPM terms, hence this is the name-version-release, aka the nvr
122+
nvr := sprintf("%s-%s", [rpm_name, rpm_version])
123+
}

0 commit comments

Comments
 (0)