internal/server/api/decommission_poke_test.go
Ref: Size: 4.2 KiB History
package api
import (
"encoding/json"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestDecommissionPokesAgent pins the fix for the decommission stall: the
// handler must wake the host's desired-state stream (like every VM mutation
// handler does), otherwise an online host never learns its VMs were tombstoned
// and stalls in `decommissioning` forever.
func TestDecommissionPokesAgent(t *testing.T) {
ts, _, _, _, a := newServer(t)
out := enroll(t, ts)
hostID := out["host_id"]
ch, cancel := a.hub.Subscribe(hostID)
defer cancel()
resp := do(t, "DELETE", ts.URL+"/api/v1/hosts/"+hostID, testPAT, nil)
require.Equal(t, http.StatusAccepted, resp.StatusCode)
select {
case <-ch:
case <-time.After(2 * time.Second):
t.Fatal("decommission must poke the host's desired-state stream")
}
}
// TestForceDecommissionRemovesHostWithVMs pins the dead-hardware escape hatch:
// ?force=true purges VM rows and removes the host immediately, without waiting
// for an agent drain that (for dead hardware) can never happen.
func TestForceDecommissionRemovesHostWithVMs(t *testing.T) {
ts, _, _, _, _ := newServer(t)
out := enroll(t, ts)
hostID := out["host_id"]
resp := do(t, "POST", ts.URL+"/api/v1/vms", testPAT, map[string]any{"host_id": hostID, "name": "vm-a"})
require.Equal(t, http.StatusCreated, resp.StatusCode)
// Graceful delete would only tombstone and wait; force removes now.
resp = do(t, "DELETE", ts.URL+"/api/v1/hosts/"+hostID+"?force=true", testPAT, nil)
require.Equal(t, http.StatusOK, resp.StatusCode)
hosts := decodeJSONKeys(t, do(t, "GET", ts.URL+"/api/v1/hosts", testPAT, nil))
assert.Empty(t, hosts, "force must remove the host immediately")
}
// TestForceDecommissionAuditNamesWhatItDestroyed pins the receipt for the one
// path that loses data on purpose. The volumes are deleted with the host and
// their ids exist nowhere afterwards, so this audit row is the only record an
// operator reconciling a lost host against backups has to work from.
func TestForceDecommissionAuditNamesWhatItDestroyed(t *testing.T) {
ts, st, _, reg, _ := newServer(t)
floorVolumes(t, "v0.0.7")
out := enroll(t, ts)
hostID := out["host_id"]
onlineAt(reg, hostID, "v0.0.7")
resp := do(t, "POST", ts.URL+"/api/v1/volume-claims", testPAT,
map[string]any{"name": "payroll", "size_gb": 5})
require.Equal(t, http.StatusCreated, resp.StatusCode)
var claim struct {
ID string `json:"id"`
}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&claim))
require.NotEmpty(t, claim.ID)
require.Equal(t, http.StatusCreated, do(t, "POST", ts.URL+"/api/v1/vms", testPAT,
map[string]any{"host_id": hostID, "name": "vm-a", "volume_claims": []string{claim.ID}}).StatusCode)
// The volume id as minted, read while the row still exists.
vols, err := st.ListVolumesForHost(hostID)
require.NoError(t, err)
require.Len(t, vols, 1)
require.Equal(t, http.StatusOK,
do(t, "DELETE", ts.URL+"/api/v1/hosts/"+hostID+"?force=true", testPAT, nil).StatusCode)
rows, err := st.ListAudit(testTenant, 10)
require.NoError(t, err)
require.NotEmpty(t, rows)
require.Equal(t, "host.decommission", rows[0].Action, "newest first")
var detail map[string]string
require.NoError(t, json.Unmarshal([]byte(rows[0].Detail), &detail))
assert.Equal(t, "true", detail["force"])
assert.Equal(t, "1", detail["vms_purged"])
assert.Equal(t, "1", detail["volumes_destroyed"])
assert.Equal(t, "1", detail["claims_unbound"])
assert.Equal(t, vols[0].ID, detail["volume_ids"], "the destroyed volume, named")
}
// TestCreateVMRejectedOnDecommissioningHost pins that a host mid-decommission
// no longer accepts new VMs (previously only the FK was enforced, so a create
// could land on a host being torn down).
func TestCreateVMRejectedOnDecommissioningHost(t *testing.T) {
ts, _, _, _, _ := newServer(t)
out := enroll(t, ts)
hostID := out["host_id"]
resp := do(t, "DELETE", ts.URL+"/api/v1/hosts/"+hostID, testPAT, nil)
require.Equal(t, http.StatusAccepted, resp.StatusCode)
resp = do(t, "POST", ts.URL+"/api/v1/vms", testPAT, map[string]any{"host_id": hostID, "name": "vm-late"})
assert.Equal(t, http.StatusConflict, resp.StatusCode, "create on a decommissioning host must be rejected")
}