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 extensions:. Removing the section disables them:

yaml
extensions:
  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
extensions:
  summary:
    enabled: false   # opt out

Explicitly Enabled

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

yaml
extensions:
  cost:
    providers:
      aws:
        enabled: true

  policy:
    enabled: true
    sources:
      - type: path
        path: policies

  tfupdate:
    enabled: true
    policy:
      bump: minor

CI Provider Detection

TerraCi auto-detects the active CI provider at runtime:

  1. TERRACI_PROVIDER env var -- explicit override:
    bash
    TERRACI_PROVIDER=gitlab terraci generate -o pipeline.yml
  2. Environment variables -- GITLAB_CI=true selects GitLab, GITHUB_ACTIONS=true selects GitHub
  3. Single active provider -- if only one CI provider is active, 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. Registry lifecycle facades own discovery; plugin authors only implement the interface contracts:

CapabilityPurposePlugins
CommandProviderCLI subcommands (terraci cost, terraci local-exec, etc.)cost, policy, summary, tfupdate, localexec
PipelineContributorAdd standalone DAG jobs to pipeline IRcost, policy, summary, tfupdate
InitContributorForm fields for terraci init wizardgitlab, github, cost, policy, summary, tfupdate
PipelineGeneratorFactoryCreate provider-specific generators from immutable pipeline IR (NewGenerator(*pipeline.IR))gitlab, github
CommentServiceFactoryCreate MR/PR comment servicegitlab, github
EnvDetectorDetect CI environment from env varsgitlab, github
CIInfoProviderProvider name, pipeline ID, commit SHAgitlab, github
ChangeDetectionProviderSDK capability that embeds plugin-agnostic workflow.ChangeDetector for VCS diffsgit
PreflightableCheap startup validation before commands rungitlab, github, git, cost, policy, tfupdate
VersionProviderContribute version info to terraci versionpolicy
KVCacheProviderNamed key/value cache backend resolutioninmemcache
BlobStoreProviderNamed blob/object store backend (NewBlobStore(ctx, appCtx, opts))diskblob

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), and Preflightable (config validation); its estimator runtime is a plugin-local lazy builder.

Plugin Lifecycle

Every plugin goes through the same lifecycle:

1. Register    -- init() registers the plugin via registry.RegisterFactory()
2. Configure   -- framework decodes the matching extensions.<key> YAML section
3. Preflight   -- cheap validation (env detection, config checks)
4. Bind        -- runflow builds immutable Prepared/AppContext state
5. Execute     -- commands lazily build plugin-local typed 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 plugin-local runtime builders, which run only when a command actually needs them.

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.RegisterFactory():

go
package myplugin

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

func init() {
    registry.RegisterFactory(func() plugin.Plugin {
        return &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"`
}

func (c *Config) Clone() *Config {
    if c == nil {
        return nil
    }
    out := *c
    return &out
}

2. Capabilities -- implement the interfaces you need:

go
// CommandProvider -- adds `terraci myplugin` command
func (p *Plugin) Commands() []*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.goPlugin-local lazy heavy state builder
usecases.goCommand orchestration over typed runtime
pipeline.goPipelineContributor 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
GitHub Actionsconfig/github
Summary Commentsconfig/summary
Cost Estimationconfig/cost
Policy Checksconfig/policy
Dependency Updatesconfig/tfupdate

Released under the MIT License.