feat: serve local terraform repos as a provider registry
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful

Local terraform repos already spoke the network mirror protocol, which needs
per-consumer .terraformrc config. This adds the provider registry protocol so
`terraform init` installs from a bare source address
(artifactapi.k8s.../{repo}/{type}) with no client setup.

- serve /.well-known/terraform.json service discovery and the providers.v1
  versions/download endpoints under /terraform/v1/providers
- map the Terraform namespace to the artifactapi repo name and locate the
  provider by type; download_url points back at the existing local file path
- generate SHA256SUMS per version and sign it with a GPG key loaded from
  TF_SIGNING_KEY_PATH; advertise the public key + key id in the download
  response. No key configured -> registry stays disabled (endpoints 404)
- new internal/tfsign (key loading + detached signing) and
  internal/api/terraform (registry handler); export ParseProviderZip for reuse
- add TF_SIGNING_KEY_PATH/PASSPHRASE and TF_PROVIDER_PROTOCOLS config
- unit test signing + verification; dockerised test of the full flow incl.
  signature verification against the advertised key

Also anchor the terraform/ gitignore to the repo root so it stops swallowing
internal/api/terraform and internal/provider/terraform test files (the latter
had gone silently untracked).
This commit is contained in:
2026-07-03 17:46:55 +10:00
parent 3a3b7fe7b7
commit edb6c7c0f7
11 changed files with 939 additions and 2 deletions
+20
View File
@@ -12,6 +12,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
tfregistry "git.unkin.net/unkin/artifactapi/internal/api/terraform"
v1 "git.unkin.net/unkin/artifactapi/internal/api/v1"
v2 "git.unkin.net/unkin/artifactapi/internal/api/v2"
"git.unkin.net/unkin/artifactapi/internal/cache"
@@ -30,6 +31,7 @@ import (
_ "git.unkin.net/unkin/artifactapi/internal/provider/terraform"
"git.unkin.net/unkin/artifactapi/internal/proxy"
"git.unkin.net/unkin/artifactapi/internal/storage"
"git.unkin.net/unkin/artifactapi/internal/tfsign"
"git.unkin.net/unkin/artifactapi/internal/virtual"
)
@@ -43,6 +45,7 @@ type Server struct {
engine *proxy.Engine
virtEngine *virtual.Engine
localHandler *v2.LocalHandler
tfRegistry *tfregistry.Handler
gc *gc.Collector
}
@@ -67,6 +70,17 @@ func New(cfg *config.Config, version string) (*Server, error) {
virtEngine := virtual.NewEngine(db, engine)
collector := gc.New(db, s3, 1*time.Hour)
// A failure to load the signing key must not take the server down: the
// terraform registry simply stays disabled until a valid key is present.
signer, err := tfsign.Load(cfg.TFSigningKeyPath, cfg.TFSigningKeyPassphrase)
if err != nil {
slog.Warn("terraform provider registry disabled", "error", err)
}
tfRegistry := tfregistry.NewHandler(db, signer, cfg.TFProviderProtocols)
if tfRegistry.Enabled() {
slog.Info("terraform provider registry enabled", "key_id", signer.KeyID())
}
s := &Server{
cfg: cfg,
version: version,
@@ -76,6 +90,7 @@ func New(cfg *config.Config, version string) (*Server, error) {
engine: engine,
virtEngine: virtEngine,
localHandler: localHandler,
tfRegistry: tfRegistry,
gc: collector,
}
@@ -97,6 +112,11 @@ func (s *Server) routes() chi.Router {
r.Get("/", s.handleRoot)
r.Get("/version", s.handleVersion)
// Terraform provider registry: service discovery at the well-known path,
// providers.v1 protocol under /terraform/v1/providers.
r.Get("/.well-known/terraform.json", s.tfRegistry.ServiceDiscovery)
r.Mount(tfregistry.MountPath, s.tfRegistry.Routes())
proxyHandler := v1.NewProxyHandler(s.engine, s.virtEngine, s.db, s.store, s.localHandler)
r.Mount("/api/v1", proxyHandler.Routes())
r.Mount("/v2", proxyHandler.DockerV2Routes())