Deployment and lifecycle management for Nix
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

web: dynamically update deployment page

+166 -6
+2
apps/sower/lib/sower/orchestration/deployment_pubsub.ex
··· 11 11 12 12 Broadcasts to multiple topics: 13 13 - "deployments" - Global topic for all deployments 14 + - "deployment:<deployment_sid>" - Per-deployment topic 14 15 - "deployments:agent:<agent_sid>" - Per-agent topic 15 16 - "deployments:subscription:<subscription_sid>" - Per-subscription topics 16 17 """ ··· 18 19 deployment = Sower.Repo.preload(deployment, [:agent, :subscriptions]) 19 20 20 21 broadcast("deployments", {:deployment, event, deployment}) 22 + broadcast("deployment:#{deployment.sid}", {:deployment, event, deployment}) 21 23 22 24 if deployment.agent do 23 25 broadcast(
+29 -6
apps/sower/lib/sower_web/live/deployment_live/show.ex
··· 132 132 end 133 133 134 134 @impl true 135 + def mount(%{"sid" => sid}, _session, socket) do 136 + if connected?(socket) do 137 + Phoenix.PubSub.subscribe(Sower.PubSub, "deployment:#{sid}") 138 + end 139 + 140 + {:ok, initialize_socket(socket)} 141 + end 142 + 135 143 def mount(_params, _session, socket) do 136 - {:ok, 137 - socket 138 - |> assign(:page_title, "Show Deployment") 139 - |> assign(:expanded_seed_logs, MapSet.new()) 140 - |> assign(:loaded_seed_logs, MapSet.new()) 141 - |> assign(:retrying, false)} 144 + {:ok, initialize_socket(socket)} 142 145 end 143 146 144 147 @impl true ··· 158 161 |> assign(:deployment, deployment) 159 162 |> assign(:expanded_seed_logs, MapSet.new()) 160 163 |> assign(:loaded_seed_logs, MapSet.new())} 164 + end 165 + end 166 + 167 + @impl true 168 + def handle_info({:deployment, _event, %Sower.Orchestration.Deployment{} = deployment}, socket) do 169 + case Orchestration.get_deployment_sid(deployment.sid) do 170 + nil -> 171 + {:noreply, socket} 172 + 173 + deployment -> 174 + deployment = Sower.Repo.preload(deployment, [:seeds, :subscriptions, :agent]) 175 + {:noreply, assign(socket, :deployment, deployment)} 161 176 end 162 177 end 163 178 ··· 223 238 224 239 defp retryable?(deployment) do 225 240 deployment.result in [:success, :failure] 241 + end 242 + 243 + defp initialize_socket(socket) do 244 + socket 245 + |> assign(:page_title, "Show Deployment") 246 + |> assign(:expanded_seed_logs, MapSet.new()) 247 + |> assign(:loaded_seed_logs, MapSet.new()) 248 + |> assign(:retrying, false) 226 249 end 227 250 end
+49
apps/sower/test/sower/orchestration/deployment_pubsub_test.exs
··· 1 + defmodule Sower.Orchestration.DeploymentPubSubTest do 2 + use Sower.DataCase, async: true 3 + 4 + alias Sower.Orchestration.DeploymentPubSub 5 + 6 + import Sower.AccountsFixtures 7 + import Sower.OrchestrationFixtures 8 + import Sower.SeedFixtures 9 + 10 + setup do 11 + org = organization_fixture() 12 + Sower.Repo.put_org_id(org.org_id) 13 + 14 + %{organization: org} 15 + end 16 + 17 + test "broadcast_deployment_change/2 publishes per-deployment topic", %{organization: org} do 18 + agent = agent_fixture(%{org_id: org.org_id}) 19 + seed = seed_fixture(%{org_id: org.org_id, name: "seed", seed_type: "nixos"}) 20 + 21 + subscription = 22 + subscription_fixture(%{ 23 + agent_id: agent.id, 24 + seed_name: seed.name, 25 + seed_type: seed.seed_type 26 + }) 27 + 28 + deployment = 29 + deployment_fixture(%{ 30 + org_id: org.org_id, 31 + agent_id: agent.id, 32 + seeds: [seed], 33 + subscriptions: [subscription] 34 + }) 35 + 36 + Phoenix.PubSub.subscribe(Sower.PubSub, "deployments") 37 + Phoenix.PubSub.subscribe(Sower.PubSub, "deployment:#{deployment.sid}") 38 + Phoenix.PubSub.subscribe(Sower.PubSub, "deployments:agent:#{agent.sid}") 39 + Phoenix.PubSub.subscribe(Sower.PubSub, "deployments:subscription:#{subscription.sid}") 40 + 41 + assert {:ok, _deployment} = DeploymentPubSub.broadcast_deployment_change(deployment, :updated) 42 + 43 + deployment_sid = deployment.sid 44 + 45 + Enum.each(1..4, fn _ -> 46 + assert_receive {:deployment, :updated, %{sid: ^deployment_sid}} 47 + end) 48 + end 49 + end
+86
apps/sower/test/sower_web/live/deployment_live_show_test.exs
··· 2 2 use SowerWeb.ConnCase, async: true 3 3 4 4 import Phoenix.LiveViewTest 5 + import Ecto.Changeset 6 + 7 + alias Sower.Orchestration.DeploymentPubSub 5 8 import Sower.OrchestrationFixtures 6 9 import Sower.SeedFixtures 7 10 ··· 60 63 61 64 assert html =~ "Seeds" 62 65 assert html =~ "No seeds." 66 + end 67 + 68 + test "subscribes to per-deployment topic on mount", %{conn: conn, user: user} do 69 + Sower.Repo.put_org_id(user.org_id) 70 + agent = agent_fixture() 71 + 72 + current_deployment = 73 + deployment_fixture(%{ 74 + agent_id: agent.id, 75 + result: nil, 76 + deployed_at: nil 77 + }) 78 + 79 + other_deployment = 80 + deployment_fixture(%{ 81 + agent_id: agent.id, 82 + result: nil, 83 + deployed_at: nil 84 + }) 85 + 86 + {:ok, show_live, _html} = live(conn, ~p"/deployments/#{current_deployment.sid}") 87 + refute has_element?(show_live, "button", "Retry") 88 + 89 + other_deployment 90 + |> change(%{result: :success, deployed_at: DateTime.utc_now() |> DateTime.truncate(:second)}) 91 + |> Sower.Repo.update!() 92 + 93 + assert {:ok, _deployment} = 94 + DeploymentPubSub.broadcast_deployment_change(other_deployment, :updated) 95 + 96 + refute has_element?(show_live, "button", "Retry") 97 + end 98 + 99 + test "refreshes deployment when update is broadcast", %{conn: conn, user: user} do 100 + Sower.Repo.put_org_id(user.org_id) 101 + agent = agent_fixture() 102 + 103 + deployment = 104 + deployment_fixture(%{ 105 + agent_id: agent.id, 106 + result: nil, 107 + deployed_at: nil 108 + }) 109 + 110 + {:ok, show_live, _html} = live(conn, ~p"/deployments/#{deployment.sid}") 111 + refute has_element?(show_live, "button", "Retry") 112 + 113 + deployment 114 + |> change(%{result: :success, deployed_at: DateTime.utc_now() |> DateTime.truncate(:second)}) 115 + |> Sower.Repo.update!() 116 + 117 + assert {:ok, _deployment} = DeploymentPubSub.broadcast_deployment_change(deployment, :updated) 118 + 119 + assert has_element?(show_live, "button", "Retry") 120 + end 121 + 122 + test "cleans up PubSub subscription on LiveView termination", %{conn: conn, user: user} do 123 + Sower.Repo.put_org_id(user.org_id) 124 + agent = agent_fixture() 125 + 126 + deployment = 127 + deployment_fixture(%{ 128 + agent_id: agent.id, 129 + result: nil, 130 + deployed_at: nil 131 + }) 132 + 133 + {:ok, show_live, _html} = live(conn, ~p"/deployments/#{deployment.sid}") 134 + 135 + topic = "deployment:#{deployment.sid}" 136 + 137 + assert Enum.any?(Registry.lookup(Sower.PubSub, topic), fn {pid, _} -> 138 + pid == show_live.pid 139 + end) 140 + 141 + monitor_ref = Process.monitor(show_live.pid) 142 + GenServer.stop(show_live.pid) 143 + 144 + assert_receive {:DOWN, ^monitor_ref, :process, _pid, _reason} 145 + 146 + refute Enum.any?(Registry.lookup(Sower.PubSub, topic), fn {pid, _} -> 147 + pid == show_live.pid 148 + end) 63 149 end 64 150 65 151 test "shows retry button only for terminal deployments", %{conn: conn, user: user} do