Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 19 additions & 18 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,26 @@ This document provides guidelines for AI agents working with the RDS Analyzer co

## Project Overview

RDS Analyzer is a Go CLI tool that evaluates evaluates kube-compare JSON reports against a set of rules. It determines the impact of configuration deviations and generates text or HTML reports.
RDS Analyzer is a Go CLI tool and library that evaluates kube-compare JSON reports against a set of rules. It determines the impact of configuration deviations and generates text or HTML reports.

## Architecture

### Package Structure

```
internal/
```text
pkg/ # Public library API - importable by external Go modules
├── analyzer/ # Orchestration - coordinates rule engine and report generation
├── cli/ # Cobra CLI - command parsing and flag handling
├── parser/ # Diff parsing - transforms unified diff to structured data
├── report/ # Output generators - text (terminal) and HTML formats
├── rules/ # Rule engine - pattern matching and impact resolution
└── types/ # Data structures - shared types for validation reports
internal/
└── cli/ # Cobra CLI - command parsing and flag handling (not importable)
```

### Data Flow

1. CLI loads rules YAML (`-r`). For a full run, `analyzer.New` calls `rules.ValidateRulesRegexpPatterns` (which walks the YAML AST with `validateRegexPatternsFromYAML`), then builds `rules.Engine`. With **`--validate-rules-only`**, the CLI calls `rules.ValidateRulesRegexpPatterns` only and exits without building an engine or reading input JSON. On regexp failure, the process exits before reading input.
1. CLI loads rules YAML (`-r`). For a full run, `analyzer.New` calls `rules.ValidateRulesRegexpPatterns` (which walks the YAML AST with `validateRegexPatternsFromYAML`), then builds `rules.Engine`. With **`--validate-rules-only`**, the CLI calls `rules.ValidateRulesRegexpPatterns` only and exits without building an engine or reading input JSON. On regexp failure, the process exits before reading input. For library usage, `analyzer.NewFromBytes` accepts in-memory YAML rules bytes instead of a file path.
2. CLI reads JSON input (file or stdin) into `types.ValidationReport`
3. `analyzer.Analyzer` orchestrates processing:
- Uses the loaded `rules.Engine`
Expand All @@ -34,7 +35,7 @@ internal/

### Regex Validation

All regex patterns in rule files are validated by `rules.ValidateRulesRegexpPatterns` (YAML regexp walk). `analyzer.New` invokes it before constructing the engine for normal analysis. The **`--validate-rules-only`** path runs the same validation from `internal/cli` without loading the full analyzer. This includes:
All regex patterns in rule files are validated by `rules.ValidateRulesRegexpPatterns` (YAML regex walk). `analyzer.New` invokes it before constructing the engine for normal analysis. The **`--validate-rules-only`** path runs the same validation from the CLI without loading the full analyzer. This includes:
- `regex` patterns in condition rules (global_rules, rules)
- `value_regex` patterns in label_annotation_rules

Expand Down Expand Up @@ -92,7 +93,7 @@ import (
"github.com/spf13/cobra"

// Internal packages
"github.com/telco-operations/rds-analyzer/internal/types"
"github.com/openshift-kni/rds-analyzer/pkg/types"
)
```

Expand All @@ -107,35 +108,35 @@ import (

### Adding a New Rule Condition Type

1. Define in `internal/rules/types.go` (add to Condition.Type options)
2. Handle in `internal/rules/engine.go`:
1. Define in `pkg/rules/types.go` (add to Condition.Type options)
2. Handle in `pkg/rules/engine.go`:
- Add case in `evaluateCondition()`
- Implement matching logic
3. **Add tests in `internal/rules/engine_test.go`**:
3. **Add tests in `pkg/rules/engine_test.go`**:
- Add test cases to `TestConditionTypes` for the new condition type
- Add example rules using the new type to `testRulesYAML` constant
- Add realistic scenarios to `TestEvaluate` or `TestEvaluateFromOutputJSON`

### Adding a New Rule Type (e.g., Count Rules, Label Rules)

1. Define types in `internal/rules/types.go`
2. Implement evaluation in `internal/rules/engine.go`
3. **Add comprehensive tests in `internal/rules/engine_test.go`**:
1. Define types in `pkg/rules/types.go`
2. Implement evaluation in `pkg/rules/engine.go`
3. **Add comprehensive tests in `pkg/rules/engine_test.go`**:
- Create a dedicated test function (e.g., `TestEvaluateNewRuleType`)
- Add the new rule type to `testRulesYAML` constant
- Test edge cases (empty input, no match, multiple matches)
- Test impact priority when combined with other rules

### Adding a New Output Format

1. Create `internal/report/newformat.go`
1. Create `pkg/report/newformat.go`
2. Implement generator with `Generate(io.Writer, types.ValidationReport) error`
3. Add case in `internal/analyzer/analyzer.go`
3. Add case in `pkg/analyzer/analyzer.go`
4. Add CLI option in `internal/cli/root.go`

### Modifying the HTML Template

The HTML template is embedded in `internal/report/html.go` as the `htmlTemplate` constant. Edit directly; Go will include it at build time.
The HTML template is embedded in `pkg/report/html.go` as the `htmlTemplate` constant. Edit directly; Go will include it at build time.

## Testing Guidelines

Expand All @@ -146,7 +147,7 @@ The HTML template is embedded in `internal/report/html.go` as the `htmlTemplate`
- Use table-driven tests for multiple cases
- Name test functions `TestFunctionName_Scenario`

### Rules Engine Tests (`internal/rules/engine_test.go`)
### Rules Engine Tests (`pkg/rules/engine_test.go`)

The engine test file contains comprehensive tests for the rule evaluation system. When adding new rules or rule types, update this file:

Expand Down Expand Up @@ -283,7 +284,7 @@ Before submitting changes:
- [ ] Errors are wrapped with context
- [ ] No hardcoded paths or values
- [ ] **New rule types have corresponding tests in `engine_test.go`**
- [ ] **Test coverage remains above 80%** (`go test -cover ./internal/rules/...`)
- [ ] **Test coverage remains above 80%** (`go test -cover ./pkg/rules/...`)

---

Expand Down
1 change: 0 additions & 1 deletion CLAUDE.md

This file was deleted.

1 change: 1 addition & 0 deletions CLAUDE.md
6 changes: 3 additions & 3 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
"io"
"os"

"github.com/openshift-kni/rds-analyzer/internal/analyzer"
"github.com/openshift-kni/rds-analyzer/internal/rules"
"github.com/openshift-kni/rds-analyzer/internal/types"
"github.com/openshift-kni/rds-analyzer/pkg/analyzer"
"github.com/openshift-kni/rds-analyzer/pkg/rules"
"github.com/openshift-kni/rds-analyzer/pkg/types"
"github.com/spf13/cobra"
)

Expand Down
22 changes: 19 additions & 3 deletions internal/analyzer/analyzer.go → pkg/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import (
"fmt"
"io"

"github.com/openshift-kni/rds-analyzer/internal/report"
"github.com/openshift-kni/rds-analyzer/internal/rules"
"github.com/openshift-kni/rds-analyzer/internal/types"
"github.com/openshift-kni/rds-analyzer/pkg/report"
"github.com/openshift-kni/rds-analyzer/pkg/rules"
"github.com/openshift-kni/rds-analyzer/pkg/types"
)

// Analyzer orchestrates the RDS validation analysis.
Expand All @@ -31,6 +31,22 @@ func New(rulesFile string, version string) (*Analyzer, error) {
return &Analyzer{ruleEngine: engine}, nil
}

// NewFromBytes creates a new Analyzer with rules loaded from in-memory YAML bytes.
// This allows creating an analyzer from rules data fetched from a ConfigMap or other
// in-memory source instead of a file path.
// If version is non-empty, rules are evaluated against that OCP version.
func NewFromBytes(rulesData []byte, version string) (*Analyzer, error) {
if err := rules.ValidateRulesRegexpPatternsFromBytes(rulesData, "rules"); err != nil {
return nil, fmt.Errorf("failed to initialize rule engine: %w", err)
}
engine, err := rules.NewEngineFromBytes(rulesData, version)
if err != nil {
return nil, fmt.Errorf("failed to initialize rule engine: %w", err)
}

return &Analyzer{ruleEngine: engine}, nil
}

// Analyze processes a validation report and writes results to the given writer.
// The format parameter determines output type: "text" or "html".
// The mode parameter determines output mode: "simple" or "reporting".
Expand Down
175 changes: 174 additions & 1 deletion internal/analyzer/analyzer_test.go → pkg/analyzer/analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"strings"
"testing"

"github.com/openshift-kni/rds-analyzer/internal/types"
"github.com/openshift-kni/rds-analyzer/pkg/types"
)

// testRulesYAML contains a minimal rules configuration for testing.
Expand Down Expand Up @@ -258,6 +258,179 @@ func TestAnalyze_EmptyReport(t *testing.T) {
}
}

func TestNewFromBytes_TableCases(t *testing.T) {
invalidRegexRules := `
version: "1.0"
settings:
default_impact: "NeedsReview"
rules:
- id: "bad-rule"
match: {}
conditions:
- type: "Any"
regex: "[unclosed"
impact: "Impacting"
comment: "bad regex"
`
tests := []struct {
name string
rulesData []byte
version string
wantErr bool
errContains string
wantVersion string
}{
{
name: "valid rules",
rulesData: []byte(testRulesYAML),
},
{
name: "with version",
rulesData: []byte(testRulesYAML),
version: "4.19",
wantVersion: "4.19",
},
{
name: "invalid YAML",
rulesData: []byte("not: [valid: yaml"),
wantErr: true,
},
{
name: "invalid version",
rulesData: []byte(testRulesYAML),
version: "invalid",
wantErr: true,
},
{
name: "invalid regex",
rulesData: []byte(invalidRegexRules),
wantErr: true,
errContains: "failed to initialize rule engine",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
analyzer, err := NewFromBytes(tt.rulesData, tt.version)
if tt.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("expected error containing %q, got: %v", tt.errContains, err)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if analyzer == nil {
t.Fatal("expected analyzer, got nil")
}
if tt.wantVersion != "" && analyzer.GetTargetVersion() != tt.wantVersion {
t.Errorf("expected version %s, got %s", tt.wantVersion, analyzer.GetTargetVersion())
}
})
}
}

