Skip to content

Architecture

Technical overview of TerraTidy's internal architecture.

High-Level Architecture

┌─────────────────────────────────────────────────────────────┐
│                         CLI Layer                           │
│  (cmd/terratidy - Cobra commands)                           │
├─────────────────────────────────────────────────────────────┤
│                      Core Orchestrator                      │
│  (internal/runner - Engine coordination, parallel execution)│
├─────────────────────────────────────────────────────────────┤
│                        Engine Layer                         │
│       ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐       │
│       │   Fmt   │ │  Style  │ │  Lint   │ │ Policy  │       │
│       └─────────┘ └─────────┘ └─────────┘ └─────────┘       │
├─────────────────────────────────────────────────────────────┤
│                       Plugin System                         │
│  (internal/plugins - Custom rule loading)                   │
├─────────────────────────────────────────────────────────────┤
│                         SDK Layer                           │
│  (pkg/sdk - Public API for plugins)                         │
└─────────────────────────────────────────────────────────────┘

Directory Structure

terratidy/
├── cmd/
│   └── terratidy/           # CLI entry points
│       ├── main.go          # Main entry
│       ├── root.go          # Root command
│       ├── check.go         # Check command
│       ├── fmt.go           # Format command
│       ├── style.go         # Style command
│       ├── lint.go          # Lint command
│       ├── policy.go        # Policy command
│       └── lsp.go           # LSP server command
├── internal/
│   ├── runner/              # Engine runner, parallel execution
│   │   └── runner.go        # Engine interface, Runner struct
│   ├── config/              # Configuration loading
│   ├── output/              # Output formatting
│   ├── engines/             # Engine implementations
│   │   ├── fmt/             # Format engine
│   │   ├── style/           # Style engine
│   │   ├── lint/            # Lint engine
│   │   └── policy/          # Policy engine
│   ├── lsp/                 # Language server
│   └── plugins/             # Plugin system
├── pkg/
│   └── sdk/                 # Public SDK
│       └── types.go         # Rule interface, Finding, Context types
└── docs/                    # Documentation

Core Components

Engine Interface

All engines implement the Engine interface defined in internal/runner/runner.go:

type Engine interface {
    Name() string
    Run(ctx context.Context, files []string) ([]sdk.Finding, error)
}

Finding Type

Findings represent issues detected by engines:

type Finding struct {
    Rule     string      `json:"rule"`
    Message  string      `json:"message"`
    File     string      `json:"file"`
    Location Location    `json:"location"`
    Severity Severity    `json:"severity"`
    Fix      *FixResult  `json:"fix,omitempty"`
}

type FixResult struct {
    Content []byte
}

Runner

The runner coordinates engine execution:

type Runner struct {
    engines  []Engine
    parallel bool
}

func (r *Runner) Run(ctx context.Context, files []string) ([]sdk.Finding, error) {
    if r.parallel {
        return r.runParallel(ctx, files)
    }
    return r.runSequential(ctx, files)
}

Engine Implementations

Format Engine

Uses the HCL formatter:

func (e *FmtEngine) Run(ctx context.Context, files []string) ([]Finding, error) {
    for _, file := range files {
        content, _ := os.ReadFile(file)
        formatted := hclwrite.Format(content)

        if !bytes.Equal(content, formatted) {
            findings = append(findings, Finding{
                Rule:    "fmt",
                Message: "File is not formatted",
                File:    file,
                Fix:     &FixResult{Content: formatted},
            })
        }
    }
    return findings, nil
}

Style Engine

Implements custom style rules:

func (e *StyleEngine) Run(ctx context.Context, files []string) ([]Finding, error) {
    for _, file := range files {
        ast, _ := hclparse.ParseHCLFile(file)

        for _, rule := range e.rules {
            if e.config.IsRuleEnabled(rule.Name()) {
                findings = append(findings, rule.Check(ast)...)
            }
        }
    }
    return findings, nil
}

Lint Engine

Provides built-in AST rules and optional TFLint integration (subprocess, not linked):

