Models: - cloud_projects: arcadia-cloud's mirror of DO Projects, indexed by (provider, provider_id); tenant_id + purpose classify each project. - cloud_resources: single unified resource table; kind-specific bits in attrs JSONB; first_seen_at / last_seen_at / stale_strike_count drive three-strike deletion. - cloud_resource_events: append-only audit (discovered, updated, deleted, drift_detected, tagged, restored). ArcadiaCloud.Cloud context owns the single upsert chokepoint that: - inserts new with `discovered` event - updates existing only when meaningful fields change - restores tombstoned rows seen again - bumps last_seen_at and resets strike count mark_stale/3 implements the three-strike rule. ArcadiaCloud.DigitalOcean.Client is a Req wrapper with auto-pagination. Per-purpose token resolution via .Tokens (phase 1: env DO_API_TOKEN; phase 2: vault). Per project_arcadia_cloud memory the long-term shape is one PAT per queue purpose for rate-limit isolation. ArcadiaCloud.Sync.Bootstrap ensures the skyai-internal DO Project exists on first sync, idempotent thereafter. ArcadiaCloud.Sync.DropletsWorker runs full droplet sync on the cloud_sync_full Oban queue. InventoryController wired to real data: platform_admin sees all, tenants see only their scope. Live smoke test against real DO: 5 droplets synced; skyai-internal project auto-created; events written; endpoint returns scoped results. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
63 lines
1.5 KiB
Elixir
63 lines
1.5 KiB
Elixir
defmodule ArcadiaCloudWeb.InventoryController do
|
|
@moduledoc """
|
|
Cloud resource inventory.
|
|
|
|
Scope rules (phase 1):
|
|
- platform_admin tenants see every non-deleted resource
|
|
- other tenants see only resources tagged to their tenant_id
|
|
|
|
Filters: `?kind=droplet`, `?deployment_id=...`
|
|
"""
|
|
|
|
use ArcadiaCloudWeb, :controller
|
|
|
|
alias ArcadiaCloud.Cloud
|
|
|
|
def index(conn, params) do
|
|
identity = conn.assigns.current_identity
|
|
|
|
base_opts =
|
|
[]
|
|
|> maybe_put(:kind, params["kind"])
|
|
|> maybe_put(:deployment_id, params["deployment_id"])
|
|
|
|
opts =
|
|
if platform_admin?(identity) do
|
|
base_opts
|
|
else
|
|
Keyword.put(base_opts, :tenant_id, identity.tenant_id)
|
|
end
|
|
|
|
resources =
|
|
Cloud.list_resources(opts)
|
|
|> Enum.map(&shape/1)
|
|
|
|
json(conn, %{resources: resources, count: length(resources)})
|
|
end
|
|
|
|
defp platform_admin?(%{roles: roles}) when is_list(roles), do: "platform_admin" in roles
|
|
defp platform_admin?(_), do: false
|
|
|
|
defp maybe_put(opts, _key, nil), do: opts
|
|
defp maybe_put(opts, _key, ""), do: opts
|
|
defp maybe_put(opts, key, value), do: Keyword.put(opts, key, value)
|
|
|
|
defp shape(r) do
|
|
%{
|
|
id: r.id,
|
|
provider: r.provider,
|
|
provider_id: r.provider_id,
|
|
kind: r.kind,
|
|
name: r.name,
|
|
region: r.region,
|
|
status: r.status,
|
|
size_slug: r.size_slug,
|
|
tenant_id: r.tenant_id,
|
|
deployment_id: r.deployment_id,
|
|
tags: r.tags,
|
|
first_seen_at: r.first_seen_at,
|
|
last_seen_at: r.last_seen_at
|
|
}
|
|
end
|
|
end
|