From 1312a16435a3a8039fd52e382711ce57617fdfc8 Mon Sep 17 00:00:00 2001 From: Matthew Howard <78384492+matthowardcohere@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:58:48 -0400 Subject: [PATCH] fix(credential): don't taint state on a failed adopt; carry model_id into the adopt-update Two bugs in the adopt-on-conflict path, both found in review: - d.SetId ran before the adopt PATCH could fail. A failed PATCH left a tainted entry for a credential this run doesn't own, so the next apply would destroy it. The ID now only sticks once the adopt actually succeeds. - The adopt path routed through resourceLiteLLMCredentialUpdate, whose request never carried model_id, so adopting a model_id-scoped credential skipped the proxy's model-based credential resolution and could silently apply the wrong values. --- .../litellm/resource_credential_crud.go | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/terraform/provider/litellm/resource_credential_crud.go b/terraform/provider/litellm/resource_credential_crud.go index ace0d97d5a0..cb5031ee04e 100644 --- a/terraform/provider/litellm/resource_credential_crud.go +++ b/terraform/provider/litellm/resource_credential_crud.go @@ -89,14 +89,24 @@ func resourceLiteLLMCredentialCreate(d *schema.ResourceData, m interface{}) erro err = handleCredentialAPIResponse(resp, nil, client) if err != nil { // If a credential with this name already exists, adopt it instead of - // failing. credential_name is the natural key, so we take ownership - // and update the existing credential to match the configured values + // failing: take ownership and update the existing credential's + // values (merged onto whatever it already had - not a full replace) // rather than erroring on the unique-constraint conflict. See // https://github.com/BerriAI/terraform-provider-litellm/issues/8. if err.Error() == "credential_conflict" { log.Printf("[WARN] Credential %q already exists; adopting it and updating to match configuration.", credentialName) d.SetId(credentialName) - return resourceLiteLLMCredentialUpdate(d, m) + if updateErr := resourceLiteLLMCredentialUpdate(d, m); updateErr != nil { + // Adoption failed before this run took ownership of + // anything real. Clear the ID so create is reported as + // failed outright (matching pre-adoption behavior) instead + // of tainting state for a credential this run doesn't own - + // state that would otherwise get destroyed on the next + // apply. + d.SetId("") + return fmt.Errorf("failed to adopt existing credential %q: %w", credentialName, updateErr) + } + return nil } return fmt.Errorf("failed to create credential: %w", err) } @@ -152,6 +162,7 @@ func resourceLiteLLMCredentialUpdate(d *schema.ResourceData, m interface{}) erro client := m.(*Client) credentialName := d.Id() + modelID := d.Get("model_id").(string) credentialInfo := d.Get("credential_info").(map[string]interface{}) credentialValues := d.Get("credential_values").(map[string]interface{}) @@ -167,8 +178,13 @@ func resourceLiteLLMCredentialUpdate(d *schema.ResourceData, m interface{}) erro credValuesMap[k] = v } + // model_id must travel with the update the same way it does on create, + // so the proxy's model-based credential resolution still applies. Without + // it, updating (or adopting) a model_id-scoped credential silently loses + // that association. credentialRequest := CredentialRequest{ CredentialName: credentialName, + ModelID: modelID, CredentialInfo: credInfoMap, CredentialValues: credValuesMap, }