Phase 4a: deployment-provisioning choreography saga
Wire a full tenant deployment as one orchestrated, compensating saga: mark → create droplet → wait active → register in inventory → link to deployment → point DNS → activate. A failure anywhere rolls the whole thing back — droplet destroyed, DNS reverted, deployment moved to cancelled. - New lifecycle state `provisioning`; deployments created via the provision path enter here and only reach `active` once the saga's ActivateDeployment step runs. - Four new steps: MarkDeploymentProvisioning (owns the deployment's failure state), LinkDeploymentResource, PointDeploymentDns, ActivateDeployment. - Provisioning.provision_deployment/2 assembles + starts the saga. - DeploymentController: POST /deployments with provision:true creates in `provisioning` and kicks the saga (202); GET /deployments/:id now returns the provisioning saga + per-step progress. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
defmodule ArcadiaCloud.Provisioning.Steps.LinkDeploymentResource do
|
||||
@moduledoc """
|
||||
Attaches the freshly-registered cloud resource (set by RegisterDroplet
|
||||
as `cloud_resource_id` in context) to the saga's deployment: stamps
|
||||
`deployment_id` and `tenant_id` onto the `cloud_resources` row.
|
||||
|
||||
This is what makes the resource show up under the deployment in
|
||||
inventory and bill against the right tenant.
|
||||
|
||||
Idempotent: re-running just re-writes the same two columns.
|
||||
|
||||
Compensation: clears `deployment_id` and `tenant_id` back to nil. The
|
||||
resource row itself (and the droplet) are undone by RegisterDroplet /
|
||||
CreateDroplet compensation.
|
||||
"""
|
||||
|
||||
@behaviour ArcadiaCloud.Provisioning.Step
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias ArcadiaCloud.Deployments
|
||||
alias ArcadiaCloud.Cloud.CloudResource
|
||||
alias ArcadiaCloud.Provisioning.SagaState
|
||||
alias ArcadiaCloud.Repo
|
||||
|
||||
@impl true
|
||||
def name, do: "link_deployment_resource"
|
||||
|
||||
@impl true
|
||||
def execute(state) do
|
||||
with {:ok, resource_id} <- fetch(state, :cloud_resource_id),
|
||||
{:ok, deployment} <- fetch_deployment(state) do
|
||||
{_, _} =
|
||||
from(r in CloudResource, where: r.id == ^resource_id)
|
||||
|> Repo.update_all(
|
||||
set: [
|
||||
deployment_id: deployment.id,
|
||||
tenant_id: deployment.tenant_id,
|
||||
updated_at: DateTime.utc_now() |> DateTime.truncate(:second)
|
||||
]
|
||||
)
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def compensate(state) do
|
||||
case SagaState.get_output(state, :cloud_resource_id) do
|
||||
nil ->
|
||||
:ok
|
||||
|
||||
resource_id ->
|
||||
from(r in CloudResource, where: r.id == ^resource_id)
|
||||
|> Repo.update_all(set: [deployment_id: nil, tenant_id: nil])
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch(state, key) do
|
||||
case SagaState.get_output(state, key) do
|
||||
nil -> {:error, {:missing_context, key}}
|
||||
value -> {:ok, value}
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_deployment(state) do
|
||||
case state.saga && state.saga.deployment_id do
|
||||
nil ->
|
||||
{:error, :saga_has_no_deployment}
|
||||
|
||||
id ->
|
||||
case Deployments.get_deployment(id) do
|
||||
nil -> {:error, :deployment_not_found}
|
||||
deployment -> {:ok, deployment}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user