Deployment and lifecycle management for Nix
0
fork

Configure Feed

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

server: subscription rule handling

+242 -4
+34 -2
apps/sower/lib/sower/orchestration.ex
··· 318 318 end 319 319 320 320 alias Sower.Orchestration.Deployment 321 + alias Sower.Seed 321 322 322 - def match_seed(%Subscription{} = subscription) do 323 - Sower.Seed.latest(subscription.seed_name, subscription.seed_type) 323 + def match_seed(%Subscription{rules: rules} = subscription) do 324 + # Build subquery to find all matching seed IDs 325 + base_query = 326 + from s in Seed, 327 + where: s.name == ^subscription.seed_name and s.seed_type == ^subscription.seed_type, 328 + select: s.id 329 + 330 + matching_seed_ids = 331 + Enum.reduce(rules || [], base_query, fn rule, q -> 332 + case rule.op do 333 + :eq -> 334 + from s in q, 335 + join: t in assoc(s, :tags), 336 + where: t.key == ^rule.key and t.value == ^rule.value 337 + end 338 + end) 339 + |> distinct(true) 340 + |> Repo.all() 341 + 342 + # Now find the latest seed from the matching IDs 343 + case matching_seed_ids do 344 + [] -> 345 + nil 346 + 347 + ids -> 348 + from(s in Seed, 349 + where: s.id in ^ids, 350 + order_by: [desc: s.inserted_at, desc: s.id], 351 + limit: 1 352 + ) 353 + |> Repo.one() 354 + |> Repo.preload(:tags) 355 + end 324 356 end 325 357 326 358 @doc """
+8
apps/sower/lib/sower/orchestration/subscription.ex
··· 26 26 def changeset(subscription, attrs) do 27 27 subscription 28 28 |> cast(attrs, [:agent_id, :seed_name, :seed_type]) 29 + |> cast_embed(:rules, with: &__MODULE__.Rule.changeset/2) 29 30 |> unique_constraint([:agent_id, :org_id, :seed_name, :seed_type]) 30 31 end 31 32 32 33 defmodule Rule do 33 34 use Ecto.Schema 35 + import Ecto.Changeset 34 36 35 37 embedded_schema do 36 38 field :key, :string 37 39 field :op, Ecto.Enum, values: [:eq] 38 40 field :value, :string 41 + end 42 + 43 + def changeset(rule, attrs) do 44 + rule 45 + |> cast(attrs, [:key, :op, :value]) 46 + |> validate_required([:key, :op, :value]) 39 47 end 40 48 end 41 49 end
+3 -2
apps/sower/lib/sower_web/agent_channel.ex
··· 119 119 Sower.Orchestration.create_subscription(%{ 120 120 agent_id: socket.assigns.agent.id, 121 121 seed_name: req_sub.seed_name, 122 - seed_type: req_sub.seed_type 122 + seed_type: req_sub.seed_type, 123 + rules: req_sub.rules 123 124 }) do 124 125 subscription = 125 126 SowerClient.Schemas.Orchestration.Subscription.cast(subscription) ··· 131 132 132 133 {:error, error} -> 133 134 Logger.error( 134 - msg: "Failed to get seed", 135 + msg: "Failed to register subscription", 135 136 payload: payload, 136 137 error: error 137 138 )
+197
apps/sower/test/sower/orchestration_test.exs
··· 3 3 4 4 alias Sower.Orchestration 5 5 import Sower.AccountsFixtures 6 + import Sower.SeedFixtures 6 7 7 8 setup _ do 8 9 org = organization_fixture() ··· 63 64 test "change_agent/1 returns a agent changeset" do 64 65 agent = agent_fixture() 65 66 assert %Ecto.Changeset{} = Orchestration.change_agent(agent) 67 + end 68 + end 69 + 70 + describe "match_seed/1" do 71 + import Sower.OrchestrationFixtures 72 + 73 + test "returns nil when no seed matches name and type" do 74 + agent = agent_fixture() 75 + 76 + subscription = 77 + subscription_fixture(%{ 78 + agent_id: agent.id, 79 + seed_name: "nonexistent", 80 + seed_type: "nixos" 81 + }) 82 + 83 + assert Orchestration.match_seed(subscription) == nil 84 + end 85 + 86 + test "returns seed when name and type match with no rules" do 87 + agent = agent_fixture() 88 + seed = seed_fixture(%{name: "myhost", seed_type: "nixos"}) 89 + 90 + subscription = 91 + subscription_fixture(%{ 92 + agent_id: agent.id, 93 + seed_name: "myhost", 94 + seed_type: "nixos" 95 + }) 96 + 97 + matched = Orchestration.match_seed(subscription) 98 + assert matched.id == seed.id 99 + end 100 + 101 + test "returns seed when single rule matches" do 102 + agent = agent_fixture() 103 + 104 + seed = 105 + seed_fixture(%{ 106 + name: "myhost", 107 + seed_type: "nixos", 108 + tags: [%{key: "branch", value: "main"}] 109 + }) 110 + 111 + subscription = 112 + subscription_fixture(%{ 113 + agent_id: agent.id, 114 + seed_name: "myhost", 115 + seed_type: "nixos", 116 + rules: [%{key: "branch", op: :eq, value: "main"}] 117 + }) 118 + 119 + matched = Orchestration.match_seed(subscription) 120 + assert matched.id == seed.id 121 + end 122 + 123 + test "returns seed when all rules match" do 124 + agent = agent_fixture() 125 + 126 + seed = 127 + seed_fixture(%{ 128 + name: "myhost", 129 + seed_type: "nixos", 130 + tags: [ 131 + %{key: "branch", value: "main"}, 132 + %{key: "repo", value: "http://example.com/repo"} 133 + ] 134 + }) 135 + 136 + subscription = 137 + subscription_fixture(%{ 138 + agent_id: agent.id, 139 + seed_name: "myhost", 140 + seed_type: "nixos", 141 + rules: [ 142 + %{key: "branch", op: :eq, value: "main"}, 143 + %{key: "repo", op: :eq, value: "http://example.com/repo"} 144 + ] 145 + }) 146 + 147 + matched = Orchestration.match_seed(subscription) 148 + assert matched.id == seed.id 149 + assert length(matched.tags) == 2 150 + end 151 + 152 + test "returns seed when all rules match even if seed has more tags" do 153 + agent = agent_fixture() 154 + 155 + seed = 156 + seed_fixture(%{ 157 + name: "myhost", 158 + seed_type: "nixos", 159 + tags: [ 160 + %{key: "branch", value: "main"}, 161 + %{key: "repo", value: "http://example.com/repo"}, 162 + %{key: "sometag", value: "somevalue"} 163 + ] 164 + }) 165 + 166 + subscription = 167 + subscription_fixture(%{ 168 + agent_id: agent.id, 169 + seed_name: "myhost", 170 + seed_type: "nixos", 171 + rules: [ 172 + %{key: "branch", op: :eq, value: "main"}, 173 + %{key: "repo", op: :eq, value: "http://example.com/repo"} 174 + ] 175 + }) 176 + 177 + matched = Orchestration.match_seed(subscription) 178 + assert matched.id == seed.id 179 + end 180 + 181 + test "returns nil when rule does not match" do 182 + agent = agent_fixture() 183 + 184 + seed_fixture(%{ 185 + name: "myhost", 186 + seed_type: "nixos", 187 + tags: [%{key: "branch", value: "dev"}] 188 + }) 189 + 190 + subscription = 191 + subscription_fixture(%{ 192 + agent_id: agent.id, 193 + seed_name: "myhost", 194 + seed_type: "nixos", 195 + rules: [%{key: "branch", op: :eq, value: "main"}] 196 + }) 197 + 198 + assert Orchestration.match_seed(subscription) == nil 199 + end 200 + 201 + test "returns nil when only some rules match" do 202 + agent = agent_fixture() 203 + 204 + seed_fixture(%{ 205 + name: "myhost", 206 + seed_type: "nixos", 207 + tags: [ 208 + %{key: "branch", value: "main"} 209 + ] 210 + }) 211 + 212 + subscription = 213 + subscription_fixture(%{ 214 + agent_id: agent.id, 215 + seed_name: "myhost", 216 + seed_type: "nixos", 217 + rules: [ 218 + %{key: "branch", op: :eq, value: "main"}, 219 + %{key: "repo", op: :eq, value: "http://example.com/repo"} 220 + ] 221 + }) 222 + 223 + assert Orchestration.match_seed(subscription) == nil 224 + end 225 + 226 + test "returns latest seed when multiple seeds match" do 227 + agent = agent_fixture() 228 + 229 + artifact1 = random_nix_artifact() 230 + artifact2 = random_nix_artifact() 231 + 232 + _older_seed = 233 + seed_fixture(%{ 234 + name: "myhost", 235 + seed_type: "nixos", 236 + artifact: artifact1, 237 + tags: [%{key: "branch", value: "main"}] 238 + }) 239 + 240 + # Sleep to ensure different timestamps 241 + Process.sleep(10) 242 + 243 + _newer_seed = 244 + seed_fixture(%{ 245 + name: "myhost", 246 + seed_type: "nixos", 247 + artifact: artifact2, 248 + tags: [%{key: "branch", value: "main"}] 249 + }) 250 + 251 + subscription = 252 + subscription_fixture(%{ 253 + agent_id: agent.id, 254 + seed_name: "myhost", 255 + seed_type: "nixos", 256 + rules: [%{key: "branch", op: :eq, value: "main"}] 257 + }) 258 + 259 + matched = Orchestration.match_seed(subscription) 260 + # Verify we got the newer seed by checking the artifact 261 + # The newer seed should have artifact2 since it was created second 262 + assert matched.artifact == artifact2 66 263 end 67 264 end 68 265 end