Deployment and lifecycle management for Nix
0
fork

Configure Feed

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

cleanup: remove local_sid and garden:hello handshake

With Boruta-only socket auth and API-based registration, local_sid
and the garden:hello channel handshake are no longer needed.

Removes:
- garden:hello handler and do_handle_hello from GardenChannel
- get_garden/2 hello-matching logic from Garden
- maybe_provision_oauth_client (only used by hello flow)
- get_garden_local_sid functions and delegates
- local_sid from Garden schema, changeset, JSON derive, and UI
- local_sid from Garden.Storage and default generation
- SowerClient.GardenHello schema module
- Dead authorize_private_join local_sid clause
- DB migration to drop local_sid column from gardens

sow-150, sow-153

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

+22 -246
+1 -1
apps/garden/lib/garden/socket.ex
··· 367 367 |> assign(:conn_sid, conn_sid) 368 368 |> assign(:garden_sid, garden_sid) 369 369 |> maybe_schedule_existing_reauth(storage.oauth_credentials) 370 - |> join("garden:#{garden_sid}", %{local_sid: storage.local_sid}) 370 + |> join("garden:#{garden_sid}", %{}) 371 371 372 372 {:ok, socket} 373 373 end
+2 -5
apps/garden/lib/garden/storage.ex
··· 4 4 5 5 require Logger 6 6 7 - @derive {Jason.Encoder, only: [:local_sid, :garden_sid]} 7 + @derive {Jason.Encoder, only: [:garden_sid]} 8 8 9 9 typedstruct do 10 - field :local_sid, String.t() 11 10 field :garden_sid, String.t() 12 11 field :subscriptions, list(SowerClient.Orchestration.Subscription) 13 12 field :oauth_credentials, map() ··· 119 118 end 120 119 121 120 defp default() do 122 - %__MODULE__{ 123 - local_sid: SowerClient.Sid.generate("lc_grdn") 124 - } 121 + %__MODULE__{} 125 122 end 126 123 end
-3
apps/sower/lib/sower/orchestration.ex
··· 14 14 defdelegate create_garden(attrs \\ %{}), to: Garden 15 15 defdelegate delete_garden(garden), to: Garden 16 16 defdelegate get_garden!(id), to: Garden 17 - defdelegate get_garden(hello, socket), to: Garden 18 17 defdelegate register_new_garden(attrs), to: Garden 19 - defdelegate get_garden_local_sid!(local_sid), to: Garden 20 - defdelegate get_garden_local_sid(local_sid), to: Garden 21 18 defdelegate get_garden_sid!(sid), to: Garden 22 19 defdelegate get_garden_sid(sid), to: Garden 23 20 defdelegate list_gardens(), to: Garden
+2 -143
apps/sower/lib/sower/orchestration/garden.ex
··· 2 2 use Ecto.Schema 3 3 import Ecto.Changeset 4 4 import Ecto.Query, warn: false 5 - import Sower.Authorization 6 5 7 6 alias Sower.Repo 8 7 alias Sower.Orchestration.Deployment 9 8 10 9 require Logger 11 10 12 - @derive {Jason.Encoder, only: [:sid, :local_sid]} 11 + @derive {Jason.Encoder, only: [:sid]} 13 12 @derive {Phoenix.Param, key: :sid} 14 13 15 14 @derive { ··· 26 25 schema "gardens" do 27 26 field :sid, SowerClient.Sid, autogenerate: true 28 27 field :name, :string 29 - field :local_sid, :string 30 28 field :org_id, Ecto.UUID 31 29 field :oauth_client_id, :string 32 30 ··· 42 40 @doc false 43 41 def changeset(garden, attrs) do 44 42 garden 45 - |> cast(attrs, [:name, :org_id, :local_sid, :oauth_client_id]) 43 + |> cast(attrs, [:name, :org_id, :oauth_client_id]) 46 44 |> validate_required([:name]) 47 45 end 48 46 ··· 86 84 Flop.validate_and_run(query, params, for: __MODULE__) 87 85 end 88 86 89 - def get_garden( 90 - %SowerClient.GardenHello{ 91 - garden_sid: nil, 92 - name: name, 93 - local_sid: local_sid, 94 - public_key: public_key 95 - }, 96 - socket 97 - ) do 98 - case get_garden_local_sid(local_sid) do 99 - nil -> 100 - Logger.debug( 101 - msg: "Registering new garden", 102 - name: name, 103 - local_sid: local_sid 104 - ) 105 - 106 - if socket.assigns.access_token |> can() |> create?(__MODULE__) do 107 - register_new_garden(%{name: name, local_sid: local_sid, public_key: public_key}) 108 - else 109 - {:error, :unauthorized} 110 - end 111 - 112 - %__MODULE__{} = garden -> 113 - Logger.error( 114 - msg: "Local garden attempted to re-register existing garden", 115 - name: garden.name, 116 - local_sid: local_sid, 117 - existing_garden_sid: garden.sid 118 - ) 119 - 120 - {:error, :unauthorized_garden_hello} 121 - end 122 - end 123 - 124 - def get_garden( 125 - %SowerClient.GardenHello{ 126 - garden_sid: garden_sid, 127 - name: name, 128 - local_sid: local_sid, 129 - public_key: public_key 130 - }, 131 - socket 132 - ) do 133 - case get_garden_sid(garden_sid) do 134 - nil -> 135 - Logger.debug( 136 - msg: "Local garden requested a missing garden", 137 - name: name, 138 - local_sid: local_sid, 139 - requested_garden_sid: garden_sid 140 - ) 141 - 142 - if socket.assigns.access_token |> can() |> create?(__MODULE__) do 143 - register_new_garden(%{name: name, local_sid: local_sid, public_key: public_key}) 144 - else 145 - {:error, :unauthorized} 146 - end 147 - 148 - %__MODULE__{local_sid: nil} = garden when garden.name == name -> 149 - Logger.debug( 150 - msg: "Registering local sid to existing garden", 151 - name: garden.name, 152 - local_sid: local_sid, 153 - garden_sid: garden.sid 154 - ) 155 - 156 - if socket.assigns.access_token |> can() |> create?(__MODULE__) do 157 - {:ok, garden} = update_garden(garden, %{local_sid: local_sid}) 158 - maybe_provision_oauth_client(garden, public_key) 159 - else 160 - {:error, :unauthorized_garden_hello} 161 - end 162 - 163 - %__MODULE__{} = garden 164 - when garden.sid == garden_sid and 165 - garden.name == name and 166 - garden.local_sid == local_sid -> 167 - Logger.debug( 168 - msg: "Found matching garden", 169 - name: garden.name, 170 - local_sid: local_sid, 171 - garden_sid: garden.sid 172 - ) 173 - 174 - maybe_provision_oauth_client(garden, public_key) 175 - 176 - %__MODULE__{} = garden 177 - when garden.sid == garden_sid and 178 - garden.name != name and 179 - garden.local_sid == local_sid -> 180 - Logger.info( 181 - msg: "Found matching garden with different name, renaming", 182 - name: name, 183 - previous_name: garden.name, 184 - local_sid: local_sid, 185 - garden_sid: garden.sid 186 - ) 187 - 188 - {:ok, garden} = update_garden(garden, %{name: name}) 189 - 190 - maybe_provision_oauth_client(garden, public_key) 191 - 192 - %__MODULE__{} = garden -> 193 - Logger.error( 194 - msg: "Invalid garden request", 195 - local_sid: local_sid, 196 - garden_sid: garden.sid 197 - ) 198 - 199 - {:error, :unauthorized_garden_hello} 200 - end 201 - end 202 - 203 87 def get_garden!(id), do: Repo.get!(__MODULE__, id) 204 88 205 89 def get_garden_sid!(sid), do: Repo.get_by!(__MODULE__, sid: sid) ··· 209 93 def get_by_oauth_client_id(client_id), 210 94 do: Repo.get_by(__MODULE__, [oauth_client_id: client_id], skip_org_id: true) 211 95 212 - def get_garden_local_sid(local_sid), do: Repo.get_by(__MODULE__, local_sid: local_sid) 213 - 214 - def get_garden_local_sid!(local_sid), do: Repo.get_by!(__MODULE__, local_sid: local_sid) 215 - 216 96 def register_new_garden(%{public_key: public_key} = attrs) do 217 97 with {:ok, garden} <- create_garden(attrs), 218 98 {:ok, client} <- Sower.GardenAuth.create_client(garden.sid, public_key), ··· 223 103 Logger.error(msg: "Failed to register new garden with OAuth", error: inspect(reason)) 224 104 {:error, reason} 225 105 end 226 - end 227 - 228 - defp maybe_provision_oauth_client(%__MODULE__{oauth_client_id: nil} = garden, public_key) 229 - when is_binary(public_key) do 230 - with {:ok, client} <- Sower.GardenAuth.create_client(garden.sid, public_key), 231 - {:ok, garden} <- update_garden(garden, %{oauth_client_id: client.id}) do 232 - {:ok, garden, %{client_id: client.id}} 233 - else 234 - {:error, reason} -> 235 - Logger.error( 236 - msg: "Failed to provision OAuth client for existing garden", 237 - garden_sid: garden.sid, 238 - error: inspect(reason) 239 - ) 240 - 241 - {:ok, garden} 242 - end 243 - end 244 - 245 - defp maybe_provision_oauth_client(%__MODULE__{} = garden, _public_key) do 246 - {:ok, garden} 247 106 end 248 107 249 108 def create_garden(attrs \\ %{}) do
-33
apps/sower/lib/sower_web/garden_channel.ex
··· 82 82 end 83 83 end 84 84 85 - defp authorize_private_join(topic_sid, %{"local_sid" => local_sid}, _access_token) do 86 - case Orchestration.get_garden_sid(topic_sid) do 87 - %{local_sid: ^local_sid} = garden when not is_nil(local_sid) -> {:ok, garden} 88 - _ -> :error 89 - end 90 - end 91 - 92 85 defp authorize_private_join(_topic_sid, _params, _access_token), do: :error 93 86 94 87 @impl Phoenix.Channel ··· 99 92 100 93 def handle_in("pong", %{"ref" => _ref}, socket) do 101 94 {:reply, :ok, socket} 102 - end 103 - 104 - def handle_in("garden:hello", payload, socket), do: do_handle_hello(payload, socket) 105 - 106 - defp do_handle_hello(payload, socket) do 107 - case payload 108 - |> SowerClient.GardenHello.cast!() 109 - |> Sower.Orchestration.get_garden(socket) do 110 - {:ok, garden, oauth_credentials} -> 111 - reply = %{ 112 - sid: garden.sid, 113 - local_sid: garden.local_sid, 114 - oauth_credentials: oauth_credentials 115 - } 116 - 117 - Logger.debug(msg: "Replying to hello with oauth credentials", garden_sid: garden.sid) 118 - {:reply, {:ok, reply}, assign(socket, :garden_sid, garden.sid)} 119 - 120 - {:ok, garden} -> 121 - Logger.debug(msg: "Replying to hello", garden: garden) 122 - {:reply, {:ok, garden}, assign(socket, :garden_sid, garden.sid)} 123 - 124 - {:error, error} -> 125 - Logger.error(msg: "Error returning hello", error: error) 126 - {:reply, {:error, error}, socket} 127 - end 128 95 end 129 96 130 97 handle_schema(SowerClient.Seed, &Sower.Orchestration.Seed.get_by_request/1)
-2
apps/sower/lib/sower_web/live/garden_live/show.html.heex
··· 15 15 </.header> 16 16 17 17 <div class="mt-8 space-y-10"> 18 - <.detail_field label="Local Sid">{@garden.local_sid}</.detail_field> 19 - 20 18 <section> 21 19 <div class="flex items-center justify-between mb-4"> 22 20 <h2 class="text-sm font-semibold text-zinc-900 dark:text-zinc-200">Seed Generations</h2>
+9
apps/sower/priv/repo/migrations/20260411162335_remove_local_sid_from_gardens.exs
··· 1 + defmodule Sower.Repo.Migrations.RemoveLocalSidFromGardens do 2 + use Ecto.Migration 3 + 4 + def change do 5 + alter table(:gardens) do 6 + remove :local_sid, :string 7 + end 8 + end 9 + end
+3 -4
apps/sower/test/sower/orchestration_test.exs
··· 30 30 end 31 31 32 32 test "create_garden/1 with valid data creates a garden" do 33 - valid_attrs = %{name: "some garden", local_sid: "some local_sid"} 33 + valid_attrs = %{name: "some garden"} 34 34 35 35 assert {:ok, %Garden{} = garden} = Orchestration.create_garden(valid_attrs) 36 36 assert garden.name == "some garden" 37 - assert garden.local_sid == "some local_sid" 38 37 end 39 38 40 39 test "create_garden/1 with invalid data returns error changeset" do ··· 43 42 44 43 test "update_garden/2 with valid data updates the garden" do 45 44 garden = garden_fixture() 46 - update_attrs = %{local_sid: "some updated local_sid"} 45 + update_attrs = %{name: "updated garden"} 47 46 48 47 assert {:ok, %Garden{} = garden} = Orchestration.update_garden(garden, update_attrs) 49 - assert garden.local_sid == "some updated local_sid" 48 + assert garden.name == "updated garden" 50 49 end 51 50 52 51 test "update_garden/2 with invalid data returns error changeset" do
-16
apps/sower/test/sower_web/channels/garden_channel_handle_in_test.exs
··· 10 10 end 11 11 end 12 12 13 - describe "garden:hello" do 14 - test "returns garden info for an existing garden" do 15 - %{socket: socket, garden: garden} = connect_and_join_garden() 16 - 17 - ref = 18 - push(socket, "garden:hello", %{ 19 - "garden_sid" => garden.sid, 20 - "local_sid" => garden.local_sid, 21 - "name" => garden.name 22 - }) 23 - 24 - assert_reply ref, :ok, reply, 1_000 25 - assert reply.sid == garden.sid 26 - end 27 - end 28 - 29 13 describe "deployment:request" do 30 14 test "returns request_id for valid deployment request" do 31 15 %{socket: socket, garden: garden} = connect_and_join_garden()
+3 -3
apps/sower/test/sower_web/channels/garden_channel_test.exs
··· 47 47 end 48 48 49 49 describe "join/3" do 50 - test "joins with matching local_sid and assigns garden" do 50 + test "joins and assigns garden" do 51 51 %{socket: socket, garden: garden} = connect_and_join_garden() 52 52 53 53 assert socket.assigns.garden.id == garden.id ··· 69 69 socket, 70 70 SowerWeb.GardenChannel, 71 71 "garden:nonexistent_sid", 72 - %{"local_sid" => "some_local_sid"} 72 + %{} 73 73 ) 74 74 end 75 75 ··· 140 140 socket, 141 141 SowerWeb.GardenChannel, 142 142 "garden:#{garden.sid}", 143 - %{"local_sid" => garden.local_sid} 143 + %{} 144 144 ) 145 145 146 146 assert_push "deployment", payload
+2 -3
apps/sower/test/support/channel_case.ex
··· 47 47 Sower.OrchestrationFixtures.garden_fixture( 48 48 Map.merge( 49 49 %{ 50 - sid: SowerClient.Sid.generate("grdn"), 51 - local_sid: SowerClient.Sid.generate("lc_grdn") 50 + sid: SowerClient.Sid.generate("grdn") 52 51 }, 53 52 attrs 54 53 ) ··· 102 101 socket, 103 102 SowerWeb.GardenChannel, 104 103 "garden:#{garden.sid}", 105 - %{"local_sid" => garden.local_sid} 104 + %{} 106 105 ) 107 106 108 107 %{socket: socket, garden: garden, user: user}
-1
apps/sower/test/support/fixtures/orchestration_fixtures.ex
··· 12 12 attrs 13 13 |> Enum.into(%{ 14 14 name: SowerClient.Sid.generate("grdn"), 15 - local_sid: "some local_sid", 16 15 sid: "some sid" 17 16 }) 18 17 |> Sower.Orchestration.create_garden()
-1
apps/sower_client/lib/sower_client.ex
··· 23 23 components: %OpenApiSpex.Components{schemas: %{}} 24 24 } 25 25 |> OpenApiSpex.add_schemas([ 26 - SowerClient.GardenHello, 27 26 SowerClient.GardenRegistration, 28 27 SowerClient.Auth.OAuthCredentials, 29 28 SowerClient.Auth.TokenInfo,
-31
apps/sower_client/lib/sower_client/garden_hello.ex
··· 1 - defmodule SowerClient.GardenHello do 2 - use SowerClient.Schema 3 - use SowerClient.ChannelMessage, event: "garden:hello", topic_type: :lobby 4 - 5 - OpenApiSpex.schema(%{ 6 - title: "GardenHello", 7 - type: :object, 8 - properties: %{ 9 - garden_sid: %Schema{ 10 - type: :string, 11 - description: "sid allocated by Sower", 12 - readOnly: true, 13 - nullable: true 14 - }, 15 - local_sid: %Schema{ 16 - type: :string, 17 - description: "sid allocated locally" 18 - }, 19 - name: %Schema{ 20 - type: :string, 21 - description: "Name of garden" 22 - }, 23 - public_key: %Schema{ 24 - type: :string, 25 - description: "PEM-encoded RSA public key for private_key_jwt authentication", 26 - nullable: true 27 - } 28 - }, 29 - required: [:local_sid, :name] 30 - }) 31 - end