mirror of
https://github.com/lukaszraczylo/kubemirror.git
synced 2026-06-30 06:55:10 +00:00
improvements jan2025 (#6)
* feat(controller): add lazy watcher, improve resource usage and add pattern validation - [x] Add cache sync health check for readiness probe verification - [x] Create namespace lister with API reader support for fresh label queries - [x] Add pattern validation with warning logs for invalid glob patterns - [x] Implement lazy watcher initialization mode to scan for active resources - [x] Add requeue delay to namespace reconciler for cache settlement - [x] Replace custom containsString with slices.Contains from stdlib - [x] Add structured logging context to reconcilers (kind, group, version) - [x] Improve error variable naming for clarity in nested conditions - [x] Add nil-safe label access in namespace reconciler setup - [x] Add APIReader to namespace and source reconcilers for fresh data - [x] Improve type assertions with proper error handling in mirror operations - [x] Reorder struct fields for consistency and readability - [x] Add comprehensive pattern validation tests and validation API * feat(controller): add lazy watcher, improve resource usage and add pattern validation - [x] Add circuit breaker for reconciliation failure tracking and prevention - [x] Implement granular registration state tracking (not-registered, source-only, fully-registered) - [x] Add lazy controller initialization for active resource types only - [x] Consolidate namespace listing into single API call for efficiency - [x] Add mirror creation verification to catch webhook rejections - [x] Implement high-cardinality resource detection and warnings - [x] Add source deletion check in mirror reconciler to prevent races - [x] Preserve transformation annotations on errors in mirror reconciliation - [x] Expand constants documentation with labels vs annotations design rationale - [x] Add comprehensive test coverage for circuit breaker and registration states - [x] Add mutation-safety tests for hash computation * fixup! feat(controller): add lazy watcher, improve resource usage and add pattern validation
This commit is contained in:
@@ -2,12 +2,79 @@
|
||||
package filter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/lukaszraczylo/kubemirror/pkg/constants"
|
||||
)
|
||||
|
||||
// PatternValidationResult contains the result of validating a pattern.
|
||||
type PatternValidationResult struct {
|
||||
Error error
|
||||
Pattern string
|
||||
Valid bool
|
||||
}
|
||||
|
||||
// ValidatePattern checks if a glob pattern is syntactically valid.
|
||||
// Returns an error if the pattern cannot be compiled by filepath.Match.
|
||||
func ValidatePattern(pattern string) error {
|
||||
// Empty pattern is invalid
|
||||
if pattern == "" {
|
||||
return fmt.Errorf("empty pattern")
|
||||
}
|
||||
|
||||
// Special keywords are always valid
|
||||
if pattern == constants.TargetNamespacesAll || pattern == constants.TargetNamespacesAllLabeled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use filepath.Match with a test string to validate pattern syntax
|
||||
// We use "test" as a dummy value - we only care about the error
|
||||
_, err := filepath.Match(pattern, "test")
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid glob pattern %q: %w", pattern, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePatterns validates a list of patterns and returns results for each.
|
||||
// Returns a slice of validation results and a boolean indicating if all patterns are valid.
|
||||
func ValidatePatterns(patterns []string) ([]PatternValidationResult, bool) {
|
||||
if len(patterns) == 0 {
|
||||
return nil, true
|
||||
}
|
||||
|
||||
results := make([]PatternValidationResult, len(patterns))
|
||||
allValid := true
|
||||
|
||||
for i, pattern := range patterns {
|
||||
err := ValidatePattern(pattern)
|
||||
results[i] = PatternValidationResult{
|
||||
Pattern: pattern,
|
||||
Valid: err == nil,
|
||||
Error: err,
|
||||
}
|
||||
if err != nil {
|
||||
allValid = false
|
||||
}
|
||||
}
|
||||
|
||||
return results, allValid
|
||||
}
|
||||
|
||||
// InvalidPatterns returns only the invalid patterns from a validation result.
|
||||
func InvalidPatterns(results []PatternValidationResult) []PatternValidationResult {
|
||||
var invalid []PatternValidationResult
|
||||
for _, r := range results {
|
||||
if !r.Valid {
|
||||
invalid = append(invalid, r)
|
||||
}
|
||||
}
|
||||
return invalid
|
||||
}
|
||||
|
||||
// NamespaceFilter handles namespace filtering logic including patterns and exclusions.
|
||||
type NamespaceFilter struct {
|
||||
excludedNamespaces map[string]bool
|
||||
|
||||
@@ -592,3 +592,167 @@ func BenchmarkResolveTargetNamespaces_LargeScale(b *testing.B) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Tests for pattern validation
|
||||
|
||||
func TestValidatePattern(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pattern string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid simple pattern",
|
||||
pattern: "app-*",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid complex pattern",
|
||||
pattern: "*-app-*-db",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid exact match",
|
||||
pattern: "my-namespace",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid question mark pattern",
|
||||
pattern: "app-?",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid character class pattern",
|
||||
pattern: "app-[abc]",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid 'all' keyword",
|
||||
pattern: constants.TargetNamespacesAll,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid 'all-labeled' keyword",
|
||||
pattern: constants.TargetNamespacesAllLabeled,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid unclosed bracket",
|
||||
pattern: "app-[",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "character range pattern is valid",
|
||||
pattern: "app-[z-a]",
|
||||
wantErr: false, // filepath.Match accepts character ranges
|
||||
},
|
||||
{
|
||||
name: "empty pattern is invalid",
|
||||
pattern: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidatePattern(tt.pattern)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err, "expected error for pattern %q", tt.pattern)
|
||||
} else {
|
||||
assert.NoError(t, err, "unexpected error for pattern %q", tt.pattern)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePatterns(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
patterns []string
|
||||
wantAllValid bool
|
||||
wantInvalid int
|
||||
}{
|
||||
{
|
||||
name: "all valid patterns",
|
||||
patterns: []string{"app-*", "prod-*", "staging-db"},
|
||||
wantAllValid: true,
|
||||
wantInvalid: 0,
|
||||
},
|
||||
{
|
||||
name: "empty patterns list",
|
||||
patterns: []string{},
|
||||
wantAllValid: true,
|
||||
wantInvalid: 0,
|
||||
},
|
||||
{
|
||||
name: "one invalid pattern",
|
||||
patterns: []string{"app-*", "invalid-[", "prod-*"},
|
||||
wantAllValid: false,
|
||||
wantInvalid: 1,
|
||||
},
|
||||
{
|
||||
name: "multiple invalid patterns",
|
||||
patterns: []string{"invalid-[", "app-*", "bad-["},
|
||||
wantAllValid: false,
|
||||
wantInvalid: 2,
|
||||
},
|
||||
{
|
||||
name: "all invalid patterns",
|
||||
patterns: []string{"bad-[", "worse-["},
|
||||
wantAllValid: false,
|
||||
wantInvalid: 2,
|
||||
},
|
||||
{
|
||||
name: "mixed with keywords",
|
||||
patterns: []string{constants.TargetNamespacesAll, "bad-[", "app-*"},
|
||||
wantAllValid: false,
|
||||
wantInvalid: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
results, allValid := ValidatePatterns(tt.patterns)
|
||||
assert.Equal(t, tt.wantAllValid, allValid, "allValid mismatch")
|
||||
|
||||
invalidPatterns := InvalidPatterns(results)
|
||||
assert.Equal(t, tt.wantInvalid, len(invalidPatterns), "invalid count mismatch")
|
||||
|
||||
// Verify all invalid patterns have errors
|
||||
for _, invalid := range invalidPatterns {
|
||||
assert.False(t, invalid.Valid)
|
||||
assert.NotNil(t, invalid.Error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidPatterns(t *testing.T) {
|
||||
t.Run("filters only invalid patterns", func(t *testing.T) {
|
||||
results := []PatternValidationResult{
|
||||
{Pattern: "app-*", Valid: true, Error: nil},
|
||||
{Pattern: "bad-[", Valid: false, Error: fmt.Errorf("invalid")},
|
||||
{Pattern: "prod-*", Valid: true, Error: nil},
|
||||
{Pattern: "worse-[", Valid: false, Error: fmt.Errorf("invalid")},
|
||||
}
|
||||
|
||||
invalid := InvalidPatterns(results)
|
||||
assert.Len(t, invalid, 2)
|
||||
assert.Equal(t, "bad-[", invalid[0].Pattern)
|
||||
assert.Equal(t, "worse-[", invalid[1].Pattern)
|
||||
})
|
||||
|
||||
t.Run("returns nil for empty input", func(t *testing.T) {
|
||||
invalid := InvalidPatterns(nil)
|
||||
assert.Nil(t, invalid)
|
||||
})
|
||||
|
||||
t.Run("returns nil for all valid patterns", func(t *testing.T) {
|
||||
results := []PatternValidationResult{
|
||||
{Pattern: "app-*", Valid: true, Error: nil},
|
||||
{Pattern: "prod-*", Valid: true, Error: nil},
|
||||
}
|
||||
invalid := InvalidPatterns(results)
|
||||
assert.Nil(t, invalid)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user