defmodule ArcadiaCloud.Provisioning.Steps.RegisterDroplet do @moduledoc """ Makes arcadia-cloud's own DB reflect a freshly-provisioned droplet without waiting for the next 15-minute sync: 1. fetch the droplet from DO 2. upsert it into cloud_resources (inventory immediately consistent) 3. record desired-state in cloud_provisioned (so drift detection has a baseline) Compensation: marks the cloud_resources row deleted and removes the cloud_provisioned row. The droplet itself is destroyed by the CreateDroplet step's compensate — this step only undoes DB rows. """ @behaviour ArcadiaCloud.Provisioning.Step import Ecto.Query alias ArcadiaCloud.{Cloud, Provisioning, Repo} alias ArcadiaCloud.Cloud.CloudResource alias ArcadiaCloud.DigitalOcean.Client alias ArcadiaCloud.Provisioning.{CloudProvisioned, SagaState} @impl true def name, do: "register_droplet" @impl true def execute(state) do case SagaState.get_output(state, :droplet_id) do nil -> {:error, :no_droplet_id_in_context} droplet_id -> with {:ok, droplet} <- Client.get_droplet(droplet_id), {:ok, resource} <- Cloud.upsert_resource(normalize(droplet)), {:ok, _prov} <- record_provisioned(state, droplet, resource) do {:ok, SagaState.put_output(state, :cloud_resource_id, resource.id)} end end end @impl true def compensate(state) do case SagaState.get_output(state, :cloud_resource_id) do nil -> :ok resource_id -> Repo.delete_all(from(p in CloudProvisioned, where: p.resource_id == ^resource_id)) from(r in CloudResource, where: r.id == ^resource_id) |> Repo.update_all(set: [deleted_at: DateTime.utc_now() |> DateTime.truncate(:second)]) :ok end end # ---- internals ------------------------------------------------------------ defp record_provisioned(state, droplet, resource) do Provisioning.record_provisioned( resource.id, %{ "size_slug" => droplet["size_slug"], "region" => get_in(droplet, ["region", "slug"]), "image" => get_in(droplet, ["image", "slug"]) }, provisioned_by: "saga:#{state.saga_id}", saga_id: state.saga_id ) end defp normalize(d) do now = DateTime.utc_now() |> DateTime.truncate(:second) %{ provider: "digitalocean", provider_id: to_string(d["id"]), kind: "droplet", name: d["name"], region: get_in(d, ["region", "slug"]), status: normalize_status(d["status"]), size_slug: d["size_slug"], tags: d["tags"] || [], attrs: %{ memory_mb: d["memory"], vcpus: d["vcpus"], disk_gb: d["disk"], networks: d["networks"], do_created_at: d["created_at"] }, first_seen_at: now, last_seen_at: now } end defp normalize_status("active"), do: "active" defp normalize_status("off"), do: "off" defp normalize_status("new"), do: "provisioning" defp normalize_status(other) when is_binary(other), do: other defp normalize_status(_), do: "unknown" end