Skip to content

Commit 1e079a6

Browse files
committed
test: add volatile config warnings schema contract test
Add acceptance test scenario that validates the schema contract between ec-cli and ec-policies for volatile config warnings. This test: 1. Creates a policy with volatileConfig.include rules containing: - Rule with no expiration (triggers no_expiration warning) - Rule with effectiveUntil in future (triggers expiring_rule warning) - Rule with effectiveOn in future (triggers pending_rule warning) - Rule scoped to component name (tests componentNames field) 2. Uses a Rego policy that reads from: - input.policy_spec.sources[_].volatileConfig.include[_] - input.component_name - All VolatileCriteria fields (effectiveOn, effectiveUntil, componentNames, etc.) 3. If either the CLI or ec-policies changes the schema in a breaking way, this test will fail, providing early detection of contract violations. Refs: https://issues.redhat.com/browse/EC-1615
1 parent 05e8bf8 commit 1e079a6

2 files changed

Lines changed: 175 additions & 0 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Policy to validate volatile config schema contract between ec-cli and ec-policies.
2+
# This serves as an integration test to ensure the input schema is correctly populated.
3+
package volatile_config_contract
4+
5+
import rego.v1
6+
7+
# METADATA
8+
# title: Volatile config rule pending activation
9+
# description: Warns when a volatile config rule has effectiveOn in the future
10+
# custom:
11+
# short_name: pending_rule
12+
# failure_msg: "Volatile %s rule '%s' is pending activation (effective on: %s)"
13+
# solution: Informational warning about pending volatile config rule
14+
# collections:
15+
# - minimal
16+
warn contains result if {
17+
some source in object.get(input, ["policy_spec", "sources"], [])
18+
volatile_config := object.get(source, "volatileConfig", {})
19+
some config in object.get(volatile_config, "include", [])
20+
_is_applicable(config)
21+
effective_on := object.get(config, "effectiveOn", "")
22+
effective_on != ""
23+
_is_future_date(effective_on)
24+
result := {
25+
"code": "volatile_config_contract.pending_rule",
26+
"msg": sprintf("Volatile include rule '%s' is pending activation (effective on: %s)", [config.value, effective_on]),
27+
}
28+
}
29+
30+
# METADATA
31+
# title: Volatile config rule expiring soon
32+
# description: Warns when a volatile config rule will expire within threshold
33+
# custom:
34+
# short_name: expiring_rule
35+
# failure_msg: "Volatile %s rule '%s' expires soon (effective until: %s)"
36+
# solution: Review the volatile configuration rule and decide whether to extend it
37+
# collections:
38+
# - minimal
39+
warn contains result if {
40+
some source in object.get(input, ["policy_spec", "sources"], [])
41+
volatile_config := object.get(source, "volatileConfig", {})
42+
some config in object.get(volatile_config, "include", [])
43+
_is_applicable(config)
44+
effective_until := object.get(config, "effectiveUntil", "")
45+
effective_until != ""
46+
not _is_past_date(effective_until)
47+
result := {
48+
"code": "volatile_config_contract.expiring_rule",
49+
"msg": sprintf("Volatile include rule '%s' expires soon (effective until: %s)", [config.value, effective_until]),
50+
}
51+
}
52+
53+
# METADATA
54+
# title: Volatile config rule has no expiration
55+
# description: Warns when a volatile config rule has no effectiveUntil date
56+
# custom:
57+
# short_name: no_expiration
58+
# failure_msg: "Volatile %s rule '%s' has no expiration date set"
59+
# solution: Consider adding an effectiveUntil date
60+
# collections:
61+
# - minimal
62+
warn contains result if {
63+
some source in object.get(input, ["policy_spec", "sources"], [])
64+
volatile_config := object.get(source, "volatileConfig", {})
65+
some config in object.get(volatile_config, "include", [])
66+
_is_applicable(config)
67+
object.get(config, "effectiveUntil", "") == ""
68+
effective_on := object.get(config, "effectiveOn", "")
69+
not _is_future_date(effective_on)
70+
result := {
71+
"code": "volatile_config_contract.no_expiration",
72+
"msg": sprintf("Volatile include rule '%s' has no expiration date set", [config.value]),
73+
}
74+
}
75+
76+
# Check if rule is applicable to current image (global or matching)
77+
_is_applicable(config) if {
78+
# Global rule - no image/component constraints
79+
object.get(config, "imageRef", "") == ""
80+
object.get(config, "imageUrl", "") == ""
81+
object.get(config, "imageDigest", "") == ""
82+
count(object.get(config, "componentNames", [])) == 0
83+
}
84+
85+
_is_applicable(config) if {
86+
# Match by component name
87+
component_names := object.get(config, "componentNames", [])
88+
count(component_names) > 0
89+
input_component := object.get(input, "component_name", "")
90+
some name in component_names
91+
name == input_component
92+
}
93+
94+
_is_applicable(config) if {
95+
# Match by imageDigest
96+
image_digest := object.get(config, "imageDigest", "")
97+
image_digest != ""
98+
_extract_digest(object.get(input, ["image", "ref"], "")) == image_digest
99+
}
100+
101+
# Helper to extract digest from image ref
102+
_extract_digest(ref) := digest if {
103+
contains(ref, "@")
104+
parts := split(ref, "@")
105+
count(parts) == 2
106+
digest := parts[1]
107+
}
108+
109+
_extract_digest(ref) := "" if {
110+
not contains(ref, "@")
111+
}
112+
113+
# Helper to check if date is in the future (simplified - just check it's set)
114+
_is_future_date(date_str) if {
115+
date_str != ""
116+
# Parse and compare - for testing we use a fixed "now" via effective_current_time_ns
117+
# In real usage this would use time.now_ns()
118+
date_ns := time.parse_rfc3339_ns(date_str)
119+
now_ns := _current_time_ns
120+
date_ns > now_ns
121+
}
122+
123+
# Helper to check if date is in the past
124+
_is_past_date(date_str) if {
125+
date_str != ""
126+
date_ns := time.parse_rfc3339_ns(date_str)
127+
now_ns := _current_time_ns
128+
date_ns < now_ns
129+
}
130+
131+
# Get current time - can be overridden in tests
132+
_current_time_ns := time.now_ns()

