Skip to content

Plugin System

TerraCi uses a compile-time plugin system inspired by the database/sql driver pattern in Go. Plugins register themselves via init() and blank imports at build time.

Built-in Plugins

PluginPurposeConfig Required
gitChanged-module detection via git diffNo
gitlabGitLab CI pipeline generation and MR commentsYes (presence activates)
githubGitHub Actions workflow generation and PR commentsYes (presence activates)
summaryMR/PR plan summary commentsNo (enabled by default)
costCloud cost estimation (AWS)Yes (providers.aws.enabled: true)
policyOPA policy checksYes (enabled: true)
tfupdateTerraform dependency resolver and lock synchronizerYes (enabled: true)

Activation Policies

Each plugin has an activation policy that determines when it participates in the current run.

Always Active

The git plugin requires no configuration. It provides changed-module detection for --changed-only mode and is always available.

Activated by Config Presence

gitlab and github plugins activate when their config section exists under plugins:. Removing the section disables them:

yaml
plugins:
  gitlab:      # presence of this section activates the plugin
    image: { name: hashicorp/terraform:1.6 }

Active by Default

summary is active unless explicitly disabled. It posts plan summaries to MR/PR comments:

yaml
plugins:
  summary:
    enabled: false   # opt out

Explicitly Enabled

cost, policy, and tfupdate must be explicitly opted in:

yaml
plugins:
  cost:
    providers:
      aws:
        enabled: true

  policy:
    enabled: true
    sources:
      - path: policies

  tfupdate:
    enabled: true
    policy:
      bump: minor

CI Provider Detection

TerraCi auto-detects the active CI provider at runtime:

  1. Environment variables -- GITLAB_CI=true selects GitLab, GITHUB_ACTIONS=true selects GitHub
  2. TERRACI_PROVIDER env var -- explicit override:
    bash
    TERRACI_PROVIDER=gitlab terraci generate -o pipeline.yml
  3. Single configured provider -- if only one CI provider has config, it is used automatically

If multiple providers are configured and no environment is detected, TerraCi returns an error with instructions to set TERRACI_PROVIDER.

Plugin Capabilities

Plugins implement one or more capability interfaces. The framework discovers them at runtime via type assertion:

CapabilityPurposePlugins
CommandProviderCLI subcommands (terraci cost, etc.)cost, policy, summary, tfupdate
PipelineContributorInject steps/jobs into pipeline IRcost, policy, summary
InitContributorForm fields for terraci init wizardgitlab, github, cost, policy, summary, tfupdate
GeneratorFactoryCreate provider-specific pipeline generatorgitlab, github
CommentFactoryCreate MR/PR comment servicegitlab, github
EnvDetectorDetect CI environment from env varsgitlab, github
ChangeDetectionProviderDetect changed modules via VCS diffgit
RuntimeProviderLazy construction of heavy runtime statecost, policy, tfupdate
PreflightableCheap startup validation before commands rungitlab, github, git, cost, policy, tfupdate
VersionProviderContribute version info to terraci versionpolicy

A single plugin can implement multiple capabilities. For example, cost implements CommandProvider (the terraci cost command), PipelineContributor (adds cost estimation step to pipeline), InitContributor (adds toggle to init wizard), RuntimeProvider (lazy estimator setup), and Preflightable (config validation).

Plugin Lifecycle

Every plugin goes through the same lifecycle:

1. Register    -- init() registers the plugin via registry.Register()
2. Configure   -- framework decodes the matching plugins.<key> YAML section
3. Preflight   -- cheap validation (env detection, config checks)
4. Freeze      -- AppContext is frozen, no further config mutations
5. Execute     -- commands lazily build RuntimeProvider runtimes as needed

Preflight runs for all enabled plugins before any command. It must be fast and side-effect-light -- no network calls, no heavy state. Heavy work (API clients, caches, estimators) belongs in RuntimeProvider, which constructs the runtime lazily when a command actually needs it.

Custom Plugins

Building with xterraci

xterraci produces a custom TerraCi binary with additional or fewer plugins:

bash
# Add an external plugin
xterraci build --with github.com/your-org/terraci-plugin-slack

# Pin a specific version
xterraci build --with github.com/your-org/terraci-plugin-slack@v1.2.0

# Use a local plugin during development
xterraci build --with github.com/your-org/plugin=../my-plugin

# Remove built-in plugins you don't need
xterraci build --without cost --without policy

# Combine
xterraci build \
  --with github.com/your-org/terraci-plugin-slack \
  --without cost \
  --output ./build/terraci-custom

How It Works

xterraci build:

  1. Creates a temporary Go module
  2. Generates a main.go with blank imports of selected plugins
  3. Runs go get for each external plugin
  4. Runs go build to produce the binary

The resulting binary is identical to the standard terraci but with a different set of plugins compiled in.

List Built-in Plugins

bash
xterraci list-plugins

Writing a Plugin

A minimal external plugin needs:

1. Registration -- init() function that calls registry.Register():

go
package myplugin

import (
    "github.com/edelwud/terraci/pkg/plugin"
    "github.com/edelwud/terraci/pkg/plugin/registry"
)

func init() {
    registry.Register(&Plugin{
        BasePlugin: plugin.BasePlugin[*Config]{
            PluginName: "myplugin",
            PluginDesc: "My custom plugin",
            EnableMode: plugin.EnabledExplicitly,
            DefaultCfg: func() *Config { return &Config{} },
            IsEnabledFn: func(cfg *Config) bool {
                return cfg != nil && cfg.Enabled
            },
        },
    })
}

type Plugin struct {
    plugin.BasePlugin[*Config]
}

type Config struct {
    Enabled bool   `yaml:"enabled"`
    APIKey  string `yaml:"api_key"`
}

2. Capabilities -- implement the interfaces you need:

go
// CommandProvider -- adds `terraci myplugin` command
func (p *Plugin) Commands(ctx *plugin.AppContext) []*cobra.Command {
    return []*cobra.Command{{
        Use:   "myplugin",
        Short: "Run my custom plugin",
        RunE: func(cmd *cobra.Command, _ []string) error {
            // your logic here
            return nil
        },
    }}
}

3. Go module -- publish as a Go module with a go.mod that depends on github.com/edelwud/terraci.

Plugin File Convention

For larger plugins, follow the one-file-per-capability convention used by built-in plugins:

FileContents
plugin.goinit(), Plugin struct, BasePlugin embedding
lifecycle.goPreflightable implementation
commands.goCommandProvider with cobra commands
runtime.goRuntimeProvider for lazy heavy state
usecases.goCommand orchestration over typed runtime
pipeline.goPipelineContributor steps/jobs
init_wizard.goInitContributor form fields
output.goCLI rendering helpers
report.goCI report assembly

Working Example

See examples/external-plugin for a complete working example that adds terraci hello.

Configuration Reference

PluginConfig page
GitLab CIconfig/gitlab
GitLab MRconfig/gitlab-mr
GitHub Actionsconfig/github
Cost Estimationconfig/cost
Policy Checksconfig/policy
Dependency Updatesconfig/tfupdate

Released under the MIT License.