Files
arcadia-cloud/lib/arcadia_cloud/provisioning/steps/mark_deployment_provisioning.ex
Giuliano Silvestro 29f4ad97d6 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>
2026-05-20 20:44:49 +10:00

58 lines
1.5 KiB
Elixir

defmodule ArcadiaCloud.Provisioning.Steps.MarkDeploymentProvisioning do
@moduledoc """
First step of the deployment-provisioning choreography.
Forward: a no-op — `Provisioning.provision_deployment/2` already
created the deployment row in the `provisioning` state. Having this
step at index 0 gives the saga a compensation hook that owns the
deployment's failure state: if ANY later step fails, the runner walks
compensation back to here and we move the deployment to `cancelled`.
Without this step a mid-saga failure would leave the deployment stuck
in `provisioning` forever.
"""
@behaviour ArcadiaCloud.Provisioning.Step
require Logger
alias ArcadiaCloud.Deployments
@impl true
def name, do: "mark_deployment_provisioning"
@impl true
def execute(state), do: {:ok, state}
@impl true
def compensate(state) do
case deployment(state) do
nil ->
:ok
deployment ->
case Deployments.transition_state(deployment, "cancelled",
reason: "provision_failed",
actor: "saga:#{state.saga_id}"
) do
{:ok, _} ->
:ok
{:error, reason} ->
Logger.warning(
"[saga #{state.saga_id}] could not cancel deployment on rollback: #{inspect(reason)}"
)
:ok
end
end
end
defp deployment(state) do
case state.saga && state.saga.deployment_id do
nil -> nil
id -> Deployments.get_deployment(id)
end
end
end