features/validate_image.feature

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,49 @@ Feature: evaluate enterprise contract
958958
And the output should match the snapshot
959959
And the "${TMPDIR}/input.json" file should match the snapshot
960960

961+
Scenario: volatile config warnings schema contract
962+
Given a key pair named "known"
963+
Given an image named "acceptance/volatile-config-test"
964+
Given a valid image signature of "acceptance/volatile-config-test" image signed by the "known" key
965+
Given a valid attestation of "acceptance/volatile-config-test" signed by the "known" key
966+
Given a git repository named "volatile-config-policy" with
967+
| main.rego | examples/volatile_config_warnings.rego |
968+
Given policy configuration named "ec-policy" with specification
969+
"""
970+
{
971+
"sources": [
972+
{
973+
"name": "volatile-test-source",
974+
"policy": [
975+
"git::https://${GITHOST}/git/volatile-config-policy.git"
976+
],
977+
"volatileConfig": {
978+
"include": [
979+
{
980+
"value": "test.rule_with_no_expiration"
981+
},
982+
{
983+
"value": "test.rule_expiring_soon",
984+
"effectiveUntil": "2099-12-31T23:59:59Z"
985+
},
986+
{
987+
"value": "test.rule_pending_activation",
988+
"effectiveOn": "2099-01-01T00:00:00Z"
989+
},
990+
{
991+
"value": "test.component_scoped_rule",
992+
"componentNames": ["Unnamed"]
993+
}
994+
]
995+
}
996+
}
997+
]
998+
}
999+
"""
1000+
When ec command is run with "validate image --image ${REGISTRY}/acceptance/volatile-config-test --policy acceptance/ec-policy --public-key ${known_PUBLIC_KEY} --ignore-rekor --output json"
1001+
Then the exit status should be 0
1002+
Then the output should match the snapshot
1003+
9611004
Scenario: Unsupported policies
9621005
Given a key pair named "known"
9631006
Given an image named "acceptance/image"

0 commit comments

Comments
 (0)