fix(terraform): clear removed key aliases explicitly

This commit is contained in:
Julian Löffler 2026-09-28 10:10:42 +02:00
parent 25d4dc85be
commit 15a2b0f096
No known key found for this signature in database
4 changed files with 70 additions and 6 deletions

View file

@ -132,11 +132,10 @@ func (c *Client) UpdateKey(key *Key) (*Key, error) {
"blocked": key.Blocked,
}
// /key/generate omits an empty key_alias, so sending "" here would store an
// alias the key never had and collide with every other aliasless key on the
// proxy's uniqueness check.
if key.KeyAlias != "" {
updateData["key_alias"] = key.KeyAlias
} else if key.clearKeyAlias {
updateData["key_alias"] = nil
}
// The proxy keeps the stored metadata only when the field is absent, so nil means omit.

View file

@ -322,6 +322,7 @@ func resourceKeyUpdate(ctx context.Context, d *schema.ResourceData, m interface{
key := &Key{Key: d.Id()}
mapResourceDataToKey(d, key)
key.clearKeyAlias = d.HasChange("key_alias") && key.KeyAlias == ""
if !d.HasChange("duration") {
key.Duration = ""
}

View file

@ -321,9 +321,6 @@ func TestUpdateKeyOmitsEmptyBudgetDuration(t *testing.T) {
}
}
// /key/generate omits an empty key_alias, so an update that sends "" stores an
// alias the key never had, and the proxy then 400s every other aliasless key on
// its unique-alias check.
func TestUpdateKeyOmitsEmptyKeyAlias(t *testing.T) {
var captured map[string]interface{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -350,6 +347,72 @@ func TestUpdateKeyOmitsEmptyKeyAlias(t *testing.T) {
}
}
func TestResourceKeyAliasChangesConverge(t *testing.T) {
for _, tc := range []struct {
name string
priorAlias string
configuredAlias interface{}
wantAlias string
wantPresent bool
wantNull bool
}{
{name: "aliasless"},
{name: "remove", priorAlias: "alias-1", wantPresent: true, wantNull: true},
{name: "blank", priorAlias: "alias-1", configuredAlias: "", wantPresent: true, wantNull: true},
{name: "assign", configuredAlias: "alias-1", wantAlias: "alias-1", wantPresent: true},
{name: "rename", priorAlias: "alias-1", configuredAlias: "alias-2", wantAlias: "alias-2", wantPresent: true},
{name: "retain", priorAlias: "alias-1", configuredAlias: "alias-1", wantAlias: "alias-1", wantPresent: true},
} {
t.Run(tc.name, func(t *testing.T) {
storedAlias := tc.priorAlias
updates := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
if req.URL.Path == "/key/update" {
updates++
var payload map[string]interface{}
if err := json.NewDecoder(req.Body).Decode(&payload); err != nil {
t.Error(err)
w.WriteHeader(http.StatusBadRequest)
return
}
value, present := payload["key_alias"]
if present != tc.wantPresent || (present && (value == nil) != tc.wantNull) {
t.Errorf("key_alias = %#v, present = %v; want present = %v, null = %v", value, present, tc.wantPresent, tc.wantNull)
}
if present {
storedAlias, _ = value.(string)
}
}
json.NewEncoder(w).Encode(map[string]interface{}{
"key": "hash-1",
"info": map[string]interface{}{"key_alias": storedAlias, "team_id": "team-1"},
})
}))
defer srv.Close()
res := resourceKey()
priorData := newKeyResourceData(t, map[string]interface{}{"key_alias": tc.priorAlias, "team_id": "team-1", "max_budget": 10.0})
priorData.SetId("hash-1")
config := terraform.NewResourceConfigRaw(map[string]interface{}{"key_alias": tc.configuredAlias, "team_id": "team-1", "max_budget": 25.0})
diff, err := res.Diff(context.Background(), priorData.State(), config, nil)
if err != nil {
t.Fatal(err)
}
state, diags := res.Apply(context.Background(), priorData.State(), diff, NewClient(srv.URL, "test-key", true))
if diags.HasError() {
t.Fatalf("apply failed: %v", diags)
}
if updates != 1 || storedAlias != tc.wantAlias || state.Attributes["key_alias"] != tc.wantAlias {
t.Fatalf("updates = %d, stored alias = %q, state alias = %q; want %q", updates, storedAlias, state.Attributes["key_alias"], tc.wantAlias)
}
nextDiff, err := res.Diff(context.Background(), state, config, nil)
if err != nil || (nextDiff != nil && !nextDiff.Empty()) {
t.Fatalf("subsequent plan not clean: diff = %v, error = %v", nextDiff, err)
}
})
}
}
func TestResourceKeyUpdateFailureKeepsPriorState(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")

View file

@ -129,6 +129,7 @@ type ModelInfo struct {
// Key represents a LiteLLM API key.
type Key struct {
clearKeyAlias bool
Key string `json:"key,omitempty"`
TokenID string `json:"token_id,omitempty"`
Models []string `json:"models"`