Deployment and lifecycle management for Nix
0
fork

Configure Feed

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

feat: add name column to subscriptions

Decouple subscription identity from (seed_name, seed_type) by adding a
name column as the new unique key. The unique constraint is now
(garden_id, org_id, name) instead of (garden_id, org_id, seed_name,
seed_type).

- DB migration adds name column (not null, backfilled from seed_name)
- Server derives name from client struct, generates sid if absent
- Client schema adds optional name field (required in 0.9.0)
- Removes deployment_profile from client schema; deployer always uses
default profile

SOW-118

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

+65 -58
+8 -17
apps/garden/lib/garden/deployer.ex
··· 243 243 %DeploymentProfile{} 244 244 245 245 sub -> 246 - profile_name = 247 - case get_in(sub.deployment_profile) do 248 - nil -> 249 - default_profile_name = default_deployment_profile() 246 + profile_name = default_deployment_profile() 250 247 251 - Logger.info( 252 - msg: "Subscription deployment profile not found, using default", 253 - default_deployment_profile: default_profile_name, 254 - deploy_subscription_sid: subscription_sid, 255 - subscription_seed_name: get_in(sub.seed_name), 256 - subscription_seed_type: get_in(sub.seed_type) 257 - ) 258 - 259 - default_profile_name 260 - 261 - configured_profile_name -> 262 - configured_profile_name 263 - end 248 + Logger.info( 249 + msg: "Using default deployment profile", 250 + default_deployment_profile: profile_name, 251 + deploy_subscription_sid: subscription_sid, 252 + subscription_seed_name: get_in(sub.seed_name), 253 + subscription_seed_type: get_in(sub.seed_type) 254 + ) 264 255 265 256 subscription_overrides = find_profile.(profile_name) || %DeploymentProfile{} 266 257
+7 -17
apps/garden/test/garden/deployer_test.exs
··· 45 45 } 46 46 end 47 47 48 - test "uses subscription deployment_profile string to resolve named profile overrides" do 48 + test "uses default deployment profile to resolve profile overrides" do 49 49 sid = "sub_boot" 50 - profile_name = "boot_profile" 51 50 52 - sub = %Subscription{ 53 - sid: sid, 54 - deployment_profile: profile_name 55 - } 51 + sub = %Subscription{sid: sid} 56 52 57 53 find_sub = fn 58 54 ^sid -> sub ··· 60 56 end 61 57 62 58 find_profile = fn 63 - ^profile_name -> %{activation_args: ["boot"], reboot_policy: "always"} 59 + "default" -> %{activation_args: ["boot"], reboot_policy: "always"} 64 60 _ -> %{} 65 61 end 66 62 ··· 70 66 } 71 67 end 72 68 73 - test "keeps defaults for gardens not present in resolved profile overrides" do 69 + test "keeps defaults for fields not present in resolved profile overrides" do 74 70 sid = "sub_partial" 75 71 76 - sub = %Subscription{ 77 - sid: sid, 78 - deployment_profile: "partial_profile" 79 - } 72 + sub = %Subscription{sid: sid} 80 73 81 74 assert Deployer.get_deployment_profile( 82 75 sid, ··· 88 81 } 89 82 end 90 83 91 - test "falls back to defaults when named profile is not found" do 84 + test "falls back to defaults when profile is not found" do 92 85 sid = "sub_unknown_profile" 93 86 94 - sub = %Subscription{ 95 - sid: sid, 96 - deployment_profile: "missing_profile" 97 - } 87 + sub = %Subscription{sid: sid} 98 88 99 89 assert Deployer.get_deployment_profile( 100 90 sid,
+17 -19
apps/sower/lib/sower/orchestration/subscription.ex
··· 19 19 20 20 many_to_many :deployments, Deployment, join_through: SubscriptionDeployment 21 21 22 + field :name, :string 22 23 field :seed_name, :string 23 24 field :seed_type, :string 24 25 field :schedule, :string ··· 31 32 @doc false 32 33 def changeset(subscription, attrs) do 33 34 subscription 34 - |> cast(attrs, [:garden_id, :seed_name, :seed_type, :schedule, :timezone]) 35 + |> cast(attrs, [:garden_id, :name, :seed_name, :seed_type, :schedule, :timezone]) 35 36 |> cast_embed(:rules, with: &__MODULE__.Rule.changeset/2) 36 - |> unique_constraint([:garden_id, :org_id, :seed_name, :seed_type]) 37 + |> unique_constraint([:garden_id, :org_id, :name]) 37 38 end 38 39 39 40 def list_subscriptions do ··· 128 129 end 129 130 130 131 def create_subscription(attrs \\ %{}) do 132 + attrs = Map.put_new_lazy(attrs, :name, fn -> attrs[:seed_name] end) 133 + 131 134 case %__MODULE__{ 132 135 org_id: Repo.get_org_id(), 133 136 sid: SowerClient.Sid.generate("sub") 134 137 } 135 138 |> changeset(attrs) 136 139 |> Repo.insert( 137 - on_conflict: {:replace, [:updated_at, :rules, :schedule, :timezone]}, 138 - conflict_target: [:garden_id, :org_id, :seed_name, :seed_type], 140 + on_conflict: 141 + {:replace, [:updated_at, :seed_name, :seed_type, :rules, :schedule, :timezone]}, 142 + conflict_target: [:garden_id, :org_id, :name], 139 143 returning: true 140 144 ) do 141 145 {:ok, sub} -> {:ok, Repo.reload(sub)} ··· 143 147 end 144 148 end 145 149 146 - def register_subscription( 147 - %SowerClient.Orchestration.Subscription{ 148 - seed_name: seed_name, 149 - seed_type: seed_type, 150 - rules: rules, 151 - schedule: schedule, 152 - timezone: timezone 153 - }, 154 - garden_id 155 - ) do 150 + def register_subscription(%SowerClient.Orchestration.Subscription{} = sub, garden_id) do 151 + name = sub.name || SowerClient.Sid.generate("sub") 152 + 156 153 case create_subscription(%{ 157 154 garden_id: garden_id, 158 - seed_name: seed_name, 159 - seed_type: seed_type, 160 - rules: rules, 161 - schedule: schedule, 162 - timezone: timezone 155 + name: name, 156 + seed_name: sub.seed_name, 157 + seed_type: sub.seed_type, 158 + rules: sub.rules, 159 + schedule: sub.schedule, 160 + timezone: sub.timezone 163 161 }) do 164 162 {:ok, subscription} -> 165 163 {:ok, SowerClient.Orchestration.Subscription.cast!(subscription)}
+27
apps/sower/priv/repo/migrations/20260329024057_add_name_to_subscriptions.exs
··· 1 + defmodule Sower.Repo.Migrations.AddNameToSubscriptions do 2 + use Ecto.Migration 3 + 4 + def up do 5 + alter table(:subscriptions) do 6 + add :name, :string 7 + end 8 + 9 + execute "UPDATE subscriptions SET name = seed_name WHERE name IS NULL" 10 + 11 + alter table(:subscriptions) do 12 + modify :name, :string, null: false 13 + end 14 + 15 + drop unique_index(:subscriptions, [:garden_id, :org_id, :seed_name, :seed_type]) 16 + create unique_index(:subscriptions, [:garden_id, :org_id, :name]) 17 + end 18 + 19 + def down do 20 + drop unique_index(:subscriptions, [:garden_id, :org_id, :name]) 21 + create unique_index(:subscriptions, [:garden_id, :org_id, :seed_name, :seed_type]) 22 + 23 + alter table(:subscriptions) do 24 + remove :name 25 + end 26 + end 27 + end
+6 -5
apps/sower_client/lib/sower_client/orchestration/subscription.ex
··· 12 12 readOnly: true, 13 13 nullable: true 14 14 }, 15 + # TODO: make required in 0.9.0 once all gardens send name 16 + name: %Schema{ 17 + type: :string, 18 + description: "Human-readable subscription name", 19 + nullable: true 20 + }, 15 21 seed_name: %Schema{ 16 22 type: :string, 17 23 description: "Name of the seed", ··· 30 36 items: __MODULE__.Rule, 31 37 default: [], 32 38 description: "Tag-based rules to filter seeds" 33 - }, 34 - deployment_profile: %Schema{ 35 - type: :string, 36 - description: "Name of deployment profile to apply", 37 - nullable: true 38 39 }, 39 40 schedule: %Schema{ 40 41 type: :string,