fix(core): ShouldRemoveFromState читает deleted/404 через GetInstanceStateRaw без падения
This commit is contained in:
@@ -67,6 +67,14 @@ func CreateExistingResourceDiagnostics(ctx context.Context, client *core.Univers
|
||||
}
|
||||
|
||||
// ShouldRemoveFromState checks instance status and indicates if TF state should be removed.
|
||||
//
|
||||
// Возвращает (true, nil) ТОЛЬКО когда инстанс реально отсутствует на платформе:
|
||||
// - состояние ответило deleted (IsDeleted=true или ExplainedStatus=="deleted"), или
|
||||
// - API ответил 404 / "not found" (инстанс полностью стёрт).
|
||||
//
|
||||
// В остальных случаях (живой инстанс, сетевая ошибка, 5xx, 403 — нет прав)
|
||||
// возвращает (false, nil) или (false, err). Это принципиально: при временной
|
||||
// недоступности API нельзя молча вычищать ресурсы из state.
|
||||
func ShouldRemoveFromState(ctx context.Context, client *core.UniversalClient, instanceID string) (bool, error) {
|
||||
if client == nil {
|
||||
return false, nil
|
||||
@@ -75,20 +83,49 @@ func ShouldRemoveFromState(ctx context.Context, client *core.UniversalClient, in
|
||||
return false, nil
|
||||
}
|
||||
|
||||
state, err := client.GetInstanceState(ctx, instanceID)
|
||||
// Читаем БЕЗ валидации статуса: нужно отличить deleted/404 (убрать из state)
|
||||
// от прочих ошибок (пробросить как ошибку).
|
||||
state, err := client.GetInstanceStateRaw(ctx, instanceID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if state != nil {
|
||||
status := strings.ToLower(strings.TrimSpace(state.ExplainedStatus))
|
||||
if state.IsDeleted || status == "deleted" {
|
||||
if isInstanceNotFoundError(err) {
|
||||
return true, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
if state == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
status := strings.ToLower(strings.TrimSpace(state.ExplainedStatus))
|
||||
if state.IsDeleted || status == "deleted" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// isInstanceNotFoundError определяет, является ли ошибка чтения инстанса
|
||||
// признаком того, что инстанс полностью удалён (HTTP 404 / not found),
|
||||
// а не временным сбоем или отсутствием прав.
|
||||
func isInstanceNotFoundError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
msg := strings.ToLower(strings.TrimSpace(err.Error()))
|
||||
if msg == "" {
|
||||
return false
|
||||
}
|
||||
// Точный статус 404 — основной признак «инстанс стёрт».
|
||||
if strings.Contains(msg, "ошибка api 404") || strings.Contains(msg, "error:404") || strings.Contains(msg, "status 404") {
|
||||
return true
|
||||
}
|
||||
// Дополнительные маркеры «not found» (как в IsSubresourceMissingError).
|
||||
if strings.Contains(msg, "not found") && !strings.Contains(msg, "permission") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizeStatus(status string) string {
|
||||
return strings.ToLower(strings.TrimSpace(status))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package resources_core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"terraform-provider-nubes/internal/core"
|
||||
)
|
||||
|
||||
func makeShouldRemoveClient(handler http.HandlerFunc) (*core.UniversalClient, func()) {
|
||||
srv := httptest.NewServer(handler)
|
||||
c := &core.UniversalClient{
|
||||
HttpClient: srv.Client(),
|
||||
ApiEndpoint: srv.URL,
|
||||
}
|
||||
return c, func() { srv.Close() }
|
||||
}
|
||||
|
||||
func TestShouldRemoveFromState(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
handler http.HandlerFunc
|
||||
wantRemove bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "deleted статус",
|
||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"instance":{"instanceUid":"u1","explainedStatus":"deleted","isDeleted":true}}`))
|
||||
},
|
||||
wantRemove: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "isDeleted=true без статуса deleted",
|
||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"instance":{"instanceUid":"u1","explainedStatus":"running","isDeleted":true}}`))
|
||||
},
|
||||
wantRemove: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "HTTP 404 not found",
|
||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte(`"Instance not found or no permission to view"`))
|
||||
},
|
||||
wantRemove: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "живой инстанс",
|
||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"instance":{"instanceUid":"u1","explainedStatus":"running","isDeleted":false}}`))
|
||||
},
|
||||
wantRemove: false,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "HTTP 500 — НЕ удалять",
|
||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte(`{"message":"boom"}`))
|
||||
},
|
||||
wantRemove: false,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "HTTP 403 — НЕ удалять",
|
||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
_, _ = w.Write([]byte(`"permission denied"`))
|
||||
},
|
||||
wantRemove: false,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
client, cleanup := makeShouldRemoveClient(tc.handler)
|
||||
defer cleanup()
|
||||
|
||||
remove, err := ShouldRemoveFromState(context.Background(), client, "u1")
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Fatalf("expected err=%v, got err=%v", tc.wantErr, err)
|
||||
}
|
||||
if remove != tc.wantRemove {
|
||||
t.Fatalf("expected remove=%v, got remove=%v", tc.wantRemove, remove)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldRemoveFromState_NilClient(t *testing.T) {
|
||||
remove, err := ShouldRemoveFromState(context.Background(), nil, "u1")
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error for nil client, got %v", err)
|
||||
}
|
||||
if remove {
|
||||
t.Fatalf("expected remove=false for nil client")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldRemoveFromState_EmptyID(t *testing.T) {
|
||||
client, cleanup := makeShouldRemoveClient(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("should not make request for empty id")
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
remove, err := ShouldRemoveFromState(context.Background(), client, " ")
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error for empty id, got %v", err)
|
||||
}
|
||||
if remove {
|
||||
t.Fatalf("expected remove=false for empty id")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user