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:
@@ -8,7 +8,7 @@ defmodule ArcadiaCloudWeb.DeploymentController do
|
||||
|
||||
use ArcadiaCloudWeb, :controller
|
||||
|
||||
alias ArcadiaCloud.{Catalog, Deployments, Subscriptions}
|
||||
alias ArcadiaCloud.{Catalog, Deployments, Provisioning, Subscriptions}
|
||||
|
||||
def index(conn, params) do
|
||||
identity = conn.assigns.current_identity
|
||||
@@ -30,7 +30,8 @@ defmodule ArcadiaCloudWeb.DeploymentController do
|
||||
json(conn, %{
|
||||
deployment: shape(deployment),
|
||||
subscription: shape_subscription(sub),
|
||||
events: Enum.map(events, &shape_event/1)
|
||||
events: Enum.map(events, &shape_event/1),
|
||||
provisioning: shape_provisioning(deployment.id)
|
||||
})
|
||||
else
|
||||
{:halt, conn} -> conn
|
||||
@@ -52,15 +53,60 @@ defmodule ArcadiaCloudWeb.DeploymentController do
|
||||
|> Map.put("tenant_id", tenant_id)
|
||||
|> Map.put("actor", identity.email)
|
||||
|
||||
case Deployments.create_deployment(attrs) do
|
||||
{:ok, deployment} ->
|
||||
conn |> put_status(:created) |> json(%{deployment: shape(deployment)})
|
||||
if truthy(params["provision"]) do
|
||||
create_and_provision(conn, params, attrs, identity)
|
||||
else
|
||||
case Deployments.create_deployment(attrs) do
|
||||
{:ok, deployment} ->
|
||||
conn |> put_status(:created) |> json(%{deployment: shape(deployment)})
|
||||
|
||||
{:error, changeset} ->
|
||||
conn |> put_status(:unprocessable_entity) |> json(%{error: errors(changeset)})
|
||||
{:error, changeset} ->
|
||||
conn |> put_status(:unprocessable_entity) |> json(%{error: errors(changeset)})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Creates the deployment row in `provisioning` and kicks off the
|
||||
# choreography saga. The deployment only reaches `active` once the
|
||||
# saga's ActivateDeployment step runs; a saga failure rolls it to
|
||||
# `cancelled`. Returns 202 — provisioning is asynchronous.
|
||||
defp create_and_provision(conn, params, attrs, identity) do
|
||||
size = params["size"]
|
||||
image = params["image"]
|
||||
|
||||
cond do
|
||||
is_nil(size) or is_nil(image) ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{error: "size and image are required to provision"})
|
||||
|
||||
true ->
|
||||
case Deployments.create_deployment(Map.put(attrs, "state", "provisioning")) do
|
||||
{:ok, deployment} ->
|
||||
{:ok, saga} =
|
||||
Provisioning.provision_deployment(deployment,
|
||||
size: size,
|
||||
image: image,
|
||||
region: params["region"] || deployment.region,
|
||||
dns_domain: params["dns_domain"],
|
||||
dns_record_name: params["dns_record_name"],
|
||||
triggered_by: identity.email
|
||||
)
|
||||
|
||||
conn
|
||||
|> put_status(:accepted)
|
||||
|> json(%{deployment: shape(deployment), saga_id: saga.id})
|
||||
|
||||
{:error, changeset} ->
|
||||
conn |> put_status(:unprocessable_entity) |> json(%{error: errors(changeset)})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp truthy(true), do: true
|
||||
defp truthy("true"), do: true
|
||||
defp truthy(_), do: false
|
||||
|
||||
def transition(conn, %{"id" => id, "to_state" => to_state} = params) do
|
||||
with {:ok, deployment} <- fetch_scoped(conn, id) do
|
||||
case Deployments.transition_state(deployment, to_state,
|
||||
@@ -166,6 +212,37 @@ defmodule ArcadiaCloudWeb.DeploymentController do
|
||||
}
|
||||
end
|
||||
|
||||
defp shape_provisioning(deployment_id) do
|
||||
case Provisioning.list_sagas(deployment_id: deployment_id, limit: 1) do
|
||||
[saga | _] ->
|
||||
%{
|
||||
saga_id: saga.id,
|
||||
kind: saga.kind,
|
||||
status: saga.status,
|
||||
current_step_idx: saga.current_step_idx,
|
||||
started_at: saga.started_at,
|
||||
completed_at: saga.completed_at,
|
||||
error: saga.error,
|
||||
steps:
|
||||
saga.id
|
||||
|> Provisioning.list_step_results()
|
||||
|> Enum.map(fn r ->
|
||||
%{
|
||||
step_idx: r.step_idx,
|
||||
step_name: r.step_name,
|
||||
status: r.status,
|
||||
error: r.error,
|
||||
started_at: r.started_at,
|
||||
completed_at: r.completed_at
|
||||
}
|
||||
end)
|
||||
}
|
||||
|
||||
[] ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp shape_event(e) do
|
||||
%{
|
||||
from_state: e.from_state,
|
||||
|
||||
Reference in New Issue
Block a user