Skip to content

feat(terraform): vendor terraform-provider-litellm as source of truth with endpoint drift CI - #32241

Merged
yassin-berriai merged 4 commits into
litellm_internal_stagingfrom
litellm_terraform_provider_source_of_truth
Jul 7, 2026
Merged

feat(terraform): vendor terraform-provider-litellm as source of truth with endpoint drift CI#32241
yassin-berriai merged 4 commits into
litellm_internal_stagingfrom
litellm_terraform_provider_source_of_truth

Conversation

@yassin-berriai

@yassin-berriai yassin-berriai commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

Resolves LIT-4212

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Screenshots / Proof of Fix

The endpoint audit runs the exact pipeline CI runs, and caught a real bug in the imported source on its first run (the provider sent POST to an endpoint the proxy serves as PATCH only):

$ .venv/bin/python terraform/provider/tools/dump_openapi.py /tmp/openapi.json
$ cd terraform/provider && go run ./tools/endpointaudit -provider-dir ./litellm -spec /tmp/openapi.json
error: provider/proxy endpoint drift:
  litellm/resource_organization.go:158:15: POST /organization/update: path exists but method not allowed

After the fix in this PR:

$ go run ./tools/endpointaudit -provider-dir ./litellm -spec /tmp/openapi.json
OK: 54 request call sites verified against 535 proxy OpenAPI paths

After the second commit removes the dead org/team client methods flagged in review, the audit tracks only live call sites:

$ go run ./tools/endpointaudit -provider-dir ./litellm -spec /tmp/openapi.json
OK: 46 request call sites verified against 535 proxy OpenAPI paths

End user proof against a live proxy (postgres on 5461, proxy on 4061 started with python litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml --port 4061), driving the real provider binary through terraform with a dev_overrides CLI config:

$ cat main.tf
resource "litellm_organization" "demo" {
  organization_alias = var.org_alias
}

$ terraform apply -auto-approve -var org_alias=tf-demo-org
litellm_organization.demo: Creation complete after 0s [id=85bb5e7a-1971-4e69-9704-16729f0bf56c]

Updating the alias with the provider built from the current split repo source (before this PR):

$ terraform apply -auto-approve -var org_alias=tf-demo-org-renamed
Error: error updating organization: 405 Method Not Allowed - {"detail":"Method Not Allowed"}

Same update with the provider built from this PR:

$ terraform apply -auto-approve -var org_alias=tf-demo-org-renamed
litellm_organization.demo: Modifications complete after 0s [id=85bb5e7a-1971-4e69-9704-16729f0bf56c]

$ curl -s "http://localhost:4061/organization/info?organization_id=85bb5e7a-1971-4e69-9704-16729f0bf56c" \
    -H "Authorization: Bearer sk-1234"
{"organization_id":"85bb5e7a-1971-4e69-9704-16729f0bf56c","organization_alias":"tf-demo-org-renamed",...}

Type

🆕 New Feature
🐛 Bug Fix

Changes

This PR makes terraform/provider/ the source of truth for the LiteLLM Terraform provider, imported from BerriAI/terraform-provider-litellm@f1aac99. The split repo becomes a thin release mirror, following the model already used for the aws/gcp Terraform modules under terraform/litellm/. The motivation is deterministic CI that fails when the provider and the proxy endpoints drift apart, which is impossible while they live in separate repos

