Deployment and lifecycle management for Nix
0
fork

Configure Feed

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

refactor: extract build_deployment_request and build_seed_report into Garden.Socket.State

SOW-74

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+111 -30
+18 -30
apps/garden/lib/garden/socket.ex
··· 4 4 require Logger 5 5 6 6 alias Garden.Scheduler 7 + alias Garden.Socket.State 7 8 alias Garden.Storage 8 9 alias SowerClient.Orchestration.DeploymentStatus 9 10 alias SowerClient.Orchestration.SeedDeploymentStatus ··· 15 16 16 17 @impl Slipstream 17 18 def handle_cast({:deployment_request, %{sid: sid}, force?}, socket) do 18 - request_payload = 19 - if force? do 20 - %{subscription_sids: [sid], force: true} 21 - else 22 - %{subscription_sids: [sid]} 23 - end 24 - 25 - {:ok, upgrade_request} = 26 - SowerClient.Orchestration.DeploymentRequest.new(request_payload) 27 - 19 + {:ok, upgrade_request} = State.build_deployment_request(sid, force?) 28 20 {:ok, _ref} = push_message(socket, upgrade_request) 29 21 30 22 {:noreply, socket} ··· 32 24 33 25 @impl Slipstream 34 26 def handle_cast(:report_seeds, socket) do 35 - storage = Storage.read() 36 - subscriptions = Map.get(storage, :subscriptions, []) 37 - 38 - report = Garden.Profile.collect_profiles_for_subscriptions(subscriptions) 39 - 40 - if not Enum.empty?(subscriptions) and Enum.empty?(report.profiles) do 41 - Logger.debug( 42 - msg: "No profiles found for any targets", 43 - subscription_count: length(subscriptions) 44 - ) 27 + subscriptions = Map.get(Storage.read(), :subscriptions, []) 45 28 46 - {:noreply, socket} 47 - else 48 - Logger.debug( 49 - msg: "Reporting seed profiles", 50 - profile_count: length(report.profiles), 51 - subscription_count: length(subscriptions) 52 - ) 29 + case State.build_seed_report(subscriptions) do 30 + :no_profiles -> 31 + Logger.debug( 32 + msg: "No profiles found for any targets", 33 + subscription_count: length(subscriptions) 34 + ) 53 35 54 - topic = private_channel(socket) 55 - {:ok, _ref} = push(socket, topic, "garden:seeds:report", report) 36 + {:report, report} -> 37 + Logger.debug( 38 + msg: "Reporting seed profiles", 39 + profile_count: length(report.profiles), 40 + subscription_count: length(subscriptions) 41 + ) 56 42 57 - {:noreply, socket} 43 + {:ok, _ref} = push(socket, private_channel(socket), "garden:seeds:report", report) 58 44 end 45 + 46 + {:noreply, socket} 59 47 end 60 48 61 49 @impl Slipstream
+37
apps/garden/lib/garden/socket/state.ex
··· 1 + defmodule Garden.Socket.State do 2 + @moduledoc """ 3 + Pure state transition functions for Garden.Socket. 4 + 5 + Each function takes relevant state and returns a result without 6 + performing side effects. The socket callbacks are thin wrappers 7 + that call these functions and execute the returned effects. 8 + """ 9 + 10 + alias SowerClient.Orchestration.DeploymentRequest 11 + 12 + def build_seed_report( 13 + subscriptions, 14 + collect_profiles_fun \\ &Garden.Profile.collect_profiles_for_subscriptions/1 15 + ) do 16 + report = collect_profiles_fun.(subscriptions) 17 + 18 + if not Enum.empty?(subscriptions) and Enum.empty?(report.profiles) do 19 + :no_profiles 20 + else 21 + {:report, report} 22 + end 23 + end 24 + 25 + def build_deployment_request(sid, force?) do 26 + payload = %{subscription_sids: [sid]} 27 + 28 + payload = 29 + if force? do 30 + Map.put(payload, :force, true) 31 + else 32 + payload 33 + end 34 + 35 + DeploymentRequest.new(payload) 36 + end 37 + end
+55
apps/garden/test/garden/socket/state_test.exs
··· 1 + defmodule Garden.Socket.StateTest do 2 + use ExUnit.Case, async: true 3 + 4 + alias Garden.Socket.State 5 + alias SowerClient.Orchestration.DeploymentRequest 6 + alias SowerClient.Orchestration.GardenSeedsReport 7 + 8 + describe "build_deployment_request/2" do 9 + test "builds request with subscription sid" do 10 + {:ok, %DeploymentRequest{} = request} = State.build_deployment_request("sub_123", false) 11 + 12 + assert request.subscription_sids == ["sub_123"] 13 + assert request.request_id != nil 14 + end 15 + 16 + test "sets force flag when true" do 17 + {:ok, %DeploymentRequest{} = request} = State.build_deployment_request("sub_123", true) 18 + 19 + assert request.force == true 20 + end 21 + 22 + test "omits force flag when false" do 23 + {:ok, %DeploymentRequest{} = request} = State.build_deployment_request("sub_123", false) 24 + 25 + assert request.force == false 26 + end 27 + end 28 + 29 + describe "build_seed_report/1" do 30 + test "returns report when profiles are found" do 31 + subscriptions = [%{seed_type: "nixos", seed_name: "host", rules: []}] 32 + 33 + report = 34 + GardenSeedsReport.cast!(%{ 35 + profiles: [%{profile_path: "/nix/var/nix/profiles/system", tags: %{}, generations: []}] 36 + }) 37 + 38 + assert {:report, ^report} = 39 + State.build_seed_report(subscriptions, fn _subs -> report end) 40 + end 41 + 42 + test "returns no_profiles when subscriptions exist but no profiles found" do 43 + subscriptions = [%{seed_type: "nixos", seed_name: "host", rules: []}] 44 + report = GardenSeedsReport.cast!(%{profiles: []}) 45 + 46 + assert :no_profiles = State.build_seed_report(subscriptions, fn _subs -> report end) 47 + end 48 + 49 + test "returns report when subscriptions are empty" do 50 + report = GardenSeedsReport.cast!(%{profiles: []}) 51 + 52 + assert {:report, ^report} = State.build_seed_report([], fn _subs -> report end) 53 + end 54 + end 55 + end
+1
flake.nix
··· 57 57 # elixir 58 58 beamPackages.erlang 59 59 beamPackages.elixir 60 + beamPackages.expert 60 61 beamPackages.hex 61 62 62 63 # elixir deps build deps