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¶
Implementations¶
TextFormatter- Human-readable colored outputJSONFormatter- Machine-readable JSONSARIFFormatter- GitHub-compatible SARIFJUnitFormatter- JUnit XML for CI systemsMarkdownFormatter- Markdown tablesHTMLFormatter- HTML reportTableFormatter- Tabular text outputGitHubActionsFormatter- GitHub Actions annotationsHTMLFormatter- Interactive HTML reportsJUnitFormatter- 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