Phase 1 first chunk: inventory schema + DO droplet sync
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>
This commit is contained in:
51
lib/arcadia_cloud/sync/bootstrap.ex
Normal file
51
lib/arcadia_cloud/sync/bootstrap.ex
Normal file
@@ -0,0 +1,51 @@
|
||||
defmodule ArcadiaCloud.Sync.Bootstrap do
|
||||
@moduledoc """
|
||||
First-run bootstrap: ensure the `skyai-internal` DO Project exists and
|
||||
is registered in our `cloud_projects` table. Resources discovered before
|
||||
any tenant project exists land here by default.
|
||||
|
||||
Idempotent: safe to call on every sync.
|
||||
"""
|
||||
|
||||
alias ArcadiaCloud.Cloud
|
||||
alias ArcadiaCloud.DigitalOcean.Client
|
||||
|
||||
@internal_name "skyai-internal"
|
||||
@internal_purpose "skyai-infra"
|
||||
@do_purpose "Service or API"
|
||||
|
||||
def ensure_skyai_internal do
|
||||
case Cloud.skyai_internal_project() do
|
||||
%{} = project ->
|
||||
{:ok, project}
|
||||
|
||||
nil ->
|
||||
with {:ok, do_project} <- find_or_create_do_project() do
|
||||
{:ok, _local} =
|
||||
Cloud.ensure_project(%{
|
||||
provider: "digitalocean",
|
||||
provider_id: do_project["id"],
|
||||
name: @internal_name,
|
||||
purpose: @internal_purpose,
|
||||
metadata: %{
|
||||
do_purpose: do_project["purpose"],
|
||||
description: do_project["description"]
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp find_or_create_do_project do
|
||||
with {:ok, projects} <- Client.list_projects() do
|
||||
case Enum.find(projects, &(&1["name"] == @internal_name)) do
|
||||
nil ->
|
||||
Client.create_project(@internal_name, @do_purpose,
|
||||
"Sky AI internal infrastructure (auto-created by arcadia-cloud)")
|
||||
|
||||
existing ->
|
||||
{:ok, existing}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user