From 7b3a003712392d1fca4ff17b535ffe62b8228889 Mon Sep 17 00:00:00 2001 From: libaojiang <2461443384@qq.com> Date: Tue, 13 Oct 2026 13:39:37 +0800 Subject: [PATCH] fix(terraform): treat archived deleted keys as gone in key resource /key/info serves archived keys with status deleted, so a key deleted outside Terraform stayed in state, plan showed no changes while calls returned 401, and /key/delete 404s on archived keys so destroy failed until state rm. Read now drops keys whose status is deleted and Delete treats a 404 as already gone. Fixes #46067 --- terraform/provider/litellm/client.go | 4 ++ terraform/provider/litellm/resource_key.go | 13 +++++++ .../provider/litellm/resource_key_test.go | 39 +++++++++++++++++++ terraform/provider/litellm/types.go | 1 + 4 files changed, 57 insertions(+) diff --git a/terraform/provider/litellm/client.go b/terraform/provider/litellm/client.go index 33ba4c41271..f483f97c118 100644 --- a/terraform/provider/litellm/client.go +++ b/terraform/provider/litellm/client.go @@ -359,6 +359,10 @@ func (c *Client) parseKeyResponse(resp map[string]interface{}) (*Key, error) { if m, ok := v.(map[string]interface{}); ok { createdKey.ModelTPMLimit = m } + case "status": + if s, ok := v.(string); ok { + createdKey.Status = s + } case "guardrails": if guardrails, ok := v.([]interface{}); ok { createdKey.Guardrails = make([]string, len(guardrails)) diff --git a/terraform/provider/litellm/resource_key.go b/terraform/provider/litellm/resource_key.go index 2c751ae6a34..c4aa5abc7a6 100644 --- a/terraform/provider/litellm/resource_key.go +++ b/terraform/provider/litellm/resource_key.go @@ -358,6 +358,14 @@ func resourceKeyRead(ctx context.Context, d *schema.ResourceData, m interface{}) return nil } + // The gateway archives deleted keys and still serves them from /key/info + // with status "deleted", so only that flag separates them from live keys. + if key.Status == "deleted" { + log.Printf("[WARN] Key %s is deleted on the gateway, removing from state", d.Id()) + d.SetId("") + return nil + } + d.Set("server_metadata", serverKeyMetadata(key.Metadata)) key.Metadata = declaredKeyMetadata(key.Metadata, d.Get("metadata").(map[string]interface{})) mapKeyToResourceData(d, key) @@ -531,6 +539,11 @@ func resourceKeyDelete(ctx context.Context, d *schema.ResourceData, m interface{ c := m.(*Client) err := c.DeleteKey(d.Id()) + if isNotFound(err) { + log.Printf("[WARN] Key %s already deleted on the gateway", d.Id()) + d.SetId("") + return nil + } if err != nil { return diag.FromErr(fmt.Errorf("error deleting key: %s", err)) } diff --git a/terraform/provider/litellm/resource_key_test.go b/terraform/provider/litellm/resource_key_test.go index 3ad13b6c83b..079d8da1c7e 100644 --- a/terraform/provider/litellm/resource_key_test.go +++ b/terraform/provider/litellm/resource_key_test.go @@ -897,6 +897,45 @@ func TestResourceKeyReadDropsMissingKeyFromState(t *testing.T) { } } +func TestResourceKeyReadDropsArchivedDeletedKeyFromState(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"key":"sk-archived","info":{"key":"sk-archived","token_id":"tok-1","status":"deleted"}}`)) + })) + defer srv.Close() + + d := newKeyResourceData(t, map[string]interface{}{"key_alias": "archived"}) + d.SetId("deleted-outside-terraform") + + diags := resourceKeyRead(context.Background(), d, NewClient(srv.URL, "test-key", true)) + if diags.HasError() { + t.Fatalf("read of an archived deleted key must not error, got: %v", diags) + } + if d.Id() != "" { + t.Errorf("Id = %q, want empty so Terraform plans a recreate instead of trusting a 200 for a deleted key", d.Id()) + } +} + +func TestResourceKeyDeleteTreatsNotFoundAsGone(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{"error":{"message":"{'error': 'No keys found'}","type":"internal_server_error","param":null,"code":"404"}}`)) + })) + defer srv.Close() + + d := newKeyResourceData(t, map[string]interface{}{"key_alias": "already-gone"}) + d.SetId("deleted-outside-terraform") + + diags := resourceKeyDelete(context.Background(), d, NewClient(srv.URL, "test-key", true)) + if diags.HasError() { + t.Fatalf("destroy of an already-deleted key must succeed, got: %v", diags) + } + if d.Id() != "" { + t.Errorf("Id = %q, want empty so destroy completes without state rm", d.Id()) + } +} + func TestResourceKeyReadStillFailsOnNon404Errors(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) diff --git a/terraform/provider/litellm/types.go b/terraform/provider/litellm/types.go index 8baf88edee1..8a687f4eedc 100644 --- a/terraform/provider/litellm/types.go +++ b/terraform/provider/litellm/types.go @@ -164,6 +164,7 @@ type Key struct { Prompts []string `json:"prompts,omitempty"` OrganizationID string `json:"organization_id,omitempty"` ProjectID string `json:"project_id,omitempty"` + Status string `json:"status,omitempty"` } // KeyResponse represents a response from the API containing key information.