The new Terraform Provider workflow has two jobs. provider-checks runs gofmt, go vet, go build, and the provider's unit tests on terraform/provider/** changes. endpoint-drift generates the proxy's OpenAPI schema (terraform/provider/tools/dump_openapi.py) and runs terraform/provider/tools/endpointaudit, a Go AST tool that statically resolves every (method, path) the provider sends through its two request helpers (string literals, package consts, local reassignments, and fmt.Sprintf compositions) and verifies each against the schema. It fails closed: call sites it cannot resolve statically and raw http.NewRequest calls outside the helpers are errors. The drift job also triggers on litellm/proxy/** changes, so a litellm PR that removes or re-methods a management endpoint the provider depends on fails CI on this repo instead of breaking terraform users at the next release

The audit caught real drift on its first run: the proxy serves /organization/update and /organization/member_update as PATCH only, but the provider sent POST, so litellm_organization and litellm_organization_member updates failed with a 405. Both call sites are fixed here (resource_organization.go, client.go) and the extractor's tests pin every resolution shape so the audit itself stays honest

Review follow-ups folded into the first commit: the client's empty-body response fallback now covers PATCH, PUT, and DELETE alongside POST (relevant since organization updates now send PATCH), and dump_openapi.py validates its argument count instead of raising IndexError

The second commit addresses the remaining review threads. io/ioutil is replaced with io. The unused org/team CRUD client methods (and the validateUUID helper only they used) are removed so the audit tracks only live call sites, dropping the verified count from 54 to 46. Log redaction now parses the JSON payload and recursively masks sensitive fields instead of regexing the raw string, which fixes the nested credential_values leak flagged in review (the old regex also mangled vertex_credentials values containing escaped JSON and leaked part of them); a regex pass remains as fallback for non-JSON payloads, and new unit tests cover both paths and fail against the old implementation. On the secrets-in-state threads: the docs now state explicitly that Sensitive attributes persist in plaintext state, the vector store examples no longer show api_key inside litellm_params, and litellm_credential_name is the documented path for secret material. Converting the existing attributes to write-only is not possible as shaped (the SDK rejects WriteOnly on TypeMap, and the conversion would require Terraform 1.11+ and break rotation-by-diff), so the scalar _wo redesign is tracked as a provider follow-up instead

The third commit fixes the one real readback the security threads surfaced: the vector store Read wrote litellm_params straight back from the API response into a non-Sensitive attribute. The proxy redacts secrets in those responses, so in practice the readback replaced user config with redaction sentinels and caused perpetual diffs, and against a server returning raw values it would have persisted secrets into state unmarked. Read now preserves the config value like the credential and model resources do, litellm_params is marked Sensitive, and a regression test pins that a server-returned api_key never lands in state (it fails against the previous behavior)

The fourth commit resolves the security bot's follow-up round. The team member update payload omitted role, and the proxy leaves role unchanged when the field is absent, so a Terraform role downgrade (admin to user) reported as applied never took effect on the proxy; the update now always sends the configured role. The MCP server resource had the same readback problem as the vector store: env came straight back from API responses into a non-Sensitive attribute (raw for admin viewers, blanked for sanitized ones), so Read now preserves the config value, env is marked Sensitive, and the docs warn against passing secrets via args since argv is also visible in the server's process list. Regression tests cover both fixes and fail against the previous behavior

Import notes: the go.mod module path is renamed from the upstream fork author's namespace to github.com/BerriAI/terraform-provider-litellm, the stale 1MB openapi.json committed at the provider root is dropped (nothing referenced it), and README/RELEASING/CHANGELOG are updated for the new home and release flow. Publishing keeps working exactly as today (goreleaser on tag push in the mirror repo); the follow-up project-releaser PR adds the workflow that rsyncs this directory into the mirror and tags it

@yassin-berriai
yassin-berriai requested a review from a team July 6, 2026 09:55
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR vendors the LiteLLM Terraform provider into terraform/provider/ as the source of truth, fixes two live correctness bugs, and adds a CI drift detector that fails when provider endpoints diverge from the proxy's OpenAPI spec.

  • Role-downgrade bug fixed: resourceLiteLLMTeamMemberUpdate previously omitted role from the update payload; the proxy left role unchanged on absence, so a Terraform-declared role downgrade never reached the server. The field is now always sent, with a regression test that captures the HTTP payload and asserts it contains the correct role value.
  • Secret no-readback applied to MCP server env: updateSchemaFromResponse deliberately skips d.Set(\"env\", ...) so server-returned env vars never overwrite or augment Terraform state; env is also marked Sensitive: true. This mirrors the same fix already applied to vector store litellm_params in the previous commit. Both have regression tests that fail against the old behavior.
  • Endpoint-drift CI: A new endpoint-drift job regenerates the proxy OpenAPI spec and runs a Go AST auditor (endpointaudit) that statically resolves every (method, path) the provider sends and verifies each against the spec.

Confidence Score: 5/5

All correctness and security fixes are well-scoped, backed by regression tests that fail against the previous behavior, and the new CI drift detector adds durable protection against future endpoint mismatches.

The two behavioral fixes (team member role propagation and MCP server env no-readback) are straightforward and their regression tests are tight. The broader vendoring changes are mechanical imports with no new proxy-side logic. No issues were found that affect correctness, security boundaries, or state integrity.

No files require special attention.

Important Files Changed

Filename Overview
terraform/provider/litellm/resource_team_member.go Team member Update now includes role in the payload; fixes silent role-downgrade regression.
terraform/provider/litellm/resource_team_member_test.go New unit test captures the HTTP payload and asserts role is present; fails against the old implementation.
terraform/provider/litellm/resource_mcp_server_crud.go updateSchemaFromResponse omits d.Set("env") so the Sensitive env attribute is never overwritten from server responses.
terraform/provider/litellm/resource_mcp_server_crud_test.go Unit test verifies server-returned env values never land in state; fails against the previous behavior.
terraform/provider/litellm/resource_mcp_server.go env TypeMap marked Sensitive:true; args intentionally read back from server per design.
terraform/provider/litellm/resource_vector_store_crud.go resourceLiteLLMVectorStoreRead omits d.Set("litellm_params") so server-returned params never write to state.
terraform/provider/litellm/resource_vector_store_crud_test.go Test wires httptest.Server and asserts api_key never leaks into state while config values are preserved.
terraform/provider/litellm/client.go Empty-body guard covers POST/PATCH/PUT/DELETE; redactSensitiveData recursively masks nested fields; io/ioutil removed.
.github/workflows/test-terraform-provider.yml New CI workflow with provider-checks and endpoint-drift jobs; drift job also triggers on litellm/proxy/** changes.
terraform/provider/tools/endpointaudit/main.go Go AST auditor statically resolves every (method, path) call site and verifies against proxy OpenAPI; fails closed on unresolvable sites.

Reviews (6): Last reviewed commit: "fix(terraform): send role on team member..." | Re-trigger Greptile

Comment thread terraform/provider/litellm/client.go
Comment thread terraform/provider/litellm/client.go Outdated
Comment thread terraform/provider/litellm/client.go Outdated
Comment thread terraform/provider/tools/dump_openapi.py
@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR vendors the LiteLLM Terraform provider into terraform/provider/ as the source of truth and adds a CI workflow with two jobs: provider-checks (gofmt/vet/build/unit tests) and endpoint-drift (static AST audit that verifies every provider HTTP call against the proxy's live OpenAPI schema). It also fixes two real 405 regressions (POST → PATCH for /organization/update and /organization/member_update) that were caught on the audit's first run.

  • Endpoint-drift audit (tools/endpointaudit/main.go): Go AST tool that resolves string literals, package consts, local reassignments, and fmt.Sprintf compositions; fails closed on unresolved call sites and raw http.NewRequest usage outside the designated helpers.
  • Bug fixes: resource_organization.go and client.go updated from POST to PATCH for the organization update and member-update endpoints, validated end-to-end against a live proxy.
  • CI wiring: endpoint-drift also triggers on litellm/proxy/** changes, so a proxy PR that removes or re-methods a management endpoint fails CI before it can break Terraform users.

Confidence Score: 4/5

Safe to merge; the core bug fixes and audit infrastructure are correct and well-tested, with end-to-end proof provided.

The organisation and team CRUD methods in client.go are never called by their corresponding Terraform resources (both use MakeRequest directly), so ~8 of the 54 verified audit call sites are phantom. The sendRequest empty-body guard is also POST-only while live PATCH/DELETE paths go through the same function. Neither issue affects current correctness, but both are worth addressing before the provider sees heavy usage.

terraform/provider/litellm/client.go — dead organisation/team CRUD methods and the narrow empty-body guard in sendRequest.

Important Files Changed

Filename Overview
.github/workflows/test-terraform-provider.yml New CI workflow with two jobs: provider-checks (gofmt/vet/build/test) and endpoint-drift (OpenAPI schema validation); action hashes are pinned, permissions scoped to contents:read, concurrency group configured correctly.
terraform/provider/litellm/client.go Central HTTP client; contains ~8 dead-code methods (org/team CRUD) never called by resources, inflating the endpoint audit count; empty-body guard in sendRequest only covers POST, leaving PATCH/DELETE paths unprotected.
terraform/provider/litellm/resource_organization.go Fixes the core bug: update path changed from POST to PATCH /organization/update via MakeRequest; uses package-level endpoint consts that the audit tool can statically resolve.
terraform/provider/litellm/resource_organization_member.go Uses client helper methods (sendRequest under the hood) for PATCH/DELETE operations; read is a no-op stub since no single-member read endpoint exists.
terraform/provider/tools/endpointaudit/main.go Static AST extractor that resolves string literals, package consts, local reassignments, and fmt.Sprintf compositions; fails closed on unresolved or raw http.NewRequest call sites; logic is correct and well-tested.
terraform/provider/tools/dump_openapi.py Minimal script to dump proxy OpenAPI schema; missing argument count check — IndexError on bare invocation with no path argument.
terraform/provider/litellm/utils.go MakeRequest helper and not-found error detectors for model, MCP server, credential, and vector store resources; uses deprecated ioutil but otherwise clean.

Comments Outside Diff (1)

  1. terraform/provider/litellm/client.go, line 484-488 (link)

    P2 Empty-body guard in sendRequest only covers POST

    The guard that handles empty or "null" response bodies is gated on method == "POST". Both UpdateOrganizationMember (PATCH /organization/member_update) and DeleteOrganizationMember (DELETE /organization/member_delete) route through sendRequest, and both are called by the live resource implementations. If the proxy ever returns an empty body for a PATCH or DELETE, json.Unmarshal will fail and the error propagates as an unexpected failure rather than a clean success. Expanding the guard to cover PATCH and DELETE would make the behaviour consistent.

Reviews (2): Last reviewed commit: "feat(terraform): vendor terraform-provid..." | Re-trigger Greptile

Comment thread terraform/provider/litellm/client.go
Comment thread terraform/provider/tools/dump_openapi.py
@yassin-berriai
yassin-berriai force-pushed the litellm_terraform_provider_source_of_truth branch from b465f9b to a32aebb Compare July 6, 2026 10:02
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai addressed the empty-body fallback (now covers PATCH/PUT/DELETE alongside POST) and added an argument check to dump_openapi.py. The ioutil and redaction notes are pre-existing code imported verbatim from the split repo; keeping them out of this PR's scope

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Thanks for the follow-up. The two addressed items look good:

  • Empty-body fallback — removing the method == "POST" guard so all methods benefit is the right call.
  • dump_openapi.py argument check — the usage message prevents the raw IndexError traceback in CI.

The decision to scope out the ioutil and credential_values regex issues as pre-existing imported code is reasonable for this PR. Two small suggestions for follow-up tracking:

  1. ioutil deprecation — worth a quick sed -i 's/ioutil.ReadAll/io.ReadAll/g' pass in a cleanup PR; it's a mechanical change with no behavior risk.
  2. Nested-object redaction — the [^}]* flat-object pattern could leak secrets if any credential_values ever contains a nested JSON object. This one is a bit higher-priority than a style cleanup since it's a correctness issue in a log-sanitization path.

Happy to mark both client.go threads as resolved if you want to track them separately.

Comment thread terraform/provider/litellm/resource_credential.go
Comment thread terraform/provider/litellm/resource_vector_store.go
@veria-ai

veria-ai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 5 · PR risk: 0/10

Comment thread terraform/provider/litellm/resource_model.go
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai please review the current head (a32aebb); the previous review was pinned to the pre-amend commit b465f9b. The empty-body guard now covers PATCH/PUT/DELETE (client.go:360) and dump_openapi.py validates its argument count. On the other two threads: the redaction regex hardening is a fair catch and worth doing properly (balanced-brace handling, not a regex tweak), so I am tracking it with the ioutil cleanup as a follow-up in the monorepo rather than growing this import PR

Replace deprecated io/ioutil with io. Remove the unused org/team CRUD
client methods so the endpoint audit only tracks live call sites
(54 -> 46). Redact request/response logs by parsing the JSON and
recursively masking sensitive fields, which fixes the nested-object
leak in the old credential_values regex, with a regex fallback for
non-JSON payloads; covered by new unit tests. Docs: stop showing
api_key inside vector store litellm_params and document that Sensitive
attributes still persist in plaintext state, recommending
litellm_credential_name and an encrypted state backend.
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai please review the current head df520ad. It addresses the remaining threads: io/ioutil replaced with io, the dead org/team CRUD client methods removed (audit now verifies 46 live call sites), log redaction rewritten to parse the JSON and recursively mask sensitive fields with unit tests covering the nested credential_values case, and docs updated so no example puts api_key inside litellm_params and the secrets-in-state behavior is documented with litellm_credential_name as the recommended path

…ector store state

The vector store Read wrote litellm_params straight back from the API
response into state. The proxy redacts secrets in those responses, so
the readback overwrote user config with redaction sentinels and caused
perpetual diffs, and against a server that returns raw values it would
persist secrets into a non-Sensitive attribute. Read now preserves the
config value like the credential and model resources do, litellm_params
is marked Sensitive, and a regression test pins that a server-returned
api_key never lands in state
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai please review the current head 8ef3b51 (supersedes df520ad, which addressed the earlier threads). The new commit stops the vector store Read from persisting server-returned litellm_params into state, marks litellm_params Sensitive, and adds a regression test

Comment thread terraform/provider/litellm/resource_team_member.go
Comment thread terraform/provider/litellm/resource_mcp_server.go
…erver env into MCP state

The team member update payload omitted role, and the proxy leaves role
unchanged when the field is absent, so a role downgrade reported as
applied by Terraform never took effect on the proxy. The update now
always sends the configured role (the attribute is Required).

The MCP server resource wrote env straight back from API responses
into a non-Sensitive attribute, pulling admin-visible secrets into
state and, for sanitized responses, blanking user config. Read now
preserves the config value, env is marked Sensitive, and the docs warn
against passing secrets via args. Regression tests cover both fixes
and fail against the previous behavior.
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai please review the current head 5504387. Since your last review: 8ef3b51 stops the vector store Read from persisting server-returned litellm_params into state and marks it Sensitive; 5504387 fixes the team member update dropping the role field (a role downgrade never reached the proxy) and applies the same no-readback plus Sensitive treatment to the MCP server env attribute. Both commits add regression tests that fail against the previous behavior

@yassin-berriai
yassin-berriai enabled auto-merge (squash) July 7, 2026 16:15
@yassin-berriai
yassin-berriai merged commit ce2582e into litellm_internal_staging Jul 7, 2026
128 checks passed
@yassin-berriai
yassin-berriai deleted the litellm_terraform_provider_source_of_truth branch July 7, 2026 16:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants