Skip to content

Commit 0d7ab1b

Browse files
mvazquezcclaude
andauthored
Prepares the tool for integration with kubecompare MCP (openshift-kni#12)
* Prepares the tool for integration with kubecompare MCP by allowing external use of the required methods. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Mario Vazquez <mavazque@redhat.com> * Added RabbitCI suggestions Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Mario Vazquez <mavazque@redhat.com> * claude.md is a link to agents.md now Signed-off-by: Mario Vazquez <mavazque@redhat.com> --------- Signed-off-by: Mario Vazquez <mavazque@redhat.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6e3b131 commit 0d7ab1b

20 files changed

Lines changed: 394 additions & 50 deletions

AGENTS.md

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,26 @@ This document provides guidelines for AI agents working with the RDS Analyzer co
44

55
## Project Overview
66

7-
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.
7+
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.
88

99
## Architecture
1010

1111
### Package Structure
1212

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

2324
### Data Flow
2425

25-
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.
26+
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.
2627
2. CLI reads JSON input (file or stdin) into `types.ValidationReport`
2728
3. `analyzer.Analyzer` orchestrates processing:
2829
- Uses the loaded `rules.Engine`
@@ -34,7 +35,7 @@ internal/
3435

3536
### Regex Validation
3637

37-
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:
38+
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:
3839
- `regex` patterns in condition rules (global_rules, rules)
3940
- `value_regex` patterns in label_annotation_rules
4041

@@ -92,7 +93,7 @@ import (
9293
"github.com/spf13/cobra"
9394

9495
// Internal packages
95-
"github.com/telco-operations/rds-analyzer/internal/types"
96+
"github.com/openshift-kni/rds-analyzer/pkg/types"
9697
)
9798
```
9899

@@ -107,35 +108,35 @@ import (
107108

108109
### Adding a New Rule Condition Type
109110

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

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

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

129130
### Adding a New Output Format
130131

131-
1. Create `internal/report/newformat.go`
132+
1. Create `pkg/report/newformat.go`
132133
2. Implement generator with `Generate(io.Writer, types.ValidationReport) error`
133-
3. Add case in `internal/analyzer/analyzer.go`
134+
3. Add case in `pkg/analyzer/analyzer.go`
134135
4. Add CLI option in `internal/cli/root.go`
135136

136137
### Modifying the HTML Template
137138

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

140141
## Testing Guidelines
141142

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

149-
### Rules Engine Tests (`internal/rules/engine_test.go`)
150+
### Rules Engine Tests (`pkg/rules/engine_test.go`)
150151

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

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

288289
---
289290

CLAUDE.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md

internal/cli/root.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import (
77
"io"
88
"os"
99

10-
"github.com/openshift-kni/rds-analyzer/internal/analyzer"
11-
"github.com/openshift-kni/rds-analyzer/internal/rules"
12-
"github.com/openshift-kni/rds-analyzer/internal/types"
10+
"github.com/openshift-kni/rds-analyzer/pkg/analyzer"
11+
"github.com/openshift-kni/rds-analyzer/pkg/rules"
12+
"github.com/openshift-kni/rds-analyzer/pkg/types"
1313
"github.com/spf13/cobra"
1414
)
1515

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import (
66
"fmt"
77
"io"
88

9-
"github.com/openshift-kni/rds-analyzer/internal/report"
10-
"github.com/openshift-kni/rds-analyzer/internal/rules"
11-
"github.com/openshift-kni/rds-analyzer/internal/types"
9+
"github.com/openshift-kni/rds-analyzer/pkg/report"
10+
"github.com/openshift-kni/rds-analyzer/pkg/rules"
11+
"github.com/openshift-kni/rds-analyzer/pkg/types"
1212
)
1313

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

34+
// NewFromBytes creates a new Analyzer with rules loaded from in-memory YAML bytes.
35+
// This allows creating an analyzer from rules data fetched from a ConfigMap or other
36+
// in-memory source instead of a file path.
37+
// If version is non-empty, rules are evaluated against that OCP version.
38+
func NewFromBytes(rulesData []byte, version string) (*Analyzer, error) {
39+
if err := rules.ValidateRulesRegexpPatternsFromBytes(rulesData, "rules"); err != nil {
40+
return nil, fmt.Errorf("failed to initialize rule engine: %w", err)
41+
}
42+
engine, err := rules.NewEngineFromBytes(rulesData, version)
43+
if err != nil {
44+
return nil, fmt.Errorf("failed to initialize rule engine: %w", err)
45+
}
46+
47+
return &Analyzer{ruleEngine: engine}, nil
48+
}
49+
3450
// Analyze processes a validation report and writes results to the given writer.
3551
// The format parameter determines output type: "text" or "html".
3652
// The mode parameter determines output mode: "simple" or "reporting".
Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"strings"
88
"testing"
99

10-
"github.com/openshift-kni/rds-analyzer/internal/types"
10+
"github.com/openshift-kni/rds-analyzer/pkg/types"
1111
)
1212

1313
// testRulesYAML contains a minimal rules configuration for testing.
@@ -258,6 +258,179 @@ func TestAnalyze_EmptyReport(t *testing.T) {
258258
}
259259
}
260260

261+
func TestNewFromBytes_TableCases(t *testing.T) {
262+
invalidRegexRules := `
263+
version: "1.0"
264+
settings:
265+
default_impact: "NeedsReview"
266+
rules:
267+
- id: "bad-rule"
268+
match: {}
269+
conditions:
270+
- type: "Any"
271+
regex: "[unclosed"
272+
impact: "Impacting"
273+
comment: "bad regex"
274+
`
275+
tests := []struct {
276+
name string
277+
rulesData []byte
278+
version string
279+
wantErr bool
280+
errContains string
281+
wantVersion string
282+
}{
283+
{
284+
name: "valid rules",
285+
rulesData: []byte(testRulesYAML),
286+
},
287+
{
288+
name: "with version",
289+
rulesData: []byte(testRulesYAML),
290+
version: "4.19",
291+
wantVersion: "4.19",
292+
},
293+
{
294+
name: "invalid YAML",
295+
rulesData: []byte("not: [valid: yaml"),
296+
wantErr: true,
297+
},
298+
{
299+
name: "invalid version",
300+
rulesData: []byte(testRulesYAML),
301+
version: "invalid",
302+
wantErr: true,
303+
},
304+
{
305+
name: "invalid regex",
306+
rulesData: []byte(invalidRegexRules),
307+
wantErr: true,
308+
errContains: "failed to initialize rule engine",
309+
},
310+
}
311+
312+
for _, tt := range tests {
313+
t.Run(tt.name, func(t *testing.T) {
314+
analyzer, err := NewFromBytes(tt.rulesData, tt.version)
315+
if tt.wantErr {
316+
if err == nil {
317+
t.Fatal("expected error, got nil")
318+
}
319+
if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
320+
t.Errorf("expected error containing %q, got: %v", tt.errContains, err)
321+
}
322+
return
323+
}
324+
if err != nil {
325+
t.Fatalf("unexpected error: %v", err)
326+
}
327+
if analyzer == nil {
328+
t.Fatal("expected analyzer, got nil")
329+
}
330+
if tt.wantVersion != "" && analyzer.GetTargetVersion() != tt.wantVersion {
331+
t.Errorf("expected version %s, got %s", tt.wantVersion, analyzer.GetTargetVersion())
332+
}
333+
})
334+
}
335+
}
336+
337+
func TestNewFromBytes_ProducesSameOutputAsNew(t *testing.T) {
338+
rulesFile := createTestRulesFile(t)
339+
340+
fileAnalyzer, err := New(rulesFile, "4.20")
341+
if err != nil {
342+
t.Fatalf("Failed to create file analyzer: %v", err)
343+
}
344+
345+
bytesAnalyzer, err := NewFromBytes([]byte(testRulesYAML), "4.20")
346+
if err != nil {
347+
t.Fatalf("Failed to create bytes analyzer: %v", err)
348+
}
349+
350+
report := types.ValidationReport{
351+
Summary: types.Summary{
352+
NumDiffCRs: 1,
353+
TotalCRs: 5,
354+
},
355+
Diffs: []types.Diff{
356+
{
357+
DiffOutput: "- name: test\n+ name: changed",
358+
CorrelatedTemplate: "test/TestCR.yaml",
359+
CRName: "v1_ConfigMap_default_test",
360+
},
361+
},
362+
}
363+
364+
var fileBuf, bytesBuf bytes.Buffer
365+
if err := fileAnalyzer.Analyze(&fileBuf, report, "text", "simple"); err != nil {
366+
t.Fatalf("File analyzer failed: %v", err)
367+
}
368+
if err := bytesAnalyzer.Analyze(&bytesBuf, report, "text", "simple"); err != nil {
369+
t.Fatalf("Bytes analyzer failed: %v", err)
370+
}
371+
372+
if fileBuf.String() != bytesBuf.String() {
373+
t.Error("file-based and bytes-based analyzers produced different output")
374+
}
375+
}
376+
377+
func TestNewFromBytes_AnalyzeModes(t *testing.T) {
378+
tests := []struct {
379+
name string
380+
format string
381+
mode string
382+
checkOutput func(t *testing.T, output string)
383+
}{
384+
{
385+
name: "HTML format",
386+
format: "html",
387+
mode: "simple",
388+
checkOutput: func(t *testing.T, output string) {
389+
if !strings.Contains(output, "<!DOCTYPE html>") {
390+
t.Error("expected HTML output")
391+
}
392+
},
393+
},
394+
{
395+
name: "reporting mode",
396+
format: "text",
397+
mode: "reporting",
398+
checkOutput: func(t *testing.T, output string) {
399+
if output == "" {
400+
t.Error("expected non-empty output for reporting mode")
401+
}
402+
},
403+
},
404+
}
405+
406+
for _, tt := range tests {
407+
t.Run(tt.name, func(t *testing.T) {
408+
analyzer, err := NewFromBytes([]byte(testRulesYAML), "")
409+
if err != nil {
410+
t.Fatalf("Failed to create analyzer: %v", err)
411+
}
412+
413+
report := types.ValidationReport{
414+
Summary: types.Summary{NumDiffCRs: 1, TotalCRs: 5},
415+
Diffs: []types.Diff{
416+
{
417+
DiffOutput: "- value: old\n+ value: new",
418+
CorrelatedTemplate: "test/TestCR.yaml",
419+
CRName: "v1_ConfigMap_default_test",
420+
},
421+
},
422+
}
423+
424+
var buf bytes.Buffer
425+
if err := analyzer.Analyze(&buf, report, tt.format, tt.mode); err != nil {
426+
t.Fatalf("Analyze failed: %v", err)
427+
}
428+
429+
tt.checkOutput(t, buf.String())
430+
})
431+
}
432+
}
433+
261434
func TestAnalyze_WithMissingCRs(t *testing.T) {
262435
rulesFile := createTestRulesFile(t)
263436
a, err := New(rulesFile, "")
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ package parser
55
import (
66
"strings"
77

8-
"github.com/openshift-kni/rds-analyzer/internal/types"
8+
"github.com/openshift-kni/rds-analyzer/pkg/types"
99
)
1010

1111
// ANSI color codes for terminal output formatting.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package parser
33
import (
44
"testing"
55

6-
"github.com/openshift-kni/rds-analyzer/internal/types"
6+
"github.com/openshift-kni/rds-analyzer/pkg/types"
77
)
88

99
func TestParseKeyValue(t *testing.T) {
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import (
88
"strings"
99
"time"
1010

11-
"github.com/openshift-kni/rds-analyzer/internal/parser"
12-
"github.com/openshift-kni/rds-analyzer/internal/rules"
13-
"github.com/openshift-kni/rds-analyzer/internal/types"
11+
"github.com/openshift-kni/rds-analyzer/pkg/parser"
12+
"github.com/openshift-kni/rds-analyzer/pkg/rules"
13+
"github.com/openshift-kni/rds-analyzer/pkg/types"
1414
)
1515

1616
// escapeHTML escapes characters that could break HTML structure.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import (
77
"strings"
88
"testing"
99

10-
"github.com/openshift-kni/rds-analyzer/internal/rules"
11-
"github.com/openshift-kni/rds-analyzer/internal/types"
10+
"github.com/openshift-kni/rds-analyzer/pkg/rules"
11+
"github.com/openshift-kni/rds-analyzer/pkg/types"
1212
)
1313

1414
const testHTMLRulesYAML = `

0 commit comments

Comments
 (0)