func TestNewFromBytes_ProducesSameOutputAsNew(t *testing.T) {
rulesFile := createTestRulesFile(t)

fileAnalyzer, err := New(rulesFile, "4.20")
if err != nil {
t.Fatalf("Failed to create file analyzer: %v", err)
}

bytesAnalyzer, err := NewFromBytes([]byte(testRulesYAML), "4.20")
if err != nil {
t.Fatalf("Failed to create bytes analyzer: %v", err)
}

report := types.ValidationReport{
Summary: types.Summary{
NumDiffCRs: 1,
TotalCRs: 5,
},
Diffs: []types.Diff{
{
DiffOutput: "- name: test\n+ name: changed",
CorrelatedTemplate: "test/TestCR.yaml",
CRName: "v1_ConfigMap_default_test",
},
},
}

var fileBuf, bytesBuf bytes.Buffer
if err := fileAnalyzer.Analyze(&fileBuf, report, "text", "simple"); err != nil {
t.Fatalf("File analyzer failed: %v", err)
}
if err := bytesAnalyzer.Analyze(&bytesBuf, report, "text", "simple"); err != nil {
t.Fatalf("Bytes analyzer failed: %v", err)
}

if fileBuf.String() != bytesBuf.String() {
t.Error("file-based and bytes-based analyzers produced different output")
}
}

func TestNewFromBytes_AnalyzeModes(t *testing.T) {
tests := []struct {
name string
format string
mode string
checkOutput func(t *testing.T, output string)
}{
{
name: "HTML format",
format: "html",
mode: "simple",
checkOutput: func(t *testing.T, output string) {
if !strings.Contains(output, "<!DOCTYPE html>") {
t.Error("expected HTML output")
}
},
},
{
name: "reporting mode",
format: "text",
mode: "reporting",
checkOutput: func(t *testing.T, output string) {
if output == "" {
t.Error("expected non-empty output for reporting mode")
}
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
analyzer, err := NewFromBytes([]byte(testRulesYAML), "")
if err != nil {
t.Fatalf("Failed to create analyzer: %v", err)
}

report := types.ValidationReport{
Summary: types.Summary{NumDiffCRs: 1, TotalCRs: 5},
Diffs: []types.Diff{
{
DiffOutput: "- value: old\n+ value: new",
CorrelatedTemplate: "test/TestCR.yaml",
CRName: "v1_ConfigMap_default_test",
},
},
}

var buf bytes.Buffer
if err := analyzer.Analyze(&buf, report, tt.format, tt.mode); err != nil {
t.Fatalf("Analyze failed: %v", err)
}

tt.checkOutput(t, buf.String())
})
}
}

func TestAnalyze_WithMissingCRs(t *testing.T) {
rulesFile := createTestRulesFile(t)
a, err := New(rulesFile, "")
Expand Down
2 changes: 1 addition & 1 deletion internal/parser/diff.go → pkg/parser/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ package parser
import (
"strings"

"github.com/openshift-kni/rds-analyzer/internal/types"
"github.com/openshift-kni/rds-analyzer/pkg/types"
)

// ANSI color codes for terminal output formatting.
Expand Down
2 changes: 1 addition & 1 deletion internal/parser/diff_test.go → pkg/parser/diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package parser
import (
"testing"

"github.com/openshift-kni/rds-analyzer/internal/types"
"github.com/openshift-kni/rds-analyzer/pkg/types"
)

func TestParseKeyValue(t *testing.T) {
Expand Down
6 changes: 3 additions & 3 deletions internal/report/html.go → pkg/report/html.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (
"strings"
"time"

"github.com/openshift-kni/rds-analyzer/internal/parser"
"github.com/openshift-kni/rds-analyzer/internal/rules"
"github.com/openshift-kni/rds-analyzer/internal/types"
"github.com/openshift-kni/rds-analyzer/pkg/parser"
"github.com/openshift-kni/rds-analyzer/pkg/rules"
"github.com/openshift-kni/rds-analyzer/pkg/types"
)

// escapeHTML escapes characters that could break HTML structure.
Expand Down
4 changes: 2 additions & 2 deletions internal/report/html_test.go → pkg/report/html_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (
"strings"
"testing"

"github.com/openshift-kni/rds-analyzer/internal/rules"
"github.com/openshift-kni/rds-analyzer/internal/types"
"github.com/openshift-kni/rds-analyzer/pkg/rules"
"github.com/openshift-kni/rds-analyzer/pkg/types"
)

const testHTMLRulesYAML = `
Expand Down
Loading
Loading