Skip to content

Commit 240097e

Browse files
authored
INS-1595: Fix kyverno policies validate (#289)
* Fix test cases] * Fixes * Fixes * FIx * Fixing * Fix * Fix * Code cleanup * Fix
1 parent 274fd32 commit 240097e

4 files changed

Lines changed: 183 additions & 67 deletions

File tree

pkg/cli/validate_kyverno_policies.go

Lines changed: 151 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,11 @@ var validateKyvernoPoliciesCmd = &cobra.Command{
9898

9999
displayValidationResults(result, []kyverno.TestResource{testResource})
100100
if !determineActualValidationResult(result, []kyverno.TestResource{testResource}) {
101+
fmt.Println("❌ Kyverno policy validation failed.")
101102
os.Exit(1)
102103
}
103-
fmt.Println("Kyverno policy validated successfully.")
104+
fmt.Println("✅ Kyverno policy validated successfully.")
105+
return
104106
}
105107

106108
if kyvernoPolicyDir != "" {
@@ -128,20 +130,20 @@ var validateKyvernoPoliciesCmd = &cobra.Command{
128130
}
129131

130132
if len(policiesToValidate) == 0 {
131-
fmt.Println("No policies to validate")
132-
return
133+
fmt.Println("No policies to validate")
134+
os.Exit(1)
133135
}
134136

135137
// Validate each policy
136138
allValid := true
137139
for _, policyWithTestCases := range policiesToValidate {
138-
fmt.Printf("Validating policy: %s", policyWithTestCases.Policy.Name)
139-
140+
fmt.Println("\n--------------------------------")
141+
fmt.Printf("🔍 Validating policy: %s\n", policyWithTestCases.Policy.Name)
140142
result, err := kyverno.ValidateKyvernoPolicy(
141143
client, org, policyWithTestCases.Policy, policyWithTestCases.TestCases, true)
142144
if err != nil {
143-
fmt.Printf("Unable to validate policy %s: %v", policyWithTestCases.Policy.Name, err)
144145
allValid = false
146+
fmt.Printf("❌ Unable to validate policy %s: %v\n", policyWithTestCases.Policy.Name, err)
145147
continue
146148
}
147149

@@ -152,10 +154,12 @@ var validateKyvernoPoliciesCmd = &cobra.Command{
152154
}
153155

154156
if !allValid {
157+
fmt.Println("\n--------------------------------")
158+
fmt.Println("❌ Some Kyverno policies validation failed. Please check the output for details.")
155159
os.Exit(1)
156160
}
157-
158-
fmt.Println("All Kyverno policies validated successfully!")
161+
fmt.Println("✅ All Kyverno policies validated successfully!")
162+
return
159163
}
160164
},
161165
}
@@ -188,76 +192,155 @@ func displayValidationResults(result *kyverno.ValidationResult, testCases []kyve
188192
// Determine if validation actually passed based on test case results
189193
actualValid := determineActualValidationResult(result, testCases)
190194

191-
if actualValid {
192-
fmt.Printf("✅ Policy validation: PASSED\n")
193-
} else {
194-
fmt.Printf("❌ Policy validation: FAILED\n")
195+
// Display test case results with clear messaging
196+
if len(result.TestResults) > 0 {
197+
fmt.Printf("\n📋 Test case results:\n")
198+
for _, testResult := range result.TestResults {
199+
passed := testResult.Passed
200+
201+
// Display the result with test case name prominently
202+
if passed {
203+
fmt.Printf(" ✅ %s (%s)\n", testResult.TestCaseName, testResult.FileName)
204+
} else {
205+
fmt.Printf(" ❌ %s (%s)\n", testResult.TestCaseName, testResult.FileName)
206+
}
207+
}
208+
}
209+
210+
// If TestResults are empty but we have test cases, show them with fallback info
211+
if len(result.TestResults) == 0 && len(testCases) > 0 {
212+
fmt.Printf("\n📋 Test cases:\n")
213+
214+
// Check if we have mixed test cases (both success and failure)
215+
successCount := 0
216+
failureCount := 0
217+
for _, tc := range testCases {
218+
switch tc.ExpectedOutcome {
219+
case "success":
220+
successCount++
221+
case "failure":
222+
failureCount++
223+
default:
224+
continue
225+
}
226+
}
227+
hasMixedCases := successCount > 0 && failureCount > 0
228+
229+
for _, testCase := range testCases {
230+
var passed bool
231+
if hasMixedCases {
232+
passed = actualValid
233+
} else {
234+
switch testCase.ExpectedOutcome {
235+
case "success":
236+
passed = len(result.Errors) == 0
237+
case "failure":
238+
passed = len(result.Errors) > 0
239+
default:
240+
passed = actualValid
241+
}
242+
}
243+
244+
if passed {
245+
fmt.Printf(" ✅ %s (%s)\n", testCase.TestCaseName, testCase.FileName)
246+
} else {
247+
fmt.Printf(" ❌ %s (%s)\n", testCase.TestCaseName, testCase.FileName)
248+
}
249+
}
195250
}
196251

197-
// Display errors if any
198252
if len(result.Errors) > 0 {
199-
fmt.Printf("❌ Errors:\n")
253+
fmt.Printf("\n❌ Output:\n")
200254
for _, err := range result.Errors {
201255
fmt.Printf(" - %s\n", err)
202256
}
203257
}
204258

205-
// Display warnings if any
206259
if len(result.Warnings) > 0 {
207-
fmt.Printf("⚠️ Warnings:\n")
260+
fmt.Printf("\n⚠️ Output:\n")
208261
for _, warning := range result.Warnings {
209262
fmt.Printf(" - %s\n", warning)
210263
}
211264
}
265+
}
266+
267+
// matchTestResultsToTestCases matches TestResults to TestCases using FileName and TestCaseName
268+
// Returns a map of test case index to its TestResult (if found)
269+
// Uses FileName:TestCaseName as the unique identifier to ensure correct matching
270+
// Handles both patterns:
271+
// - "policy.testcase1.success.yaml" -> TestCaseName: "testcase1.success", FileName: "policy.testcase1.success.yaml"
272+
// - "policy.success.yaml" -> TestCaseName: "success", FileName: "policy.success.yaml"
273+
func matchTestResultsToTestCases(result *kyverno.ValidationResult, testCases []kyverno.TestResource) map[int]*kyverno.TestResult {
274+
resultMap := make(map[int]*kyverno.TestResult)
275+
276+
// Create a map of test cases by FileName and TestCaseName for quick lookup
277+
testCaseMap := make(map[string]int)
278+
for i, testCase := range testCases {
279+
// Use FileName and TestCaseName as the unique identifier
280+
// This handles both patterns:
281+
// - "policy.testcase1.success.yaml" with TestCaseName "testcase1.success"
282+
// - "policy.success.yaml" with TestCaseName "success"
283+
key := fmt.Sprintf("%s:%s", testCase.FileName, testCase.TestCaseName)
284+
testCaseMap[key] = i
285+
}
212286

213-
// Display test case results
287+
// Match TestResults to test cases using FileName and TestCaseName
214288
for _, testResult := range result.TestResults {
215-
if testResult.Passed {
216-
fmt.Printf("✓ Test case %s (%s): PASSED - Expected %s, got %s\n",
217-
testResult.TestCaseName, testResult.FileName,
218-
testResult.ExpectedOutcome, testResult.ActualOutcome)
219-
} else {
220-
fmt.Printf("❌ Test case %s (%s): FAILED - Expected %s, got %s\n",
221-
testResult.TestCaseName, testResult.FileName,
222-
testResult.ExpectedOutcome, testResult.ActualOutcome)
289+
if testResult.FileName != "" && testResult.TestCaseName != "" {
290+
key := fmt.Sprintf("%s:%s", testResult.FileName, testResult.TestCaseName)
291+
if idx, exists := testCaseMap[key]; exists {
292+
resultMap[idx] = &testResult
293+
}
223294
}
224295
}
296+
297+
return resultMap
225298
}
226299

227300
// determineActualValidationResult determines if validation actually passed based on test case results
301+
// A test case passes if:
302+
// - .success.yaml file: Expected "success" and actual "success" (policy allows good resource) → PASS
303+
// - .failure.yaml file: Expected "failure" and actual "failure" (policy rejects bad resource) → PASS
304+
//
305+
// A test case fails if:
306+
// - .success.yaml file: Expected "success" but actual "failure" (policy incorrectly rejects good resource) → FAIL
307+
// - .failure.yaml file: Expected "failure" but actual "success" (policy incorrectly allows bad resource) → FAIL
228308
func determineActualValidationResult(result *kyverno.ValidationResult, testCases []kyverno.TestResource) bool {
229-
// If there are test results from backend, use them
309+
// If we have TestResults, use them to match test cases by FileName and TestCaseName
230310
if len(result.TestResults) > 0 {
231-
// Create a map of test case names to expected outcomes for quick lookup
232-
expectedOutcomes := make(map[string]string)
233-
for _, testCase := range testCases {
234-
expectedOutcomes[testCase.TestCaseName] = testCase.ExpectedOutcome
235-
}
236-
237-
// Check each test result to see if it behaved as expected
238-
for _, testResult := range result.TestResults {
239-
expectedOutcome, exists := expectedOutcomes[testResult.TestCaseName]
240-
if !exists {
241-
// If we can't find the expected outcome, fall back to the test result's expected outcome
242-
expectedOutcome = testResult.ExpectedOutcome
243-
}
244-
245-
// A test case passes if:
246-
// 1. Expected "success" and actual "success" (policy allows good resource)
247-
// 2. Expected "failure" and actual "failure" (policy rejects bad resource)
248-
expectedSuccess := expectedOutcome == "success"
249-
actualSuccess := testResult.ActualOutcome == "success"
311+
resultMap := matchTestResultsToTestCases(result, testCases)
312+
313+
// Check each test case individually
314+
allPassed := true
315+
for i, testCase := range testCases {
316+
testResult, hasResult := resultMap[i]
317+
318+
if hasResult {
319+
// We have a TestResult for this test case - use it directly
320+
// A test case passes if expected outcome matches actual outcome
321+
expectedSuccess := testCase.ExpectedOutcome == "success"
322+
actualSuccess := testResult.ActualOutcome == "success"
323+
324+
// Test case fails if expected outcome doesn't match actual outcome
325+
if expectedSuccess != actualSuccess {
326+
allPassed = false
327+
}
250328

251-
// Test case passes if expected outcome matches actual outcome
252-
if expectedSuccess != actualSuccess {
253-
return false
329+
// Also check the Passed field if available
330+
if !testResult.Passed {
331+
allPassed = false
332+
}
333+
} else {
334+
// No TestResult for this test case - we can't determine if it passed
335+
// This should not happen if the backend is working correctly
336+
// Fail validation if we can't find a result for a test case
337+
allPassed = false
254338
}
255339
}
256340

257-
return true
341+
return allPassed
258342
}
259343

260-
// Count test case types
261344
successTestCases := 0
262345
failureTestCases := 0
263346
for _, testCase := range testCases {
@@ -271,11 +354,24 @@ func determineActualValidationResult(result *kyverno.ValidationResult, testCases
271354
}
272355
}
273356

274-
// Simple logic:
275-
// - If we have SUCCESS test cases and NO errors → PASS (policy allows good resources)
276-
// - If we have FAILURE test cases and HAVE errors → PASS (policy rejects bad resources)
277-
// - If we have SUCCESS test cases and HAVE errors → FAIL (policy incorrectly rejects good resources)
278-
// - If we have FAILURE test cases and NO errors → FAIL (policy incorrectly allows bad resources)
357+
// Simple fallback logic:
358+
// - If we have ONLY SUCCESS test cases and NO errors → PASS
359+
// - If we have ONLY FAILURE test cases and HAVE errors → PASS
360+
// - If we have ONLY SUCCESS test cases and HAVE errors → FAIL
361+
// - If we have ONLY FAILURE test cases and NO errors → FAIL
362+
// - If we have MIXED test cases → Cannot reliably determine without TestResults
363+
364+
if successTestCases > 0 && failureTestCases > 0 {
365+
// Mixed test cases: we can't reliably determine which errors belong to which test case
366+
// Without TestResults, we cannot properly validate mixed cases
367+
// If we have errors, assume failure test cases failed (expected) and success test cases passed (expected)
368+
if len(result.Errors) > 0 {
369+
return true
370+
} else {
371+
// No errors means failure test cases passed when they should have failed
372+
return false
373+
}
374+
}
279375

280376
if successTestCases > 0 && len(result.Errors) == 0 {
281377
return true

pkg/kyverno/kyverno.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,13 +210,12 @@ func validatePath(path string) error {
210210
}
211211

212212
func isPolicyFile(filename string) bool {
213-
// Policy file: ends with .yaml/.yml but doesn't contain .testcase
214213
return (strings.HasSuffix(filename, ".yaml") || strings.HasSuffix(filename, ".yml")) &&
215-
!strings.Contains(filename, ".testcase")
214+
!strings.Contains(filename, ".success.") && !strings.Contains(filename, ".failure.")
216215
}
217216

218217
func isTestCaseFile(filename string) bool {
219-
return strings.Contains(filename, ".testcase")
218+
return strings.Contains(filename, ".success.") || strings.Contains(filename, ".failure.")
220219
}
221220

222221
func extractPolicyNameFromFile(filename string) string {
@@ -237,10 +236,28 @@ func extractPolicyNameFromTestCase(filename string) string {
237236

238237
func extractTestCaseName(filename string) string {
239238
// Extract test case name from filename
240-
// e.g., "require-labels.testcase1.success.yaml" -> "testcase1"
239+
// Patterns:
240+
// - "require-labels.testcase1.success.yaml" -> "testcase1.success"
241+
// - "require-labels.success.yaml" -> "success"
242+
// - "require-labels.testcase2.failure.yaml" -> "testcase2.failure"
243+
// - "require-labels.failure.yaml" -> "failure"
241244
parts := strings.Split(filename, ".")
242-
for _, part := range parts {
243-
if strings.HasPrefix(part, "testcase") {
245+
246+
// Find the index of "success" or "failure"
247+
for i, part := range parts {
248+
if part == "success" || part == "failure" {
249+
// If there's a part before "success"/"failure", that's the test case name prefix
250+
if i > 0 {
251+
// Check if the previous part looks like a test case name
252+
// (not the policy name, which is typically the first part)
253+
prevPart := parts[i-1]
254+
// If previous part is not empty and not just the base filename, combine it with outcome
255+
if prevPart != "" && i > 1 {
256+
// Return "testcase1.success" or "testcase2.failure"
257+
return fmt.Sprintf("%s.%s", prevPart, part)
258+
}
259+
}
260+
// If no test case name prefix, return "success" or "failure"
244261
return part
245262
}
246263
}

pkg/kyverno/kyverno_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,12 @@ func TestExtractTestCaseName(t *testing.T) {
103103
filename string
104104
expected string
105105
}{
106-
{"require-labels.testcase1.success.yaml", "testcase1"},
107-
{"disallow-privileged.testcase2.failure.yaml", "testcase2"},
108-
{"policy.testcase3.success.yml", "testcase3"},
106+
{"require-labels.testcase1.success.yaml", "testcase1.success"},
107+
{"disallow-privileged.testcase2.failure.yaml", "testcase2.failure"},
108+
{"policy.testcase3.success.yml", "testcase3.success"},
109+
{"require-labels.success.yaml", "success"},
110+
{"disallow-privileged.failure.yaml", "failure"},
111+
{"policy.success.yml", "success"},
109112
}
110113

111114
for _, test := range tests {

testdata/scripts/validate_kyverno_policies.txtar

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ cp disallow-privileged.testcase2.success.yaml test-dir/kyverno-policies/
1919
# Test batch validation (expects success since policies correctly handle test cases)
2020
exec echo "DEBUG: Testing batch validation..."
2121
exec sh -c 'insights-cli validate kyverno-policies -b test-dir/kyverno-policies/ 2>&1 || true'
22-
stdout 'Policy validation: PASSED'
23-
stdout 'All Kyverno policies validated successfully!'
22+
stdout '📋 Test cases:'
23+
stdout 'All Kyverno policies validated successfully!'
2424

2525
# Test single policy validation (expects success with valid test case)
2626
exec echo "DEBUG: Testing single policy validation..."
2727
exec insights-cli validate kyverno-policies -r test-dir/kyverno-policies/require-labels.yaml -k test-dir/kyverno-policies/require-labels.testcase1.success.yaml
28-
stdout 'Kyverno policy validated successfully.'
28+
stdout 'Kyverno policy validated successfully.'
2929
! stderr .
3030
exec echo "DEBUG: All validation tests completed"
3131
exec echo "DEBUG: If any test failed, the actual stderr content should be shown above"

0 commit comments

Comments
 (0)