diff --git a/terraform/provider/CHANGELOG.md b/terraform/provider/CHANGELOG.md index 4123b7e6b51..d8c53e1432c 100644 --- a/terraform/provider/CHANGELOG.md +++ b/terraform/provider/CHANGELOG.md @@ -60,6 +60,7 @@ longer signal it. - **key**: Updates no longer send an empty `budget_duration`, which the proxy rejects with a 400; any update to a key without a configured `budget_duration` previously failed outright - **key**: A config-supplied `key` value (write-only) is now forwarded to `/key/generate`; previously it was silently dropped and the proxy generated a random key instead - **security**: The `litellm_key` data source and `litellm_key_block` resource normalize raw `sk-` keys to their SHA-256 token hash before building request URLs and resource IDs, so plaintext keys no longer land in reverse-proxy access logs, Terraform plan output, or state IDs +- **unified_access_group**: create now accepts any 2xx response instead of requiring exactly HTTP 200; `POST /v1/unified_access_group` legitimately returns 201, so creation previously succeeded on the proxy but failed in the provider, leaving the group out of state and forcing a `terraform import` to recover on the next apply's 409. Same fix and shape as the one already applied to `mcp_server`/`model`/`key`/`organization_member`; `handleResponse` (shared by several other resources) was the one status-check helper that fix didn't reach. The legacy `litellm_access_group` resource calls the unrelated `/access_group/new` endpoint, which already returns 200, so it was never affected ### Changed diff --git a/terraform/provider/litellm/resource_team.go b/terraform/provider/litellm/resource_team.go index bf7d2508077..622fec7d0a0 100644 --- a/terraform/provider/litellm/resource_team.go +++ b/terraform/provider/litellm/resource_team.go @@ -466,7 +466,7 @@ func toStringSlice(v interface{}) []string { } func handleResponse(resp *http.Response, action string) error { - if resp.StatusCode != http.StatusOK { + if resp.StatusCode < 200 || resp.StatusCode >= 300 { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("error %s: %s - %s", action, resp.Status, string(body)) } diff --git a/terraform/provider/litellm/resource_team_test.go b/terraform/provider/litellm/resource_team_test.go index 35d60401d30..2aa8043ad89 100644 --- a/terraform/provider/litellm/resource_team_test.go +++ b/terraform/provider/litellm/resource_team_test.go @@ -434,3 +434,41 @@ func TestTeamLimitTypesSentOnCreateOnly(t *testing.T) { } } } + +func TestHandleResponseAcceptsFullSuccessRange(t *testing.T) { + tests := []struct { + name string + statusCode int + wantErr bool + }{ + {name: "200 OK", statusCode: http.StatusOK, wantErr: false}, + {name: "201 Created", statusCode: http.StatusCreated, wantErr: false}, + {name: "202 Accepted", statusCode: http.StatusAccepted, wantErr: false}, + {name: "204 No Content", statusCode: http.StatusNoContent, wantErr: false}, + {name: "400 Bad Request", statusCode: http.StatusBadRequest, wantErr: true}, + {name: "404 Not Found", statusCode: http.StatusNotFound, wantErr: true}, + {name: "409 Conflict", statusCode: http.StatusConflict, wantErr: true}, + {name: "500 Internal Server Error", statusCode: http.StatusInternalServerError, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rec := httptest.NewRecorder() + rec.WriteHeader(tt.statusCode) + rec.WriteString(`{"access_group_id":"ag-1","access_group_name":"uag-baseline"}`) + resp := rec.Result() + + err := handleResponse(resp, "creating unified access group") + + if tt.wantErr { + if err == nil { + t.Fatalf("handleResponse returned no error for status %d", tt.statusCode) + } + return + } + if err != nil { + t.Fatalf("handleResponse returned unexpected error for status %d: %v", tt.statusCode, err) + } + }) + } +}