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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ go.work
*.swo
*~

# Worktrees
.worktrees/

# OS
.DS_Store
Thumbs.db
Expand Down
39 changes: 38 additions & 1 deletion internal/cli/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

awspkg "github.com/kaustuvprajapati/devopsctl/internal/aws"
dockerpkg "github.com/kaustuvprajapati/devopsctl/internal/docker"
gitpkg "github.com/kaustuvprajapati/devopsctl/internal/git"
"github.com/kaustuvprajapati/devopsctl/internal/reporter"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -95,18 +96,54 @@ var auditDockerCmd = &cobra.Command{
},
}

var gitRepoPath string

var auditGitCmd = &cobra.Command{
Use: "git",
Short: "Audit Git repository",
Long: `Audit Git repository for hygiene issues: size, stale branches, large files.`,
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Git audit not yet implemented")
repoPath := gitRepoPath
if repoPath == "" {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current directory: %w", err)
}
repoPath = cwd
}

runner := gitpkg.NewRunner(repoPath, AppConfig.Git)
results, err := runner.RunAll(context.Background())
if err != nil {
fmt.Fprintf(os.Stderr, "warning: some checks encountered errors: %v\n", err)
}

report := &reporter.Report{Module: "git", Results: results}

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

rep := resolveReporter()
if err := rep.Render(w, report); err != nil {
return err
}

if code := exitCodeForResults(results); code > 0 {
os.Exit(code)
}
return nil
},
}

func init() {
auditDockerCmd.Flags().StringVar(&dockerfilePath, "file", "", "path to Dockerfile (overrides config)")
auditDockerCmd.Flags().StringVar(&dockerImage, "image", "", "container image to scan with Trivy")
auditGitCmd.Flags().StringVar(&gitRepoPath, "repo", "", "path to Git repository (defaults to current directory)")
auditCmd.AddCommand(auditAWSCmd)
auditCmd.AddCommand(auditDockerCmd)
auditCmd.AddCommand(auditGitCmd)
Expand Down
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
49 changes: 34 additions & 15 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,72 @@ import (
"gopkg.in/yaml.v3"
)

// Config represents the main configuration structure.
type Config struct {
AWS AWSConfig `yaml:"aws"`
Docker DockerConfig `yaml:"docker"`
Terraform TerraformConfig `yaml:"terraform"`
Git GitConfig `yaml:"git"`
}

// AWSConfig holds AWS-specific configuration.
type AWSConfig struct {
Region string `yaml:"region"`
Profile string `yaml:"profile"`
KeyAgeDays int `yaml:"key_age_days"`
Enabled bool `yaml:"enabled"`
Region string `yaml:"region"`
Profile string `yaml:"profile"`
KeyAgeDays int `yaml:"key_age_days"`
}

// DockerConfig holds Docker-specific configuration.
type DockerConfig struct {
Enabled bool `yaml:"enabled"`
DockerfilePath string `yaml:"dockerfile_path"`
}

// TerraformConfig holds Terraform-specific configuration.
type TerraformConfig struct {
TfDir string `yaml:"tf_dir"`
Enabled bool `yaml:"enabled"`
TfDir string `yaml:"tf_dir"`
}

// GitConfig holds Git-specific configuration.
type GitConfig struct {
RepoSizeMB int `yaml:"repo_size_mb"`
BranchAgeDays int `yaml:"branch_age_days"`
LargeFileMB int `yaml:"large_file_mb"`
Enabled bool `yaml:"enabled"`
RepoSizeMB int `yaml:"repo_size_mb"`
BranchAgeDays int `yaml:"branch_age_days"`
LargeFileMB int `yaml:"large_file_mb"`
}

// IgnoreConfig holds ignore patterns for check filtering.
type IgnoreConfig struct {
Checks []string `yaml:"checks"`
}

// Config represents the main configuration structure.
type Config struct {
AWS AWSConfig `yaml:"aws"`
Docker DockerConfig `yaml:"docker"`
Terraform TerraformConfig `yaml:"terraform"`
Git GitConfig `yaml:"git"`
Ignore IgnoreConfig `yaml:"ignore"`
}

// DefaultConfig returns a Config with sensible defaults.
func DefaultConfig() *Config {
return &Config{
AWS: AWSConfig{
Enabled: true,
Region: "us-east-1",
KeyAgeDays: 90,
},
Docker: DockerConfig{
Enabled: true,
DockerfilePath: "Dockerfile",
},
Terraform: TerraformConfig{
Enabled: true,
},
Git: GitConfig{
Enabled: true,
RepoSizeMB: 500,
BranchAgeDays: 90,
LargeFileMB: 50,
},
Ignore: IgnoreConfig{
Checks: []string{},
},
}
}

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
}
Loading
Loading