Summary
Add support for loading Rego policies from a remote HTTP bundle server at plugin startup, as an alternative to the existing policy_dir local filesystem option.
Motivation
In team and CI environments, policies are typically centrally managed and distributed via an OPA bundle server (S3, GCS, nginx, or a custom registry). Today, every consumer of tflint-ruleset-opa must either:
- Commit policy files into each repository and keep them in sync, or
- Pre-download the bundle as a separate CI step before running
tflint
Neither approach is ideal. A native bundle_url option would let the plugin fetch the authoritative policy bundle directly at startup, making centralized policy distribution a first-class concern.
Proposed Configuration
plugin "opa" {
enabled = true
version = "0.10.0"
source = "github.com/terraform-linters/tflint-ruleset-opa"
bundle_url = "https://policy-server.example.com/bundles/tflint.tar.gz"
bundle_token = "..." # optional, bearer token for auth
}
The bundle_token value should also be resolvable from an environment variable to avoid hardcoding secrets.
Proposed Behaviour
- If
bundle_url is set, the plugin fetches the bundle over HTTP(S) at startup, parses it with bundle.NewReader(), and passes it to rego.New() via rego.ParsedBundle() — exactly the same in-process evaluation path used today
- If
bundle_url is not set, behaviour is identical to today (policy_dir is used)
- Both can coexist:
bundle_url supplies shared org-wide policies, policy_dir supplies local overrides
- Optional: respect
ETag / If-None-Match for caching, to avoid re-downloading an unchanged bundle on every tflint run
What This Is Not
This is not a request for the full OPA bundle polling runtime (plugins/bundle + plugin manager). That machinery is designed for long-lived server processes. For a CLI tool like tflint, a single one-shot HTTP fetch at startup is the right model — simple, fast, and with no persistent state.
Implementation Notes
The change is self-contained and relatively small:
- Extend
Config with BundleURL and BundleToken fields
- In
ApplyConfig, branch on whether BundleURL is set: if so, http.Get the URL, pipe the response body through bundle.NewReader().Read(), and pass the result to NewEngine alongside or instead of the loader.Result
NewEngine (or a sibling constructor) accepts a *bundle.Bundle and passes it to rego.New() via rego.ParsedBundle()
- The custom Rego builtins (
terraform.resources, tflint.issue, etc.) are registered on the rego.Rego instance as today — they work regardless of where the policies came from
Roughly ~100 lines of new Go, no new dependencies (all required packages are already present in the OPA module).
Alternatives Considered
- Pre-downloading the bundle in CI: works but requires an extra step and couples the CI script to the bundle server URL/auth, rather than keeping that in
.tflint.hcl
- Committing policies to each repo: creates drift and makes org-wide policy updates require PRs across every repository
Are you willing to contribute a PR?
Yes, if the approach is agreed upon.
Summary
Add support for loading Rego policies from a remote HTTP bundle server at plugin startup, as an alternative to the existing
policy_dirlocal filesystem option.Motivation
In team and CI environments, policies are typically centrally managed and distributed via an OPA bundle server (S3, GCS, nginx, or a custom registry). Today, every consumer of
tflint-ruleset-opamust either:tflintNeither approach is ideal. A native
bundle_urloption would let the plugin fetch the authoritative policy bundle directly at startup, making centralized policy distribution a first-class concern.Proposed Configuration
The
bundle_tokenvalue should also be resolvable from an environment variable to avoid hardcoding secrets.Proposed Behaviour
bundle_urlis set, the plugin fetches the bundle over HTTP(S) at startup, parses it withbundle.NewReader(), and passes it torego.New()viarego.ParsedBundle()— exactly the same in-process evaluation path used todaybundle_urlis not set, behaviour is identical to today (policy_diris used)bundle_urlsupplies shared org-wide policies,policy_dirsupplies local overridesETag/If-None-Matchfor caching, to avoid re-downloading an unchanged bundle on everytflintrunWhat This Is Not
This is not a request for the full OPA bundle polling runtime (
plugins/bundle+ plugin manager). That machinery is designed for long-lived server processes. For a CLI tool liketflint, a single one-shot HTTP fetch at startup is the right model — simple, fast, and with no persistent state.Implementation Notes
The change is self-contained and relatively small:
ConfigwithBundleURLandBundleTokenfieldsApplyConfig, branch on whetherBundleURLis set: if so,http.Getthe URL, pipe the response body throughbundle.NewReader().Read(), and pass the result toNewEnginealongside or instead of theloader.ResultNewEngine(or a sibling constructor) accepts a*bundle.Bundleand passes it torego.New()viarego.ParsedBundle()terraform.resources,tflint.issue, etc.) are registered on therego.Regoinstance as today — they work regardless of where the policies came fromRoughly ~100 lines of new Go, no new dependencies (all required packages are already present in the OPA module).
Alternatives Considered
.tflint.hclAre you willing to contribute a PR?
Yes, if the approach is agreed upon.