defmodule ArcadiaCloud.Provisioning.Steps.DestroyDroplet do @moduledoc """ Destroys a droplet and marks its cloud_resources row deleted. Saga inputs: droplet_provider_id — required; the DO numeric droplet id This is a terminal, irreversible action. `compensate/1` is therefore a noop with a log line — a destroyed droplet cannot be un-destroyed. Per the saga design, destroy-class steps don't roll back; a saga that needs to fail-safe should sequence DestroyDroplet last. Idempotent: a droplet already gone (404) is treated as success. """ @behaviour ArcadiaCloud.Provisioning.Step import Ecto.Query require Logger alias ArcadiaCloud.Cloud.CloudResource alias ArcadiaCloud.DigitalOcean.Client alias ArcadiaCloud.Provisioning.SagaState alias ArcadiaCloud.Repo @impl true def name, do: "destroy_droplet" @impl true def execute(state) do droplet_id = SagaState.get_input(state, :droplet_provider_id) cond do is_nil(droplet_id) -> {:error, :missing_droplet_provider_id} true -> case Client.destroy_droplet(droplet_id) do {:ok, _} -> mark_resource_deleted(droplet_id) {:ok, SagaState.put_output(state, :destroyed_droplet_id, droplet_id)} {:error, {:http, 404, _}} -> mark_resource_deleted(droplet_id) {:ok, SagaState.put_output(state, :destroyed_droplet_id, droplet_id)} {:error, reason} -> {:error, reason} end end end @impl true def compensate(state) do Logger.warning( "[saga #{state.saga_id}] DestroyDroplet cannot be compensated — droplet destruction is terminal" ) :ok end defp mark_resource_deleted(provider_id) do from(r in CloudResource, where: r.provider == "digitalocean" and r.kind == "droplet" and r.provider_id == ^to_string(provider_id) ) |> Repo.update_all(set: [deleted_at: DateTime.utc_now() |> DateTime.truncate(:second)]) end end