defmodule ArcadiaCloud.Sync.BackupsWorker do @moduledoc """ Sync of DO automated droplet backups. Backups are not exposed via /v2/snapshots — they live under each droplet at /v2/droplets/:id/backups. We iterate every active droplet in inventory and pull its backups, normalizing them as kind="droplet_backup" with the parent droplet_id in attrs. """ use Oban.Worker, queue: :cloud_sync_full, max_attempts: 3 import Ecto.Query alias ArcadiaCloud.Cloud alias ArcadiaCloud.Cloud.CloudResource alias ArcadiaCloud.DigitalOcean.Client alias ArcadiaCloud.Repo @kind "droplet_backup" @provider "digitalocean" @impl Oban.Worker def perform(_job) do now = DateTime.utc_now() |> DateTime.truncate(:second) droplets = list_active_droplets() Enum.each(droplets, fn d -> case Client.list_droplet_backups(d.provider_id) do {:ok, backups} -> Enum.each(backups, fn b -> Cloud.upsert_resource(normalize(b, d, now)) end) {:error, _} -> # Soft-fail per droplet; mark_stale below handles disappearances. :skip end end) Cloud.mark_stale(@kind, now) :ok end defp list_active_droplets do from(r in CloudResource, where: r.provider == ^@provider and r.kind == "droplet" and is_nil(r.deleted_at) and r.status != "archived", select: %{id: r.id, provider_id: r.provider_id, cloud_project_id: r.cloud_project_id, tenant_id: r.tenant_id} ) |> Repo.all() end defp normalize(b, droplet, now) do region = case b["regions"] do [first | _] when is_binary(first) -> first _ -> nil end %{ provider: @provider, provider_id: to_string(b["id"]), kind: @kind, name: b["name"] || "backup-#{b["id"]}", region: region, status: "active", tags: [], cloud_project_id: droplet.cloud_project_id, tenant_id: droplet.tenant_id, attrs: %{ droplet_id: droplet.provider_id, size_gigabytes: b["size_gigabytes"], min_disk_size: b["min_disk_size"], regions: b["regions"], do_created_at: b["created_at"] }, first_seen_at: now, last_seen_at: now } end end