Deployment and lifecycle management for Nix
0
fork

Configure Feed

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

server/agent: working subscription rule registration

also bumped to a fixed version of open_api_spex for 1.19

+204 -31
+1
.iex.exs
··· 1 + Sower.Accounts.Organization.list() |> List.first() |> Map.get(:org_id) |> Sower.Repo.put_org_id()
+3 -1
apps/incus_client/mix.exs
··· 28 28 [ 29 29 {:igniter, "~> 0.6", only: [:dev, :test]}, 30 30 {:jason, "~> 1.0"}, 31 - {:open_api_spex, "~> 3.0"}, 31 + {:open_api_spex, 32 + git: "https://github.com/adamcstephens/open_api_spex.git", 33 + ref: "d7ad0631b5689666d29115f27c21c5d397242888"}, 32 34 {:req, "~> 0.5"}, 33 35 {:typedstruct, "~> 0.5"} 34 36 ]
+7 -1
apps/sower/lib/sower/orchestration/subscription.ex
··· 34 34 use Ecto.Schema 35 35 import Ecto.Changeset 36 36 37 + @derive {Jason.Encoder, only: [:key, :op, :value]} 38 + 37 39 embedded_schema do 38 40 field :key, :string 39 - field :op, Ecto.Enum, values: [:eq] 41 + field :op, :string 40 42 field :value, :string 43 + end 44 + 45 + def changeset(rule, %SowerClient.Schemas.Orchestration.Subscription.Rule{} = attrs) do 46 + changeset(rule, Map.from_struct(attrs)) 41 47 end 42 48 43 49 def changeset(rule, attrs) do
-1
apps/sower/mix.exs
··· 48 48 {:mime, "~> 2.0.6"}, 49 49 {:mix_test_watch, "~> 1.0", only: [:dev, :test], runtime: false}, 50 50 {:nix, in_umbrella: true}, 51 - {:open_api_spex, "~> 3.20"}, 52 51 {:permit, "~> 0.3.0"}, 53 52 {:permit_ecto, "~> 0.2.3"}, 54 53 {:phoenix, "~> 1.8.0"},
+11 -2
apps/sower_agent/lib/sower_agent/config.ex
··· 40 40 }, 41 41 subscriptions: %Schema{ 42 42 type: :array, 43 - items: SowerClient.Schemas.Orchestration.Subscription.schema(), 43 + items: SowerClient.Schemas.Orchestration.Subscription, 44 44 default: [] 45 45 } 46 46 }, ··· 54 54 def load(config_map \\ %{}) do 55 55 Application.ensure_all_started(:logger) 56 56 57 + spec = 58 + %OpenApiSpex.OpenApi{ 59 + info: %OpenApiSpex.Info{title: "Config", version: "1.0.0"}, 60 + paths: %{}, 61 + components: nil 62 + } 63 + |> OpenApiSpex.resolve_schema_modules() 64 + |> OpenApiSpex.add_schemas([SowerAgent.Config]) 65 + 57 66 cfg = 58 67 defaults() 59 68 |> Map.merge(config_map) 60 69 |> add_config_file() 61 70 |> parse_files_to_values() 62 - |> OpenApiSpex.cast_value(schema()) 71 + |> OpenApiSpex.cast_value(spec.components.schemas["Config"], spec) 63 72 |> case do 64 73 {:ok, cfg} -> 65 74 cfg
-1
apps/sower_agent/mix.exs
··· 26 26 # Run "mix help deps" to learn about dependencies. 27 27 defp deps do 28 28 [ 29 - {:open_api_spex, "~> 3.0"}, 30 29 {:cuid2_ex, "~> 0.2"}, 31 30 {:deps_nix, "~> 2.0", only: [:dev]}, 32 31 {:igniter, "~> 0.6", only: [:dev, :test]},
+4 -2
apps/sower_client/lib/schema.ex
··· 5 5 require OpenApiSpex 6 6 7 7 def cast(attrs \\ %{}) do 8 - OpenApiSpex.cast_value(attrs, schema()) 8 + spec = SowerClient.spec() 9 + resolved_schema = spec.components.schemas[schema().title] 10 + OpenApiSpex.cast_value(attrs, resolved_schema, spec) 9 11 end 10 12 11 13 def cast!(attrs \\ %{}) do 12 - {:ok, val} = OpenApiSpex.cast_value(attrs, schema()) 14 + {:ok, val} = cast(attrs) 13 15 val 14 16 end 15 17 end
+21 -7
apps/sower_client/lib/schemas/orchestration/subscription.ex
··· 13 13 }, 14 14 seed_name: %Schema{ 15 15 type: :string, 16 - description: "Name of the seed" 16 + description: "Name of the seed", 17 + example: "myhost" 17 18 }, 18 19 seed_type: %Schema{ 19 20 type: :string, 20 21 description: "Type of the seed", 21 - enum: SowerClient.Schemas.Seed.seed_types() 22 + enum: SowerClient.Schemas.Seed.seed_types(), 23 + example: "nixos" 22 24 }, 23 25 rules: %Schema{ 24 26 type: :array, 25 27 items: __MODULE__.Rule, 26 - nullable: true 28 + default: [], 29 + description: "Tag-based rules to filter seeds" 27 30 } 28 31 }, 29 - required: [] 32 + required: [], 33 + example: %{ 34 + seed_name: "myhost", 35 + seed_type: "nixos", 36 + rules: [ 37 + %{key: "branch", op: "eq", value: "main"}, 38 + %{key: "repo", op: "eq", value: "https://github.com/example/repo"} 39 + ] 40 + } 30 41 }) 31 42 32 43 defmodule Rule do ··· 38 49 properties: %{ 39 50 key: %Schema{ 40 51 type: :string, 41 - description: "tag key" 52 + description: "tag key", 53 + example: "branch" 42 54 }, 43 55 op: %Schema{ 44 56 type: :string, 45 - description: "operation to apply" 57 + description: "operation to apply", 58 + enum: ["eq"] 46 59 }, 47 60 value: %Schema{ 48 61 type: :string, 49 - description: "value" 62 + description: "value", 63 + example: "main" 50 64 } 51 65 }, 52 66 required: [:key, :op, :value]
+15 -3
apps/sower_client/lib/sower_client.ex
··· 1 1 defmodule SowerClient do 2 - @moduledoc """ 3 - 4 - """ 2 + def spec() do 3 + %OpenApiSpex.OpenApi{ 4 + info: %OpenApiSpex.Info{ 5 + title: "SowerClient", 6 + version: to_string(Application.spec(:sower, :vsn)) 7 + }, 8 + paths: %{}, 9 + components: nil 10 + } 11 + |> OpenApiSpex.resolve_schema_modules() 12 + |> OpenApiSpex.add_schemas([ 13 + SowerClient.Schemas.AgentHello, 14 + SowerClient.Schemas.Orchestration.Subscription 15 + ]) 16 + end 5 17 end
+3 -1
apps/sower_client/mix.exs
··· 29 29 {:cuid2_ex, "~> 0.2"}, 30 30 {:igniter, "~> 0.6", only: [:dev, :test]}, 31 31 {:jason, "~> 1.0"}, 32 - {:open_api_spex, "~> 3.20"}, 32 + {:open_api_spex, 33 + git: "https://github.com/adamcstephens/open_api_spex.git", 34 + ref: "d7ad0631b5689666d29115f27c21c5d397242888"}, 33 35 {:req, "~> 0.5.14"} 34 36 ] 35 37 end
+1 -1
client-go/client.gen.go
··· 42 42 43 43 // Seed A seed is an installable unit 44 44 type Seed struct { 45 - // Artifact Store path of the seed 45 + // Artifact Artifact of the seed 46 46 Artifact string `json:"artifact"` 47 47 48 48 // Name Name of the seed
+7 -1
config/runtime.exs
··· 6 6 endpoint: "http://localhost:7150", 7 7 state_directory: Path.expand("../_build", __DIR__), 8 8 subscriptions: [ 9 - %{seed_name: "deck", seed_type: "nixos"}, 9 + %{ 10 + seed_name: "deck", 11 + seed_type: "nixos", 12 + rules: [ 13 + %{key: "branch", op: "eq", value: "main"} 14 + ] 15 + }, 10 16 %{seed_name: "deck", seed_type: "home-manager"} 11 17 ] 12 18 })
+120
docs/subscription-rules.md
··· 1 + # Subscription Rules 2 + 3 + Subscription rules allow agents to filter seeds based on tag values when requesting deployments. 4 + 5 + ## Overview 6 + 7 + When an agent subscribes to a seed, it can optionally specify rules that filter seeds by their tags. Only seeds matching ALL rules will be selected for deployment. 8 + 9 + ## Agent Configuration 10 + 11 + Configure subscriptions with rules in your agent config file or `config/runtime.exs`: 12 + 13 + ```elixir 14 + subscriptions: [ 15 + %{ 16 + seed_name: "myhost", 17 + seed_type: "nixos", 18 + rules: [ 19 + %{key: "branch", op: "eq", value: "main"}, 20 + %{key: "repo", op: "eq", value: "https://github.com/example/repo"} 21 + ] 22 + }, 23 + # Subscription without rules matches any seed with matching name/type 24 + %{ 25 + seed_name: "myhost", 26 + seed_type: "home-manager" 27 + } 28 + ] 29 + ``` 30 + 31 + ## Rule Schema 32 + 33 + Each rule has three fields: 34 + 35 + - **key** (string): The tag key to match (e.g., "branch", "repo", "environment") 36 + - **op** (string): The comparison operation. Currently supports: 37 + - `"eq"` - Equality check 38 + - **value** (string): The value to match against 39 + 40 + ## Seed Matching Behavior 41 + 42 + When a deployment is requested for a subscription with rules: 43 + 44 + 1. Filter seeds by `seed_name` and `seed_type` 45 + 2. For each rule, filter seeds that have a matching tag with `key` and `value` 46 + 3. ALL rules must match (AND logic) 47 + 4. Return the latest matching seed (by `inserted_at`) 48 + 49 + ### Examples 50 + 51 + **Seed 1:** 52 + ```elixir 53 + %{ 54 + name: "myhost", 55 + seed_type: "nixos", 56 + artifact: "/nix/store/abc...", 57 + tags: [ 58 + %{key: "branch", value: "main"}, 59 + %{key: "repo", value: "https://github.com/example/repo"} 60 + ] 61 + } 62 + ``` 63 + 64 + **Seed 2:** 65 + ```elixir 66 + %{ 67 + name: "myhost", 68 + seed_type: "nixos", 69 + artifact: "/nix/store/def...", 70 + tags: [ 71 + %{key: "branch", value: "dev"} 72 + ] 73 + } 74 + ``` 75 + 76 + **Subscription:** 77 + ```elixir 78 + %{ 79 + seed_name: "myhost", 80 + seed_type: "nixos", 81 + rules: [ 82 + %{key: "branch", op: "eq", value: "main"} 83 + ] 84 + } 85 + ``` 86 + 87 + **Result:** Seed 1 matches (has branch=main). Seed 2 does not match (has branch=dev). 88 + 89 + ## Creating Seeds with Tags 90 + 91 + When submitting seeds via CLI or API, include tags: 92 + 93 + ```bash 94 + # CLI (not yet implemented - placeholder) 95 + sower seed submit --name myhost --type nixos \ 96 + --artifact /nix/store/... \ 97 + --tag branch=main \ 98 + --tag repo=https://github.com/example/repo 99 + ``` 100 + 101 + Server-side Elixir: 102 + ```elixir 103 + Sower.Seed.create(%{ 104 + name: "myhost", 105 + seed_type: "nixos", 106 + artifact: "/nix/store/...", 107 + tags: [ 108 + %{key: "branch", value: "main"}, 109 + %{key: "repo", value: "https://github.com/example/repo"} 110 + ] 111 + }) 112 + ``` 113 + 114 + ## Future Enhancements 115 + 116 + Potential future rule operators: 117 + - `ne` - Not equal 118 + - `in` - Value in list 119 + - `regex` - Regular expression match 120 + - `gt`, `lt` - Greater/less than (for version comparisons)
+1 -1
mix.lock
··· 42 42 "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, 43 43 "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 44 44 "oidcc": {:hex, :oidcc, "3.6.0", "df03859fd2434f3545b1912d194b546318b28f95c58539aa20d87738a916080e", [:mix, :rebar3], [{:igniter, "~> 0.6.3", [hex: :igniter, repo: "hexpm", optional: true]}, {:jose, "~> 1.11", [hex: :jose, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.3.1", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "99b26b1db95d617150416b18a7a84bb09525007fdbbcf963a60edb6156c6a1ce"}, 45 - "open_api_spex": {:hex, :open_api_spex, "3.22.0", "fbf90dc82681dc042a4ee79853c8e989efbba73d9e87439085daf849bbf8bc20", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "dd751ddbdd709bb4a5313e9a24530da6e66594773c7242a0c2592cbd9f589063"}, 45 + "open_api_spex": {:git, "https://github.com/adamcstephens/open_api_spex.git", "d7ad0631b5689666d29115f27c21c5d397242888", [ref: "d7ad0631b5689666d29115f27c21c5d397242888"]}, 46 46 "owl": {:hex, :owl, "0.13.0", "26010e066d5992774268f3163506972ddac0a7e77bfe57fa42a250f24d6b876e", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "59bf9d11ce37a4db98f57cb68fbfd61593bf419ec4ed302852b6683d3d2f7475"}, 47 47 "permit": {:hex, :permit, "0.3.0", "9f54f86e9e19cbccd0779c68985a9b79eb9892a826d2edeb2997c60efe7a9f77", [:mix], [], "hexpm", "aac92428febf4e3856b90a267126a0c68183a86d7785ef70c9ea4bc07cc7764b"}, 48 48 "permit_ecto": {:hex, :permit_ecto, "0.2.4", "bb087a3bbb8caafbd6247d357bb98800f979592718965ddad026f623bb942bbc", [:mix], [{:ecto, ">= 3.11.2 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, ">= 3.11.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:permit, "~> 0.2", [hex: :permit, repo: "hexpm", optional: false]}], "hexpm", "4cc4a600d7331483674f5837a3f203d7a9b1cc1faf805a49f9ff5fd9ccc21ee9"},
+7 -6
nix/packages/deps.nix
··· 744 744 745 745 open_api_spex = 746 746 let 747 - version = "3.22.0"; 747 + version = "d7ad0631b5689666d29115f27c21c5d397242888"; 748 748 drv = buildMix { 749 749 inherit version; 750 750 name = "open_api_spex"; 751 751 appConfigPath = ../../config; 752 752 753 - src = fetchHex { 754 - inherit version; 755 - pkg = "open_api_spex"; 756 - sha256 = "dd751ddbdd709bb4a5313e9a24530da6e66594773c7242a0c2592cbd9f589063"; 753 + src = pkgs.fetchFromGitHub { 754 + owner = "adamcstephens"; 755 + repo = "open_api_spex"; 756 + rev = "d7ad0631b5689666d29115f27c21c5d397242888"; 757 + hash = "sha256-eBImBQNM2zY8yYtFz0gkynn9z4IHFE1E7gdeuuPNSIc="; 757 758 }; 758 759 759 760 beamDeps = [ 760 - decimal 761 761 jason 762 + decimal 762 763 plug 763 764 ]; 764 765 };
+3 -3
openapi.json
··· 43 43 "description": "A seed is an installable unit", 44 44 "example": { 45 45 "artifact": "/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-nixos", 46 - "id": "example4ser3adju75ddusbr", 47 46 "name": "myhost", 48 - "seed_type": "nixos" 47 + "seed_type": "nixos", 48 + "sid": "example4ser3adju75ddusbr" 49 49 }, 50 50 "properties": { 51 51 "artifact": { 52 - "description": "Store path of the seed", 52 + "description": "Artifact of the seed", 53 53 "type": "string", 54 54 "x-struct": null, 55 55 "x-validate": null