func (e *LintEngine) Run(ctx context.Context, files []string) ([]Finding, error) {
    // Run built-in AST rules first
    for _, file := range files {
        findings = append(findings, e.runBuiltinRules(file)...)
    }

    // Optionally invoke TFLint as subprocess (not embedded)
    if e.config.UseTFLint {
        modules := groupByModule(files)
        for _, module := range modules {
            cmd := exec.CommandContext(ctx, "tflint", "--format=json", module.Path)
            output, _ := cmd.Output()
            findings = append(findings, parseTFLintOutput(output)...)
        }
    }
    return findings, nil
}

Policy Engine

Uses OPA for policy evaluation:

func (e *PolicyEngine) Run(ctx context.Context, files []string) ([]Finding, error) {
    // Parse Terraform to JSON
    input := parseToJSON(files)

    // Load policies
    policies := loadPolicies(e.policyDirs)

    // Evaluate
    r := rego.New(
        rego.Query("data.terraform.deny"),
        rego.Module("policy.rego", policies),
        rego.Input(input),
    )

    rs, _ := r.Eval(ctx)
    return processResults(rs), nil
}

Configuration System

Configuration Loading

func Load(path string) (*Config, error) {
    // Default to .terratidy.yaml
    if path == "" {
        path = ".terratidy.yaml"
    }

    // If file doesn't exist, return defaults
    if _, err := os.Stat(path); os.IsNotExist(err) {
        return DefaultConfig(), nil
    }

    // Read, expand env vars, unmarshal YAML
    data, err := os.ReadFile(path)
    // ... expand ${VAR} and ${VAR:-default} syntax ...

    // Load imports (glob patterns)
    if len(cfg.Imports) > 0 {
        cfg.loadImports(filepath.Dir(path))
    }

    // Validate and return
    cfg.Validate()
    return &cfg, nil
}

Profile Resolution

func (c *Config) ResolveProfile(name string) *Config {
    profile, ok := c.Profiles[name]
    if !ok {
        return c
    }

    // Merge profile with base config
    merged := c.Clone()
    merged.Merge(profile)
    return merged
}

Output System

Formatter Interface

type Formatter interface {
    Format(findings []sdk.Finding, w io.Writer) error
}

Implementations

  • TextFormatter - Human-readable colored output
  • JSONFormatter - Machine-readable JSON
  • SARIFFormatter - GitHub-compatible SARIF
  • JUnitFormatter - JUnit XML for CI systems
  • MarkdownFormatter - Markdown tables
  • HTMLFormatter - HTML report
  • TableFormatter - Tabular text output
  • GitHubActionsFormatter - GitHub Actions annotations
  • HTMLFormatter - Interactive HTML reports
  • JUnitFormatter - CI-compatible JUnit XML

LSP Server

Architecture

┌─────────────────────────────────────────┐
│              LSP Server                 │
├─────────────────────────────────────────┤
│  ┌─────────────┐   ┌─────────────────┐  │
│  │   Handler   │   │  Document Store │  │
│  └─────────────┘   └─────────────────┘  │
├─────────────────────────────────────────┤
│ ┌─────────────────────────────────────┐ │
│ │        Engine Integration           │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘

Request Handling

func (s *Server) handleTextDocumentDidChange(params DidChangeParams) {
    // Update document store
    s.documents.Update(params.URI, params.Changes)

    // Run diagnostics
    go s.publishDiagnostics(params.URI)
}

func (s *Server) publishDiagnostics(uri string) {
    content := s.documents.Get(uri)
    findings := s.runner.Run(context.Background(), []string{uri})

    diagnostics := convertToDiagnostics(findings)
    s.client.PublishDiagnostics(uri, diagnostics)
}

Plugin System

Plugin Loading

func LoadPlugins(dirs []string) ([]Engine, error) {
    var plugins []Engine

    for _, dir := range dirs {
        files, _ := filepath.Glob(filepath.Join(dir, "*.so"))

        for _, file := range files {
            p, _ := plugin.Open(file)
            sym, _ := p.Lookup("Engine")
            engine := sym.(Engine)
            plugins = append(plugins, engine)
        }
    }

    return plugins, nil
}

Performance Considerations

Parallel Execution

  • Engines run concurrently when parallel: true
  • Files are grouped by module for efficiency
  • Context cancellation for early termination

Caching

  • Parsed ASTs are cached per file
  • Policy compilation results are cached
  • File checksums for incremental checking