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
104 changes: 103 additions & 1 deletion internal/cli/doctor.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,120 @@
package cli

import (
"context"
"encoding/json"
"fmt"
"os"

"github.com/kaustuvprajapati/devopsctl/internal/doctor"
"github.com/kaustuvprajapati/devopsctl/internal/reporter"
"github.com/spf13/cobra"
)

// awsModule wraps AWS checks as a doctor.Module
type awsModule struct{}

func (m *awsModule) Name() string { return "aws" }

func (m *awsModule) Run(ctx context.Context) ([]reporter.CheckResult, error) {
// Will be implemented in CLI after AWS clients are available
return []reporter.CheckResult{}, nil
}

// dockerModule wraps Docker checks as a doctor.Module
type dockerModule struct{}

func (m *dockerModule) Name() string { return "docker" }

func (m *dockerModule) Run(ctx context.Context) ([]reporter.CheckResult, error) {
// Will be implemented in CLI after Docker runner is available
return []reporter.CheckResult{}, nil
}

// terraformModule wraps Terraform checks as a doctor.Module
type terraformModule struct{}

func (m *terraformModule) Name() string { return "terraform" }

func (m *terraformModule) Run(ctx context.Context) ([]reporter.CheckResult, error) {
// Will be implemented in CLI after Terraform runner is available
return []reporter.CheckResult{}, nil
}

// gitModule wraps Git checks as a doctor.Module
type gitModule struct{}

func (m *gitModule) Name() string { return "git" }

func (m *gitModule) Run(ctx context.Context) ([]reporter.CheckResult, error) {
// Will be implemented in CLI after Git runner is available
return []reporter.CheckResult{}, nil
}

var doctorCmd = &cobra.Command{
Use: "doctor",
Short: "Run all checks and generate health report",
Long: `Run all available audit and validation checks, aggregate results,
and generate a comprehensive health report.`,
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Doctor not yet implemented")
engine := doctor.NewEngine()

// Register all modules
if err := engine.Register(&awsModule{}); err != nil {
return fmt.Errorf("failed to register aws module: %w", err)
}
if err := engine.Register(&dockerModule{}); err != nil {
return fmt.Errorf("failed to register docker module: %w", err)
}
if err := engine.Register(&terraformModule{}); err != nil {
return fmt.Errorf("failed to register terraform module: %w", err)
}
if err := engine.Register(&gitModule{}); err != nil {
return fmt.Errorf("failed to register git module: %w", err)
}

// Run all modules
reports, err := engine.RunAll(context.Background())
if err != nil {
fmt.Fprintf(os.Stderr, "warning: some modules encountered errors: %v\n", err)
}

// Compute summary
summary := doctor.ComputeSummary(reports)

// Output results
w, err := resolveWriter(cmd)
if err != nil {
return err
}
if w != os.Stdout {
defer w.Close()
}

rep := resolveReporter()

// Render each module's results
for _, r := range reports {
report := &reporter.Report{Module: r.Module, Results: r.Results}
if err := rep.Render(w, report); err != nil {
return err
}
if r.Error != "" {
fmt.Fprintf(w, " [ERROR] %s\n", r.Error)
}
}

// Print summary for JSON output
if jsonOutput {
summaryJSON, _ := json.MarshalIndent(summary, "", " ")
fmt.Fprintf(w, "\n%s\n", summaryJSON)
}

// Exit with appropriate code
if code := doctor.ExitCode(reports); code > 0 {
os.Exit(code)
}

return nil
},
}
Expand Down
75 changes: 75 additions & 0 deletions internal/doctor/engine.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package doctor

import (
"context"
"fmt"

"github.com/kaustuvprajapati/devopsctl/internal/reporter"
)

// ModuleReport holds results from a single module execution.
type ModuleReport struct {
Module string `json:"module"`
Results []reporter.CheckResult `json:"results"`
Error string `json:"error,omitempty"`
}

// Engine orchestrates all registered modules.
type Engine struct {
registry *Registry
}

// NewEngine creates a new doctor engine.
func NewEngine() *Engine {
return &Engine{
registry: NewRegistry(),
}
}

// Register adds a module to the engine.
func (e *Engine) Register(m Module) error {
return e.registry.Register(m)
}

// RunAll executes all registered modules and returns aggregated results.
func (e *Engine) RunAll(ctx context.Context) ([]ModuleReport, error) {
var reports []ModuleReport
moduleNames := e.registry.List()

for _, name := range moduleNames {
module, ok := e.registry.Get(name)
if !ok {
continue
}

report := ModuleReport{Module: name}

results, err := module.Run(ctx)
if err != nil {
report.Error = err.Error()
// Continue running other modules despite failure
}
report.Results = results
reports = append(reports, report)
}

// Check if any module failed
var errs []string
for _, r := range reports {
if r.Error != "" {
errs = append(errs, fmt.Sprintf("%s: %s", r.Module, r.Error))
}
}

var err error
if len(errs) > 0 {
err = fmt.Errorf("some modules failed: %v", errs)
}

return reports, err
}

// Registry returns the underlying registry.
func (e *Engine) Registry() *Registry {
return e.registry
}
141 changes: 141 additions & 0 deletions internal/doctor/engine_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package doctor

import (
"context"
"errors"
"testing"

"github.com/kaustuvprajapati/devopsctl/internal/reporter"
)

// mockModule implements Module for testing
type mockModule struct {
name string
results []reporter.CheckResult
err error
}

func (m *mockModule) Name() string { return m.name }

func (m *mockModule) Run(ctx context.Context) ([]reporter.CheckResult, error) {
return m.results, m.err
}

// TestRegistryRegister tests module registration
func TestRegistryRegister(t *testing.T) {
registry := NewRegistry()

module := &mockModule{name: "test", results: []reporter.CheckResult{}}
err := registry.Register(module)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}

if registry.Len() != 1 {
t.Errorf("Expected 1 module, got %d", registry.Len())
}
}

// TestRegistryDuplicateRegistration tests duplicate registration
func TestRegistryDuplicateRegistration(t *testing.T) {
registry := NewRegistry()

module := &mockModule{name: "test", results: []reporter.CheckResult{}}
registry.Register(module)

err := registry.Register(module)
if err != ErrModuleAlreadyRegistered {
t.Errorf("Expected ErrModuleAlreadyRegistered, got %v", err)
}
}

// TestRegistryNilModule tests nil module registration
func TestRegistryNilModule(t *testing.T) {
registry := NewRegistry()

err := registry.Register(nil)
if err != ErrNilModule {
t.Errorf("Expected ErrNilModule, got %v", err)
}
}

// TestEngineRunAll tests running all modules
func TestEngineRunAll(t *testing.T) {
engine := NewEngine()

// Register modules
engine.Register(&mockModule{
name: "module1",
results: []reporter.CheckResult{
{CheckName: "check1", Severity: "LOW", ResourceID: "res1"},
},
})
engine.Register(&mockModule{
name: "module2",
results: []reporter.CheckResult{
{CheckName: "check2", Severity: "HIGH", ResourceID: "res2"},
},
})

reports, err := engine.RunAll(context.Background())
if err != nil {
t.Errorf("Expected no error, got %v", err)
}

if len(reports) != 2 {
t.Errorf("Expected 2 reports, got %d", len(reports))
}
}

// TestEnginePartialFailure tests partial module failure handling
func TestEnginePartialFailure(t *testing.T) {
engine := NewEngine()

engine.Register(&mockModule{
name: "success",
results: []reporter.CheckResult{
{CheckName: "check1", Severity: "LOW"},
},
})
engine.Register(&mockModule{
name: "failure",
results: nil,
err: errors.New("module error"),
})

reports, err := engine.RunAll(context.Background())
if err == nil {
t.Error("Expected error due to module failure")
}

if len(reports) != 2 {
t.Errorf("Expected 2 reports, got %d", len(reports))
}

// Find the failed module
var failedReport ModuleReport
for _, r := range reports {
if r.Module == "failure" {
failedReport = r
break
}
}

if failedReport.Error == "" {
t.Error("Expected error message in failed module report")
}
}

// TestEngineNoModules tests running with no modules
func TestEngineNoModules(t *testing.T) {
engine := NewEngine()

reports, err := engine.RunAll(context.Background())
if err != nil {
t.Errorf("Expected no error, got %v", err)
}

if len(reports) != 0 {
t.Errorf("Expected 0 reports, got %d", len(reports))
}
}
Loading
Loading