Deployment and lifecycle management for Nix
0
fork

Configure Feed

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

server: begin merging seed and store path

+252 -965
-140
apps/sower/lib/sower/nix.ex
··· 7 7 alias Sower.Repo 8 8 9 9 alias Sower.Nix.Cache 10 - alias Sower.Nix.StorePath 11 10 12 11 @doc """ 13 12 Returns the list of nix_caches. ··· 119 118 """ 120 119 def change_cache(%Cache{} = cache, attrs \\ %{}) do 121 120 Cache.changeset(cache, attrs) 122 - end 123 - 124 - @doc """ 125 - Returns the list of store_paths. 126 - 127 - ## Examples 128 - 129 - iex> list_store_paths() 130 - [%StorePath{}, ...] 131 - 132 - """ 133 - def list_store_paths do 134 - Repo.all(StorePath) 135 - end 136 - 137 - @doc """ 138 - Gets a single store_path. 139 - 140 - Raises `Ecto.NoResultsError` if the Store path does not exist. 141 - 142 - ## Examples 143 - 144 - iex> get_store_path!(123) 145 - %StorePath{} 146 - 147 - iex> get_store_path!(456) 148 - ** (Ecto.NoResultsError) 149 - 150 - """ 151 - def get_store_path!(id), do: Repo.get!(StorePath, id) 152 - 153 - @doc """ 154 - Gets a single store_path using digeste. 155 - 156 - Raises `Ecto.NoResultsError` if the Store path does not exist. 157 - 158 - ## Examples 159 - 160 - iex> get_store_path_digest!(123) 161 - %StorePath{} 162 - 163 - iex> get_store_path_digest!(456) 164 - ** (Ecto.NoResultsError) 165 - 166 - """ 167 - def get_store_path_digest!(digest), do: Repo.get_by!(StorePath, path_digest: digest) 168 - 169 - @doc """ 170 - Creates a store_path. 171 - 172 - ## Examples 173 - 174 - iex> create_store_path(%{field: value}) 175 - {:ok, %StorePath{}} 176 - 177 - iex> create_store_path(%{field: bad_value}) 178 - {:error, %Ecto.Changeset{}} 179 - 180 - """ 181 - def create_store_path(attrs \\ %{}) do 182 - %StorePath{ 183 - org_id: Sower.Repo.get_org_id() 184 - } 185 - |> StorePath.changeset(attrs) 186 - |> Repo.insert() 187 - end 188 - 189 - @doc """ 190 - Updates a store_path. 191 - 192 - ## Examples 193 - 194 - iex> update_store_path(store_path, %{field: new_value}) 195 - {:ok, %StorePath{}} 196 - 197 - iex> update_store_path(store_path, %{field: bad_value}) 198 - {:error, %Ecto.Changeset{}} 199 - 200 - """ 201 - def update_store_path(%StorePath{} = store_path, attrs) do 202 - store_path 203 - |> StorePath.changeset(attrs) 204 - |> Repo.update() 205 - end 206 - 207 - @doc """ 208 - Deletes a store_path. 209 - 210 - ## Examples 211 - 212 - iex> delete_store_path(store_path) 213 - {:ok, %StorePath{}} 214 - 215 - iex> delete_store_path(store_path) 216 - {:error, %Ecto.Changeset{}} 217 - 218 - """ 219 - def delete_store_path(%StorePath{} = store_path) do 220 - Repo.delete(store_path) 221 - end 222 - 223 - @doc """ 224 - Returns an `%Ecto.Changeset{}` for tracking store_path changes. 225 - 226 - ## Examples 227 - 228 - iex> change_store_path(store_path) 229 - %Ecto.Changeset{data: %StorePath{}} 230 - 231 - """ 232 - def change_store_path(%StorePath{} = store_path, attrs \\ %{}) do 233 - StorePath.changeset(store_path, attrs) 234 - end 235 - 236 - @doc """ 237 - Submit a full store path, updating timestamp on resubmit 238 - """ 239 - def submit_store_path!(%{path: _path} = attrs) do 240 - %StorePath{ 241 - org_id: Sower.Repo.get_org_id() 242 - } 243 - |> StorePath.changeset(attrs) 244 - |> Sower.Repo.insert!( 245 - on_conflict: {:replace, [:updated_at]}, 246 - conflict_target: [:path, :org_id], 247 - returning: true 248 - ) 249 - end 250 - 251 - # Submit a full store path with only a path 252 - def submit_store_path!(path) when is_binary(path) do 253 - submit_store_path!(%{path: path}) 254 - end 255 - 256 - @doc """ 257 - Get a store path by path 258 - """ 259 - def get_store_path_by_path!(path) when is_binary(path) do 260 - Sower.Repo.get_by!(StorePath, path: path) 261 121 end 262 122 end
-52
apps/sower/lib/sower/nix/store_path.ex
··· 1 - defmodule Sower.Nix.StorePath do 2 - use Sower.Schema 3 - import Ecto.Changeset 4 - 5 - @derive {Jason.Encoder, only: [:path, :path_digest]} 6 - @derive {Phoenix.Param, key: :path_digest} 7 - 8 - @path_regex ~r'/nix/store/(?<digest>[a-z0-9]{32})-[a-z0-9]+' 9 - 10 - schema "store_paths" do 11 - field :path, :string 12 - field :path_digest, Sower.Schema.Nix.StorePathDigest 13 - field :org_id, Ecto.UUID 14 - 15 - many_to_many :seeds, Sower.Seed, join_through: Sower.SeedStorePath 16 - 17 - many_to_many :deployments, Sower.Orchestration.Deployment, 18 - join_through: Sower.Orchestration.StorePathDeployment 19 - 20 - timestamps() 21 - end 22 - 23 - @doc false 24 - def changeset(store_path, attrs) do 25 - store_path 26 - |> cast(attrs, [:path]) 27 - |> validate_required([:path]) 28 - |> validate_format(:path, @path_regex, message: "must be a valid nix store path") 29 - |> unique_constraint(:path) 30 - |> unique_constraint(:path_digest) 31 - |> compute_digest() 32 - end 33 - 34 - def compute_digest(%Ecto.Changeset{} = changeset) do 35 - case get_field(changeset, :path_digest) do 36 - nil -> 37 - case get_field(changeset, :path) do 38 - nil -> 39 - changeset 40 - 41 - path -> 42 - %{"digest" => digest} = Regex.named_captures(@path_regex, path) 43 - 44 - changeset 45 - |> put_change(:path_digest, digest) 46 - end 47 - 48 - _ -> 49 - changeset 50 - end 51 - end 52 - end
+1 -1
apps/sower/lib/sower/orchestration.ex
··· 413 413 def request_subscription_deployment( 414 414 %SowerClient.Schemas.Orchestration.UpgradeRequest{} = upgrade 415 415 ) do 416 - with subs when length(subs) > 0 <- get_subscription_sids(upgrade.subscription_sids), 416 + with subs when subs != [] <- get_subscription_sids(upgrade.subscription_sids), 417 417 seed_store_paths <- 418 418 subs |> Enum.map(&Sower.Seed.latest_store_path(&1.seed)) |> Enum.map(& &1.store_path), 419 419 {:ok, deploy} <-
+2 -3
apps/sower/lib/sower/orchestration/deployment.ex
··· 14 14 many_to_many :subscriptions, Sower.Orchestration.Subscription, 15 15 join_through: Orchestration.SubscriptionDeployment 16 16 17 - many_to_many :store_paths, Sower.Nix.StorePath, 18 - join_through: Orchestration.StorePathDeployment 17 + many_to_many :seeds, Sower.Seed, join_through: Orchestration.SeedDeployment 19 18 20 19 field :deployed_at, :utc_datetime 21 20 ··· 26 25 def changeset(deployment, attrs) do 27 26 deployment 28 27 |> cast(attrs, [:deployed_at]) 29 - |> put_assoc(:store_paths, attrs.store_paths) 28 + |> put_assoc(:seeds, attrs.seeds) 30 29 |> put_assoc(:subscriptions, attrs.subscriptions) 31 30 |> validate_required([]) 32 31 end
+5 -5
apps/sower/lib/sower/orchestration/store_path_deployment.ex
··· 1 - defmodule Sower.Orchestration.StorePathDeployment do 1 + defmodule Sower.Orchestration.SeedDeployment do 2 2 use Sower.Schema 3 3 import Ecto.Changeset 4 4 5 - schema "store_paths_deployments" do 6 - field :store_path_id, :id 5 + schema "seed_deployments" do 6 + field :seed_id, :id 7 7 field :deployment_id, :id 8 8 9 9 timestamps() 10 10 end 11 11 12 12 @doc false 13 - def changeset(store_path_deployment, attrs) do 14 - store_path_deployment 13 + def changeset(seed_deployment, attrs) do 14 + seed_deployment 15 15 |> cast(attrs, []) 16 16 |> validate_required([]) 17 17 end
+3 -6
apps/sower/lib/sower/repo/seeds/org.ex
··· 66 66 Sower.Seed.create(%{ 67 67 name: name, 68 68 seed_type: "nixos", 69 - org_id: user.org_id 69 + org_id: user.org_id, 70 + store_path: 71 + ~s"/nix/store/#{Cuid2Ex.create(length: 32) |> String.downcase()}-nixos-system-#{name}-24.11.20240703.9f4128e" 70 72 }) 71 73 72 74 seed -> 73 75 {:ok, seed} 74 76 end 75 - 76 - Sower.Seed.submit( 77 - seed, 78 - ~s"/nix/store/#{Cuid2Ex.create(length: 32) |> String.downcase()}-nixos-system-#{name}-24.11.20240703.9f4128e" 79 - ) 80 77 end) 81 78 end 82 79 end
-11
apps/sower/lib/sower/schema.ex
··· 8 8 @foreign_key_type :id 9 9 end 10 10 end 11 - 12 - defmodule Nix.StorePathDigest do 13 - use Ecto.Type 14 - 15 - @type t :: :string 16 - def type, do: :string 17 - def cast(value), do: {:ok, value} 18 - def load(value), do: {:ok, value} 19 - def dump(value) when is_binary(value), do: {:ok, value} 20 - def dump(_), do: :error 21 - end 22 11 end
+26 -48
apps/sower/lib/sower/seed.ex
··· 4 4 import Ecto.Changeset 5 5 import Ecto.Query, only: [from: 2] 6 6 7 - alias Sower.{Orchestration, Nix, Repo, Seed, SeedStorePath} 7 + alias Sower.{Nix, Repo, Seed} 8 8 9 9 @derive {Jason.Encoder, only: [:sid, :name, :seed_type]} 10 10 ··· 14 14 15 15 schema "seeds" do 16 16 field :sid, SowerClient.Schemas.Sid, autogenerate: true 17 - field :name, :string 18 - field :seed_type, :string 19 17 field :org_id, Ecto.UUID 20 18 21 - many_to_many :store_paths, Nix.StorePath, join_through: Sower.SeedStorePath 19 + field :name, :string 20 + field :seed_type, :string 21 + field :store_path, :string 22 22 23 23 timestamps() 24 24 end 25 25 26 26 def create(attrs) do 27 - %Sower.Seed{ 27 + %Seed{ 28 28 org_id: Sower.Repo.get_org_id() 29 29 } 30 30 |> changeset(attrs) 31 - |> Repo.insert() 32 - end 33 - 34 - def submit(%Seed{} = seed, path) do 35 - store_path = Nix.submit_store_path!(path) 36 - 37 - SeedStorePath.submit!(seed, store_path) 38 - 39 - {:ok, _} = updated_at_now(seed) 40 - 41 - {:ok, store_path} 42 - end 43 - 44 - def submit(seed_sid, path) do 45 - seed = get_sid!(seed_sid) 46 - submit(seed, path) 31 + |> Repo.insert( 32 + on_conflict: {:replace, [:updated_at]}, 33 + conflict_target: [:name, :seed_type, :store_path, :org_id], 34 + returning: true 35 + ) 47 36 end 48 37 49 38 def update(seed, attrs) do ··· 53 42 end 54 43 55 44 def get_by_id!(id) do 56 - Repo.get!(Sower.Seed, id) 45 + Repo.get!(Seed, id) 57 46 end 58 47 59 48 def get_by_id(id) do 60 - Repo.get(Sower.Seed, id) 49 + Repo.get(Seed, id) 61 50 end 62 51 63 52 def get!(name, seed_type) do 64 - Repo.get_by!(Sower.Seed, name: name, seed_type: seed_type) 53 + Repo.get_by!(Seed, name: name, seed_type: seed_type) 65 54 end 66 55 67 56 def get(name, seed_type) do 68 - Repo.get_by(Sower.Seed, name: name, seed_type: seed_type) 57 + Repo.get_by(Seed, name: name, seed_type: seed_type) 69 58 end 70 59 71 60 def get_sid!(sid) do 72 - Repo.get_by!(Sower.Seed, sid: sid) 61 + Repo.get_by!(Seed, sid: sid) 73 62 end 74 63 75 64 def list() do 76 - Repo.all(Sower.Seed) 65 + Repo.all(Seed) 77 66 end 78 67 79 68 def latest(name, seed_type) do 80 69 Repo.one( 81 - from s in Sower.Seed, 70 + from s in Seed, 82 71 where: s.name == ^name and s.seed_type == ^seed_type, 83 72 order_by: [desc: s.updated_at] 84 73 ) ··· 86 75 87 76 def latest_store_path(%__MODULE__{id: id}) do 88 77 Repo.one( 89 - from s in Sower.SeedStorePath, 78 + from s in Seed, 90 79 where: s.id == ^id, 91 80 order_by: [desc: s.updated_at] 92 81 ) 93 - |> Repo.preload(:store_path) 94 82 end 95 83 96 84 def latest_store_path_by_sid(sid) do 97 - seed = Sower.Seed.get_sid!(sid) 98 - 99 - query = 100 - from sp in Sower.SeedStorePath, 101 - where: sp.seed_id == ^seed.id, 102 - order_by: [desc: sp.updated_at], 103 - limit: 1 104 - 105 - case Repo.one(query) do 106 - nil -> 107 - nil 108 - 109 - store_path -> 110 - store_path |> Repo.preload(:store_path) |> Map.get(:store_path) 111 - end 85 + Repo.one( 86 + from s in Seed, 87 + where: s.sid == ^sid, 88 + order_by: [desc: s.updated_at] 89 + ) 112 90 end 113 91 114 92 defp changeset(seed, attrs) do 115 93 seed 116 - |> cast(attrs, [:name, :seed_type, :org_id]) 94 + |> cast(attrs, [:name, :seed_type, :org_id, :store_path]) 117 95 |> validate_inclusion(:seed_type, @seed_types) 118 - |> validate_required([:name, :seed_type, :org_id]) 119 - |> unique_constraint([:name, :seed_type, :org_id], error_key: :unique_seed) 96 + |> validate_required([:name, :seed_type, :org_id, :store_path]) 97 + |> unique_constraint([:name, :seed_type, :org_id, :store_path], error_key: :unique_seed) 120 98 end 121 99 122 100 defp updated_at_now(seed) do
-41
apps/sower/lib/sower/seed_store_path.ex
··· 1 - defmodule Sower.SeedStorePath do 2 - use Sower.Schema 3 - 4 - import Ecto.Changeset 5 - import Ecto.Query 6 - 7 - alias Sower.Repo 8 - 9 - schema "seeds_store_paths" do 10 - field :org_id, Ecto.UUID 11 - belongs_to :seed, Sower.Seed 12 - belongs_to :store_path, Sower.Nix.StorePath 13 - timestamps() 14 - end 15 - 16 - def find!(seed_id, store_path_id) do 17 - query = 18 - from ssp in Sower.SeedStorePath, 19 - where: ssp.seed_id == ^seed_id, 20 - where: ssp.store_path_id == ^store_path_id 21 - 22 - Repo.one!(query) 23 - end 24 - 25 - def submit!(seed, store_path) do 26 - %Sower.SeedStorePath{ 27 - org_id: Sower.Repo.get_org_id() 28 - } 29 - |> changeset(%{seed_id: seed.id, store_path_id: store_path.id}) 30 - |> Repo.insert!( 31 - on_conflict: {:replace, [:updated_at]}, 32 - conflict_target: [:seed_id, :store_path_id, :org_id] 33 - ) 34 - end 35 - 36 - defp changeset(seed_store_path, attrs) do 37 - seed_store_path 38 - |> cast(attrs, [:seed_id, :store_path_id]) 39 - |> validate_required([:seed_id, :store_path_id]) 40 - end 41 - end
+14 -14
apps/sower/lib/sower_web/agent_channel.ex
··· 84 84 end 85 85 end 86 86 87 - def handle_in("agent:current_generation", payload, socket) do 88 - payload = Nix.Profile.Generation.cast!(payload) 89 - 90 - store_path = Sower.Nix.submit_store_path!(payload.path) 91 - 92 - Sower.Orchestration.create_deployment(%{ 93 - deployed_at: payload.created, 94 - store_paths: [store_path] 95 - }) 96 - 97 - Phoenix.PubSub.broadcast(Sower.PubSub, "agent:view:#{socket.assigns.agent.sid}", payload) 98 - 99 - {:noreply, socket} 100 - end 87 + # def handle_in("agent:current_generation", payload, socket) do 88 + # payload = Nix.Profile.Generation.cast!(payload) 89 + # 90 + # store_path = Sower.Nix.submit_store_path!(payload.path) 91 + # 92 + # Sower.Orchestration.create_deployment(%{ 93 + # deployed_at: payload.created, 94 + # store_paths: [store_path] 95 + # }) 96 + # 97 + # Phoenix.PubSub.broadcast(Sower.PubSub, "agent:view:#{socket.assigns.agent.sid}", payload) 98 + # 99 + # {:noreply, socket} 100 + # end 101 101 102 102 def handle_in("seed:get", payload, socket) do 103 103 with {:ok, req_seed} <- SowerClient.Schemas.Seed.cast(payload),
+17 -56
apps/sower/lib/sower_web/controllers/api/seed_controller.ex
··· 32 32 %Plug.Conn{ 33 33 body_params: %Schemas.Seed{ 34 34 name: name, 35 - seed_type: seed_type 35 + seed_type: seed_type, 36 + store_path: store_path 36 37 } 37 38 } = conn, 38 39 _params ··· 41 42 42 43 if can(conn.assigns.access_token) 43 44 |> create?(%Sower.Seed{org_id: conn.assigns.access_token.org_id}) do 44 - case Sower.Seed.create(%{name: name, seed_type: seed_type}) do 45 + case Sower.Seed.create(%{name: name, seed_type: seed_type, store_path: store_path}) do 45 46 {:ok, %Sower.Seed{} = seed} -> 46 47 conn 47 48 |> put_status(:created) ··· 56 57 end 57 58 end 58 59 59 - operation(:new_store_path, 60 - operation_id: "NewSeedStorePath", 61 - summary: "New Seed Store Path", 60 + operation(:latest, 61 + operation_id: "LatestSeed", 62 + summary: "Find latest Seed", 62 63 parameters: [ 63 - sid: [ 64 - in: :path, 65 - description: "Seed SID", 64 + name: [ 65 + description: "Seed name", 66 66 type: :string, 67 - example: "example4ser3adju75ddusbr" 68 - ] 69 - ], 70 - request_body: {"Seed params", "application/json", Schemas.StorePath}, 71 - responses: [ 72 - created: {"Seed response", "application/json", Schemas.StorePath}, 73 - unauthorized: 74 - {"Unauthorized", "application/json", 75 - %Schema{type: :object, properties: %{error: %Schema{type: :string}}}} 76 - ] 77 - ) 78 - 79 - def new_store_path( 80 - %Plug.Conn{ 81 - body_params: %Schemas.StorePath{ 82 - path: path 83 - } 84 - } = conn, 85 - %{sid: sid} 86 - ) do 87 - conn = Map.put(conn, :body_params, %{}) 88 - 89 - if conn.assigns.access_token 90 - |> can() 91 - |> update?(%Sower.Seed{org_id: conn.assigns.access_token.org_id}) do 92 - case Sower.Seed.submit(sid, path) do 93 - {:ok, %Sower.Nix.StorePath{} = store_path} -> 94 - conn 95 - |> put_status(:created) 96 - |> render(:show, store_path: store_path) 97 - end 98 - else 99 - conn |> put_status(401) |> render(:error, error: "unauthorized") 100 - end 101 - end 102 - 103 - operation(:latest, 104 - operation_id: "LatestStorePathBySeed", 105 - summary: "Get latest Store Path for a Seed", 106 - parameters: [ 107 - sid: [ 108 - in: :path, 109 - description: "Seed SID", 67 + example: "host1" 68 + ], 69 + seed_type: [ 70 + description: "Seed type", 110 71 type: :string, 111 - example: "example4ser3adju75ddusbr" 72 + example: "nixos" 112 73 ] 113 74 ], 114 75 responses: %{ 115 - ok: {"Seed response", "application/json", Schemas.StorePath}, 76 + ok: {"Seed response", "application/json", Schemas.Seed}, 116 77 not_found: 117 - {"Store Path error response", "application/json", 78 + {"Seed error response", "application/json", 118 79 %Schema{type: :object, properties: %{error: %Schema{type: :string}}}}, 119 80 unauthorized: 120 81 {"Unauthorized", "application/json", ··· 122 83 } 123 84 ) 124 85 125 - def latest(conn, %{sid: sid}) do 86 + def latest(conn, %{name: name, seed_type: seed_type}) do 126 87 if can(conn.assigns.access_token) 127 88 |> read?(%Sower.Seed{org_id: conn.assigns.access_token.org_id}) do 128 - case Sower.Seed.latest_store_path_by_sid(sid) do 89 + case Sower.Seed.latest(name, seed_type) do 129 90 nil -> 130 91 conn |> put_status(404) |> render(:not_found) 131 92
-21
apps/sower/lib/sower_web/live/nix/store_path_live/index.ex
··· 1 - defmodule SowerWeb.Nix.StorePathLive.Index do 2 - use SowerWeb, :live_view 3 - 4 - alias Sower.Nix 5 - 6 - @impl true 7 - def mount(_params, _session, socket) do 8 - {:ok, stream(socket, :store_paths, Nix.list_store_paths())} 9 - end 10 - 11 - @impl true 12 - def handle_params(params, _url, socket) do 13 - {:noreply, apply_action(socket, socket.assigns.live_action, params)} 14 - end 15 - 16 - defp apply_action(socket, :index, _params) do 17 - socket 18 - |> assign(:page_title, "Listing Store paths") 19 - |> assign(:store_path, nil) 20 - end 21 - end
-17
apps/sower/lib/sower_web/live/nix/store_path_live/index.html.heex
··· 1 - <.header> 2 - Store paths 3 - </.header> 4 - 5 - <.table 6 - id="store_paths" 7 - rows={@streams.store_paths} 8 - row_click={fn {_id, store_path} -> JS.navigate(~p"/nix/store_paths/#{store_path}") end} 9 - > 10 - <:col :let={{_id, store_path}} label="path">{store_path.path}</:col> 11 - <:col :let={{_id, store_path}} label="updated">{store_path.updated_at}</:col> 12 - <:action :let={{_id, store_path}}> 13 - <div class="sr-only"> 14 - <.link navigate={~p"/nix/store_paths/#{store_path}"}>Show</.link> 15 - </div> 16 - </:action> 17 - </.table>
-20
apps/sower/lib/sower_web/live/nix/store_path_live/show.ex
··· 1 - defmodule SowerWeb.Nix.StorePathLive.Show do 2 - use SowerWeb, :live_view 3 - 4 - alias Sower.Nix 5 - 6 - @impl true 7 - def mount(_params, _session, socket) do 8 - {:ok, socket} 9 - end 10 - 11 - @impl true 12 - def handle_params(%{"digest" => digest}, _, socket) do 13 - {:noreply, 14 - socket 15 - |> assign(:page_title, page_title(socket.assigns.live_action)) 16 - |> assign(:store_path, Nix.get_store_path_digest!(digest) |> Sower.Repo.preload(:seeds))} 17 - end 18 - 19 - defp page_title(:show), do: "Show Store path" 20 - end
-20
apps/sower/lib/sower_web/live/nix/store_path_live/show.html.heex
··· 1 - <.header> 2 - {@store_path.path} 3 - </.header> 4 - 5 - <.list> 6 - <:item title="inserted">{@store_path.inserted_at}</:item> 7 - <:item title="updated">{@store_path.updated_at}</:item> 8 - 9 - <:item title="seeds"> 10 - <.table 11 - id="seeds" 12 - rows={@store_path.seeds} 13 - row_click={fn seed -> JS.navigate(~p"/seeds/#{seed}") end} 14 - > 15 - <:col :let={seed}>{seed.name}</:col> 16 - </.table> 17 - </:item> 18 - </.list> 19 - 20 - <.back navigate={~p"/nix/store_paths"}>Back to store_paths</.back>
+1 -4
apps/sower/lib/sower_web/router.ex
··· 64 64 live "/nix/caches/:sid/edit", Nix.CacheLive.Index, :edit 65 65 live "/nix/caches/:sid", Nix.CacheLive.Show, :show 66 66 live "/nix/caches/:sid/show/edit", Nix.CacheLive.Show, :edit 67 - live "/nix/store_paths", Nix.StorePathLive.Index, :index 68 - live "/nix/store_paths/:digest", Nix.StorePathLive.Show, :show 69 67 70 68 live "/settings", Settings.IndexLive, :index 71 69 live "/settings/access-tokens", Settings.AccessTokenLive.Index, :index ··· 107 105 get "/nix/caches", Nix.CacheController, :list 108 106 get "/seeds", SeedController, :list 109 107 get "/seeds/:sid", SeedController, :get 110 - get "/seeds/:sid/paths/latest", SeedController, :latest 108 + get "/seeds/latest", SeedController, :latest 111 109 post "/seeds", SeedController, :new 112 - post "/seeds/:sid/paths", SeedController, :new_store_path 113 110 end 114 111 115 112 scope "/auth" do
+5 -31
apps/sower/priv/repo/migrations/20240803030149_create_seeds.exs
··· 3 3 4 4 def change do 5 5 create table(:seeds) do 6 - add :name, :string 7 6 add :sid, :string, null: false 8 - add :seed_type, :string 9 7 add :org_id, references(:organizations, column: :org_id, type: :uuid), null: false 10 8 11 - timestamps() 12 - end 13 - 14 - create unique_index(:seeds, [:id, :org_id]) 15 - 16 - create table(:store_paths) do 17 - add :path, :string 18 - add :path_digest, :string 19 - add :org_id, references(:organizations, column: :org_id, type: :uuid), null: false 20 - 21 - timestamps() 22 - end 23 - 24 - create unique_index(:store_paths, [:id, :org_id]) 25 - create unique_index(:store_paths, [:path_digest, :org_id]) 26 - 27 - create table(:seeds_store_paths) do 28 - add :org_id, references(:organizations, column: :org_id, type: :uuid), null: false 29 - 30 - add :seed_id, 31 - references(:seeds, on_delete: :delete_all, with: [org_id: :org_id], match: :full), 32 - null: false 33 - 34 - add :store_path_id, 35 - references(:store_paths, on_delete: :delete_all, with: [org_id: :org_id], match: :full), 36 - null: false 9 + add :name, :string, null: false 10 + add :seed_type, :string, null: false 11 + add :store_path, :string, null: false 37 12 38 13 timestamps() 39 14 end 40 15 41 - create unique_index(:seeds, [:name, :seed_type, :org_id]) 16 + create unique_index(:seeds, [:id, :org_id]) 17 + create unique_index(:seeds, [:name, :seed_type, :store_path, :org_id]) 42 18 create unique_index(:seeds, [:sid]) 43 - create unique_index(:store_paths, [:path, :org_id]) 44 - create unique_index(:seeds_store_paths, [:seed_id, :store_path_id, :org_id]) 45 19 end 46 20 end
+5 -5
apps/sower/priv/repo/migrations/20250531133248_create_deployments.exs
··· 14 14 create index(:deployments, [:org_id]) 15 15 create unique_index(:deployments, :sid) 16 16 17 - create table(:store_paths_deployments) do 18 - add :store_path_id, references(:store_paths, on_delete: :nothing), null: false 17 + create table(:seeds_deployments) do 18 + add :seed_id, references(:seeds, on_delete: :nothing), null: false 19 19 add :deployment_id, references(:deployments, on_delete: :nothing), null: false 20 20 21 21 timestamps() 22 22 end 23 23 24 - create index(:store_paths_deployments, [:store_path_id]) 25 - create index(:store_paths_deployments, [:deployment_id]) 26 - create unique_index(:store_paths_deployments, [:store_path_id, :deployment_id]) 24 + create index(:seeds_deployments, [:seed_id]) 25 + create index(:seeds_deployments, [:deployment_id]) 26 + create unique_index(:seeds_deployments, [:seed_id, :deployment_id]) 27 27 28 28 create table(:subscriptions_deployments) do 29 29 add :subscription_id, references(:subscriptions, on_delete: :nothing), null: false
-7
apps/sower/priv/repo/migrations/20250705032155_add_seed_path_digest_index.exs
··· 1 - defmodule Sower.Repo.Migrations.AddSeedPathDigestIndex do 2 - use Ecto.Migration 3 - 4 - def change do 5 - create unique_index(:store_paths, [:path_digest]) 6 - end 7 - end
-54
apps/sower/test/sower/nix_test.exs
··· 66 66 assert %Ecto.Changeset{} = Nix.change_cache(cache) 67 67 end 68 68 end 69 - 70 - describe "store_paths" do 71 - alias Sower.Nix.StorePath 72 - 73 - import Sower.NixFixtures 74 - 75 - @invalid_attrs %{path: nil} 76 - 77 - test "list_store_paths/0 returns all store_paths" do 78 - store_path = store_path_fixture() 79 - assert Nix.list_store_paths() == [store_path] 80 - end 81 - 82 - test "get_store_path!/1 returns the store_path with given id" do 83 - store_path = store_path_fixture() 84 - assert Nix.get_store_path!(store_path.id) == store_path 85 - end 86 - 87 - test "create_store_path/1 with valid data creates a store_path" do 88 - valid_attrs = %{path: random_store_path()} 89 - 90 - assert {:ok, %StorePath{} = store_path} = Nix.create_store_path(valid_attrs) 91 - assert store_path.path == valid_attrs.path 92 - end 93 - 94 - test "create_store_path/1 with invalid data returns error changeset" do 95 - assert {:error, %Ecto.Changeset{}} = Nix.create_store_path(@invalid_attrs) 96 - end 97 - 98 - test "update_store_path/2 with valid data updates the store_path" do 99 - store_path = store_path_fixture() 100 - update_attrs = %{path: random_store_path()} 101 - 102 - assert {:ok, %StorePath{} = store_path} = Nix.update_store_path(store_path, update_attrs) 103 - assert store_path.path == update_attrs.path 104 - end 105 - 106 - test "update_store_path/2 with invalid data returns error changeset" do 107 - store_path = store_path_fixture() 108 - assert {:error, %Ecto.Changeset{}} = Nix.update_store_path(store_path, @invalid_attrs) 109 - assert store_path == Nix.get_store_path!(store_path.id) 110 - end 111 - 112 - test "delete_store_path/1 deletes the store_path" do 113 - store_path = store_path_fixture() 114 - assert {:ok, %StorePath{}} = Nix.delete_store_path(store_path) 115 - assert_raise Ecto.NoResultsError, fn -> Nix.get_store_path!(store_path.id) end 116 - end 117 - 118 - test "change_store_path/1 returns a store_path changeset" do 119 - store_path = store_path_fixture() 120 - assert %Ecto.Changeset{} = Nix.change_store_path(store_path) 121 - end 122 - end 123 69 end
+18 -3
apps/sower/test/sower/orchestration_test.exs
··· 2 2 use Sower.DataCase 3 3 4 4 alias Sower.Orchestration 5 + import Sower.AccountsFixtures 6 + 7 + setup _ do 8 + org = organization_fixture() 9 + Sower.Repo.put_org_id(org.org_id) 10 + 11 + %{organization: org} 12 + end 5 13 6 14 describe "agents" do 7 15 alias Sower.Orchestration.Agent ··· 79 87 test "create_subscription/1 with valid data creates a subscription" do 80 88 valid_attrs = %{sid: "some sid"} 81 89 82 - assert {:ok, %Subscription{} = subscription} = Orchestration.create_subscription(valid_attrs) 90 + assert {:ok, %Subscription{} = subscription} = 91 + Orchestration.create_subscription(valid_attrs) 92 + 83 93 assert subscription.sid == "some sid" 84 94 end 85 95 ··· 91 101 subscription = subscription_fixture() 92 102 update_attrs = %{sid: "some updated sid"} 93 103 94 - assert {:ok, %Subscription{} = subscription} = Orchestration.update_subscription(subscription, update_attrs) 104 + assert {:ok, %Subscription{} = subscription} = 105 + Orchestration.update_subscription(subscription, update_attrs) 106 + 95 107 assert subscription.sid == "some updated sid" 96 108 end 97 109 98 110 test "update_subscription/2 with invalid data returns error changeset" do 99 111 subscription = subscription_fixture() 100 - assert {:error, %Ecto.Changeset{}} = Orchestration.update_subscription(subscription, @invalid_attrs) 112 + 113 + assert {:error, %Ecto.Changeset{}} = 114 + Orchestration.update_subscription(subscription, @invalid_attrs) 115 + 101 116 assert subscription == Orchestration.get_subscription!(subscription.id) 102 117 end 103 118
+4 -22
apps/sower/test/sower/seed_test.exs
··· 25 25 end 26 26 end 27 27 28 - describe "submit/1" do 28 + describe "create/1" do 29 29 test "creates the seed if it does not exist" do 30 30 name = unique_seed_name() 31 31 ··· 35 35 assert %Seed{id: ^id} = Seed.latest(seed.name, "nixos") 36 36 end 37 37 38 - test "adds a store path if seed already exists" do 39 - seed = seed_fixture() 40 - 41 - {:ok, _} = 42 - Seed.submit(seed.sid, random_store_path()) 43 - 44 - assert Enum.count(seed |> Sower.Repo.preload(:store_paths) |> Map.get(:store_paths)) == 1 45 - 46 - {:ok, _} = 47 - Seed.submit(seed.sid, random_store_path()) 48 - 49 - assert Enum.count(seed |> Sower.Repo.preload(:store_paths) |> Map.get(:store_paths)) == 2 50 - end 51 - 52 - test "no new store paths if seed and path already exist" do 53 - store_path = store_path_fixture() 38 + test "upserts" do 54 39 seed = seed_fixture() 55 40 56 - {:ok, _} = Seed.submit(seed.sid, store_path.path) 57 - 58 - seed = seed |> Sower.Repo.preload(:store_paths) 41 + {:ok, _} = Seed.create(Map.from_struct(seed)) 59 42 60 - assert Enum.count(seed.store_paths) == 1 61 - assert Repo.all(Sower.Nix.StorePath) |> Enum.count() == 1 43 + assert Repo.all(Sower.Seed) |> Enum.count() == 1 62 44 end 63 45 end 64 46 end
-33
apps/sower/test/sower_web/live/nix/store_path_live_test.exs
··· 1 - defmodule SowerWeb.Nix.StorePathLiveTest do 2 - use SowerWeb.ConnCase 3 - 4 - import Phoenix.LiveViewTest 5 - import Sower.NixFixtures 6 - 7 - defp create_store_path(_) do 8 - store_path = store_path_fixture() 9 - %{store_path: store_path} 10 - end 11 - 12 - describe "Index" do 13 - setup [:register_and_log_in_user, :create_store_path] 14 - 15 - test "lists all store_paths", %{conn: conn, store_path: store_path} do 16 - {:ok, _index_live, html} = live(conn, ~p"/nix/store_paths") 17 - 18 - assert html =~ "Listing Store paths" 19 - assert html =~ store_path.path 20 - end 21 - end 22 - 23 - describe "Show" do 24 - setup [:register_and_log_in_user, :create_store_path] 25 - 26 - test "displays store_path", %{conn: conn, store_path: store_path} do 27 - {:ok, _show_live, html} = live(conn, ~p"/nix/store_paths/#{store_path}") 28 - 29 - assert html =~ "Show Store path" 30 - assert html =~ store_path.path 31 - end 32 - end 33 - end
+8 -3
apps/sower_client/lib/schemas/seed.ex
··· 10 10 properties: %{ 11 11 sid: %Schema{ 12 12 type: :string, 13 - description: "sid of the seed", 13 + description: "sid of the seed set by the server", 14 14 readOnly: true 15 15 }, 16 16 name: %Schema{ ··· 21 21 type: :string, 22 22 description: "Type of the seed", 23 23 enum: @seed_types 24 + }, 25 + store_path: %Schema{ 26 + type: :string, 27 + description: "Store path of the seed" 24 28 } 25 29 }, 26 - required: ~w(name seed_type)a, 30 + required: ~w(name seed_type store_path)a, 27 31 example: %{ 28 32 "id" => "example4ser3adju75ddusbr", 29 33 "name" => "myhost", 30 - "seed_type" => "nixos" 34 + "seed_type" => "nixos", 35 + "store_path" => "/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-nixos" 31 36 } 32 37 }) 33 38
-25
apps/sower_client/lib/schemas/store_path.ex
··· 1 - defmodule SowerClient.Schemas.StorePath do 2 - use SowerClient.Schema 3 - 4 - OpenApiSpex.schema(%{ 5 - title: "StorePath", 6 - description: "A store path is a Nix store path that can by installed by a client", 7 - type: :object, 8 - properties: %{ 9 - path: %Schema{ 10 - type: :string, 11 - description: "Nix store path" 12 - }, 13 - path_digest: %Schema{ 14 - type: :string, 15 - description: "id of the store path", 16 - readOnly: true 17 - } 18 - }, 19 - required: ~w(path)a, 20 - example: %{ 21 - "path_digest" => "examplehxpf8d7x5ys5p9v0z9x587hs1", 22 - "path" => "/nix/store/examplehxpf8d7x5ys5p9v0z9x587hs1-..." 23 - } 24 - }) 25 - end
+14 -6
apps/sower_dev/lib/sower_dev/application.ex
··· 7 7 8 8 @impl true 9 9 def start(_type, _args) do 10 - children = [ 11 - %{ 12 - id: :erl_boot_server, 13 - start: {:erl_boot_server, :start_link, [[]]} 14 - } 15 - ] 10 + children = [] ++ start_env() 16 11 17 12 # See https://hexdocs.pm/elixir/Supervisor.html 18 13 # for other strategies and supported options 19 14 opts = [strategy: :one_for_one, name: SowerDev.Supervisor] 20 15 Supervisor.start_link(children, opts) 16 + end 17 + 18 + if Mix.env() == :dev do 19 + defp start_env() do 20 + [ 21 + %{ 22 + id: :erl_boot_server, 23 + start: {:erl_boot_server, :start_link, [[]]} 24 + } 25 + ] 26 + end 27 + else 28 + defp start_env(), do: [] 21 29 end 22 30 end
+81 -208
client-go/client.gen.go
··· 48 48 // SeedType Type of the seed 49 49 SeedType SeedSeedType `json:"seed_type"` 50 50 51 - // Sid sid of the seed 51 + // Sid sid of the seed set by the server 52 52 Sid *string `json:"sid,omitempty"` 53 + 54 + // StorePath Store path of the seed 55 + StorePath string `json:"store_path"` 53 56 } 54 57 55 58 // SeedSeedType Type of the seed 56 59 type SeedSeedType string 57 60 58 - // StorePath A store path is a Nix store path that can by installed by a client 59 - type StorePath struct { 60 - // Path Nix store path 61 - Path string `json:"path"` 62 - 63 - // PathDigest id of the store path 64 - PathDigest *string `json:"path_digest,omitempty"` 65 - } 66 - 67 61 // ListSeedsParams defines parameters for ListSeeds. 68 62 type ListSeedsParams struct { 69 63 // Name Seed name ··· 73 67 SeedType *string `form:"seed_type,omitempty" json:"seed_type,omitempty"` 74 68 } 75 69 70 + // LatestSeedParams defines parameters for LatestSeed. 71 + type LatestSeedParams struct { 72 + // Name Seed name 73 + Name *string `form:"name,omitempty" json:"name,omitempty"` 74 + 75 + // SeedType Seed type 76 + SeedType *string `form:"seed_type,omitempty" json:"seed_type,omitempty"` 77 + } 78 + 76 79 // NewSeedJSONRequestBody defines body for NewSeed for application/json ContentType. 77 80 type NewSeedJSONRequestBody = Seed 78 - 79 - // NewSeedStorePathJSONRequestBody defines body for NewSeedStorePath for application/json ContentType. 80 - type NewSeedStorePathJSONRequestBody = StorePath 81 81 82 82 // RequestEditorFn is the function signature for the RequestEditor callback function 83 83 type RequestEditorFn func(ctx context.Context, req *http.Request) error ··· 163 163 164 164 NewSeed(ctx context.Context, body NewSeedJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) 165 165 166 + // LatestSeed request 167 + LatestSeed(ctx context.Context, params *LatestSeedParams, reqEditors ...RequestEditorFn) (*http.Response, error) 168 + 166 169 // GetSeed request 167 170 GetSeed(ctx context.Context, sid string, reqEditors ...RequestEditorFn) (*http.Response, error) 168 - 169 - // NewSeedStorePathWithBody request with any body 170 - NewSeedStorePathWithBody(ctx context.Context, sid string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) 171 - 172 - NewSeedStorePath(ctx context.Context, sid string, body NewSeedStorePathJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) 173 - 174 - // LatestStorePathBySeed request 175 - LatestStorePathBySeed(ctx context.Context, sid string, reqEditors ...RequestEditorFn) (*http.Response, error) 176 171 } 177 172 178 173 func (c *Client) ListNixCaches(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { ··· 223 218 return c.Client.Do(req) 224 219 } 225 220 226 - func (c *Client) GetSeed(ctx context.Context, sid string, reqEditors ...RequestEditorFn) (*http.Response, error) { 227 - req, err := NewGetSeedRequest(c.Server, sid) 228 - if err != nil { 229 - return nil, err 230 - } 231 - req = req.WithContext(ctx) 232 - if err := c.applyEditors(ctx, req, reqEditors); err != nil { 233 - return nil, err 234 - } 235 - return c.Client.Do(req) 236 - } 237 - 238 - func (c *Client) NewSeedStorePathWithBody(ctx context.Context, sid string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { 239 - req, err := NewNewSeedStorePathRequestWithBody(c.Server, sid, contentType, body) 240 - if err != nil { 241 - return nil, err 242 - } 243 - req = req.WithContext(ctx) 244 - if err := c.applyEditors(ctx, req, reqEditors); err != nil { 245 - return nil, err 246 - } 247 - return c.Client.Do(req) 248 - } 249 - 250 - func (c *Client) NewSeedStorePath(ctx context.Context, sid string, body NewSeedStorePathJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { 251 - req, err := NewNewSeedStorePathRequest(c.Server, sid, body) 221 + func (c *Client) LatestSeed(ctx context.Context, params *LatestSeedParams, reqEditors ...RequestEditorFn) (*http.Response, error) { 222 + req, err := NewLatestSeedRequest(c.Server, params) 252 223 if err != nil { 253 224 return nil, err 254 225 } ··· 259 230 return c.Client.Do(req) 260 231 } 261 232 262 - func (c *Client) LatestStorePathBySeed(ctx context.Context, sid string, reqEditors ...RequestEditorFn) (*http.Response, error) { 263 - req, err := NewLatestStorePathBySeedRequest(c.Server, sid) 233 + func (c *Client) GetSeed(ctx context.Context, sid string, reqEditors ...RequestEditorFn) (*http.Response, error) { 234 + req, err := NewGetSeedRequest(c.Server, sid) 264 235 if err != nil { 265 236 return nil, err 266 237 } ··· 403 374 return req, nil 404 375 } 405 376 406 - // NewGetSeedRequest generates requests for GetSeed 407 - func NewGetSeedRequest(server string, sid string) (*http.Request, error) { 377 + // NewLatestSeedRequest generates requests for LatestSeed 378 + func NewLatestSeedRequest(server string, params *LatestSeedParams) (*http.Request, error) { 408 379 var err error 409 380 410 - var pathParam0 string 411 - 412 - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "sid", runtime.ParamLocationPath, sid) 413 - if err != nil { 414 - return nil, err 415 - } 416 - 417 381 serverURL, err := url.Parse(server) 418 382 if err != nil { 419 383 return nil, err 420 384 } 421 385 422 - operationPath := fmt.Sprintf("/api/seeds/%s", pathParam0) 386 + operationPath := fmt.Sprintf("/api/seeds/latest") 423 387 if operationPath[0] == '/' { 424 388 operationPath = "." + operationPath 425 389 } ··· 429 393 return nil, err 430 394 } 431 395 432 - req, err := http.NewRequest("GET", queryURL.String(), nil) 433 - if err != nil { 434 - return nil, err 435 - } 396 + if params != nil { 397 + queryValues := queryURL.Query() 436 398 437 - return req, nil 438 - } 399 + if params.Name != nil { 439 400 440 - // NewNewSeedStorePathRequest calls the generic NewSeedStorePath builder with application/json body 441 - func NewNewSeedStorePathRequest(server string, sid string, body NewSeedStorePathJSONRequestBody) (*http.Request, error) { 442 - var bodyReader io.Reader 443 - buf, err := json.Marshal(body) 444 - if err != nil { 445 - return nil, err 446 - } 447 - bodyReader = bytes.NewReader(buf) 448 - return NewNewSeedStorePathRequestWithBody(server, sid, "application/json", bodyReader) 449 - } 401 + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "name", runtime.ParamLocationQuery, *params.Name); err != nil { 402 + return nil, err 403 + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { 404 + return nil, err 405 + } else { 406 + for k, v := range parsed { 407 + for _, v2 := range v { 408 + queryValues.Add(k, v2) 409 + } 410 + } 411 + } 450 412 451 - // NewNewSeedStorePathRequestWithBody generates requests for NewSeedStorePath with any type of body 452 - func NewNewSeedStorePathRequestWithBody(server string, sid string, contentType string, body io.Reader) (*http.Request, error) { 453 - var err error 413 + } 454 414 455 - var pathParam0 string 415 + if params.SeedType != nil { 456 416 457 - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "sid", runtime.ParamLocationPath, sid) 458 - if err != nil { 459 - return nil, err 460 - } 417 + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "seed_type", runtime.ParamLocationQuery, *params.SeedType); err != nil { 418 + return nil, err 419 + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { 420 + return nil, err 421 + } else { 422 + for k, v := range parsed { 423 + for _, v2 := range v { 424 + queryValues.Add(k, v2) 425 + } 426 + } 427 + } 461 428 462 - serverURL, err := url.Parse(server) 463 - if err != nil { 464 - return nil, err 465 - } 429 + } 466 430 467 - operationPath := fmt.Sprintf("/api/seeds/%s/paths", pathParam0) 468 - if operationPath[0] == '/' { 469 - operationPath = "." + operationPath 431 + queryURL.RawQuery = queryValues.Encode() 470 432 } 471 433 472 - queryURL, err := serverURL.Parse(operationPath) 434 + req, err := http.NewRequest("GET", queryURL.String(), nil) 473 435 if err != nil { 474 436 return nil, err 475 437 } 476 438 477 - req, err := http.NewRequest("POST", queryURL.String(), body) 478 - if err != nil { 479 - return nil, err 480 - } 481 - 482 - req.Header.Add("Content-Type", contentType) 483 - 484 439 return req, nil 485 440 } 486 441 487 - // NewLatestStorePathBySeedRequest generates requests for LatestStorePathBySeed 488 - func NewLatestStorePathBySeedRequest(server string, sid string) (*http.Request, error) { 442 + // NewGetSeedRequest generates requests for GetSeed 443 + func NewGetSeedRequest(server string, sid string) (*http.Request, error) { 489 444 var err error 490 445 491 446 var pathParam0 string ··· 500 455 return nil, err 501 456 } 502 457 503 - operationPath := fmt.Sprintf("/api/seeds/%s/paths/latest", pathParam0) 458 + operationPath := fmt.Sprintf("/api/seeds/%s", pathParam0) 504 459 if operationPath[0] == '/' { 505 460 operationPath = "." + operationPath 506 461 } ··· 572 527 573 528 NewSeedWithResponse(ctx context.Context, body NewSeedJSONRequestBody, reqEditors ...RequestEditorFn) (*NewSeedResponse, error) 574 529 530 + // LatestSeedWithResponse request 531 + LatestSeedWithResponse(ctx context.Context, params *LatestSeedParams, reqEditors ...RequestEditorFn) (*LatestSeedResponse, error) 532 + 575 533 // GetSeedWithResponse request 576 534 GetSeedWithResponse(ctx context.Context, sid string, reqEditors ...RequestEditorFn) (*GetSeedResponse, error) 577 - 578 - // NewSeedStorePathWithBodyWithResponse request with any body 579 - NewSeedStorePathWithBodyWithResponse(ctx context.Context, sid string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*NewSeedStorePathResponse, error) 580 - 581 - NewSeedStorePathWithResponse(ctx context.Context, sid string, body NewSeedStorePathJSONRequestBody, reqEditors ...RequestEditorFn) (*NewSeedStorePathResponse, error) 582 - 583 - // LatestStorePathBySeedWithResponse request 584 - LatestStorePathBySeedWithResponse(ctx context.Context, sid string, reqEditors ...RequestEditorFn) (*LatestStorePathBySeedResponse, error) 585 535 } 586 536 587 537 type ListNixCachesResponse struct { ··· 665 615 return 0 666 616 } 667 617 668 - type GetSeedResponse struct { 618 + type LatestSeedResponse struct { 669 619 Body []byte 670 620 HTTPResponse *http.Response 671 621 JSON200 *Seed ··· 678 628 } 679 629 680 630 // Status returns HTTPResponse.Status 681 - func (r GetSeedResponse) Status() string { 682 - if r.HTTPResponse != nil { 683 - return r.HTTPResponse.Status 684 - } 685 - return http.StatusText(0) 686 - } 687 - 688 - // StatusCode returns HTTPResponse.StatusCode 689 - func (r GetSeedResponse) StatusCode() int { 690 - if r.HTTPResponse != nil { 691 - return r.HTTPResponse.StatusCode 692 - } 693 - return 0 694 - } 695 - 696 - type NewSeedStorePathResponse struct { 697 - Body []byte 698 - HTTPResponse *http.Response 699 - JSON201 *StorePath 700 - JSON401 *struct { 701 - Error *string `json:"error,omitempty"` 702 - } 703 - } 704 - 705 - // Status returns HTTPResponse.Status 706 - func (r NewSeedStorePathResponse) Status() string { 631 + func (r LatestSeedResponse) Status() string { 707 632 if r.HTTPResponse != nil { 708 633 return r.HTTPResponse.Status 709 634 } ··· 711 636 } 712 637 713 638 // StatusCode returns HTTPResponse.StatusCode 714 - func (r NewSeedStorePathResponse) StatusCode() int { 639 + func (r LatestSeedResponse) StatusCode() int { 715 640 if r.HTTPResponse != nil { 716 641 return r.HTTPResponse.StatusCode 717 642 } 718 643 return 0 719 644 } 720 645 721 - type LatestStorePathBySeedResponse struct { 646 + type GetSeedResponse struct { 722 647 Body []byte 723 648 HTTPResponse *http.Response 724 - JSON200 *StorePath 649 + JSON200 *Seed 725 650 JSON401 *struct { 726 651 Error *string `json:"error,omitempty"` 727 652 } ··· 731 656 } 732 657 733 658 // Status returns HTTPResponse.Status 734 - func (r LatestStorePathBySeedResponse) Status() string { 659 + func (r GetSeedResponse) Status() string { 735 660 if r.HTTPResponse != nil { 736 661 return r.HTTPResponse.Status 737 662 } ··· 739 664 } 740 665 741 666 // StatusCode returns HTTPResponse.StatusCode 742 - func (r LatestStorePathBySeedResponse) StatusCode() int { 667 + func (r GetSeedResponse) StatusCode() int { 743 668 if r.HTTPResponse != nil { 744 669 return r.HTTPResponse.StatusCode 745 670 } ··· 781 706 return ParseNewSeedResponse(rsp) 782 707 } 783 708 784 - // GetSeedWithResponse request returning *GetSeedResponse 785 - func (c *ClientWithResponses) GetSeedWithResponse(ctx context.Context, sid string, reqEditors ...RequestEditorFn) (*GetSeedResponse, error) { 786 - rsp, err := c.GetSeed(ctx, sid, reqEditors...) 787 - if err != nil { 788 - return nil, err 789 - } 790 - return ParseGetSeedResponse(rsp) 791 - } 792 - 793 - // NewSeedStorePathWithBodyWithResponse request with arbitrary body returning *NewSeedStorePathResponse 794 - func (c *ClientWithResponses) NewSeedStorePathWithBodyWithResponse(ctx context.Context, sid string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*NewSeedStorePathResponse, error) { 795 - rsp, err := c.NewSeedStorePathWithBody(ctx, sid, contentType, body, reqEditors...) 709 + // LatestSeedWithResponse request returning *LatestSeedResponse 710 + func (c *ClientWithResponses) LatestSeedWithResponse(ctx context.Context, params *LatestSeedParams, reqEditors ...RequestEditorFn) (*LatestSeedResponse, error) { 711 + rsp, err := c.LatestSeed(ctx, params, reqEditors...) 796 712 if err != nil { 797 713 return nil, err 798 714 } 799 - return ParseNewSeedStorePathResponse(rsp) 715 + return ParseLatestSeedResponse(rsp) 800 716 } 801 717 802 - func (c *ClientWithResponses) NewSeedStorePathWithResponse(ctx context.Context, sid string, body NewSeedStorePathJSONRequestBody, reqEditors ...RequestEditorFn) (*NewSeedStorePathResponse, error) { 803 - rsp, err := c.NewSeedStorePath(ctx, sid, body, reqEditors...) 718 + // GetSeedWithResponse request returning *GetSeedResponse 719 + func (c *ClientWithResponses) GetSeedWithResponse(ctx context.Context, sid string, reqEditors ...RequestEditorFn) (*GetSeedResponse, error) { 720 + rsp, err := c.GetSeed(ctx, sid, reqEditors...) 804 721 if err != nil { 805 722 return nil, err 806 723 } 807 - return ParseNewSeedStorePathResponse(rsp) 808 - } 809 - 810 - // LatestStorePathBySeedWithResponse request returning *LatestStorePathBySeedResponse 811 - func (c *ClientWithResponses) LatestStorePathBySeedWithResponse(ctx context.Context, sid string, reqEditors ...RequestEditorFn) (*LatestStorePathBySeedResponse, error) { 812 - rsp, err := c.LatestStorePathBySeed(ctx, sid, reqEditors...) 813 - if err != nil { 814 - return nil, err 815 - } 816 - return ParseLatestStorePathBySeedResponse(rsp) 724 + return ParseGetSeedResponse(rsp) 817 725 } 818 726 819 727 // ParseListNixCachesResponse parses an HTTP response from a ListNixCachesWithResponse call ··· 939 847 return response, nil 940 848 } 941 849 942 - // ParseGetSeedResponse parses an HTTP response from a GetSeedWithResponse call 943 - func ParseGetSeedResponse(rsp *http.Response) (*GetSeedResponse, error) { 850 + // ParseLatestSeedResponse parses an HTTP response from a LatestSeedWithResponse call 851 + func ParseLatestSeedResponse(rsp *http.Response) (*LatestSeedResponse, error) { 944 852 bodyBytes, err := io.ReadAll(rsp.Body) 945 853 defer func() { _ = rsp.Body.Close() }() 946 854 if err != nil { 947 855 return nil, err 948 856 } 949 857 950 - response := &GetSeedResponse{ 858 + response := &LatestSeedResponse{ 951 859 Body: bodyBytes, 952 860 HTTPResponse: rsp, 953 861 } ··· 983 891 return response, nil 984 892 } 985 893 986 - // ParseNewSeedStorePathResponse parses an HTTP response from a NewSeedStorePathWithResponse call 987 - func ParseNewSeedStorePathResponse(rsp *http.Response) (*NewSeedStorePathResponse, error) { 894 + // ParseGetSeedResponse parses an HTTP response from a GetSeedWithResponse call 895 + func ParseGetSeedResponse(rsp *http.Response) (*GetSeedResponse, error) { 988 896 bodyBytes, err := io.ReadAll(rsp.Body) 989 897 defer func() { _ = rsp.Body.Close() }() 990 898 if err != nil { 991 899 return nil, err 992 900 } 993 901 994 - response := &NewSeedStorePathResponse{ 995 - Body: bodyBytes, 996 - HTTPResponse: rsp, 997 - } 998 - 999 - switch { 1000 - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: 1001 - var dest StorePath 1002 - if err := json.Unmarshal(bodyBytes, &dest); err != nil { 1003 - return nil, err 1004 - } 1005 - response.JSON201 = &dest 1006 - 1007 - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: 1008 - var dest struct { 1009 - Error *string `json:"error,omitempty"` 1010 - } 1011 - if err := json.Unmarshal(bodyBytes, &dest); err != nil { 1012 - return nil, err 1013 - } 1014 - response.JSON401 = &dest 1015 - 1016 - } 1017 - 1018 - return response, nil 1019 - } 1020 - 1021 - // ParseLatestStorePathBySeedResponse parses an HTTP response from a LatestStorePathBySeedWithResponse call 1022 - func ParseLatestStorePathBySeedResponse(rsp *http.Response) (*LatestStorePathBySeedResponse, error) { 1023 - bodyBytes, err := io.ReadAll(rsp.Body) 1024 - defer func() { _ = rsp.Body.Close() }() 1025 - if err != nil { 1026 - return nil, err 1027 - } 1028 - 1029 - response := &LatestStorePathBySeedResponse{ 902 + response := &GetSeedResponse{ 1030 903 Body: bodyBytes, 1031 904 HTTPResponse: rsp, 1032 905 } 1033 906 1034 907 switch { 1035 908 case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: 1036 - var dest StorePath 909 + var dest Seed 1037 910 if err := json.Unmarshal(bodyBytes, &dest); err != nil { 1038 911 return nil, err 1039 912 }
+12
config/runtime.exs
··· 15 15 if config_env() != :test do 16 16 Sower.Config.load() 17 17 end 18 + 19 + if config_env() == :test do 20 + SowerAgent.Config.load(%{ 21 + access_token_file: Path.expand("../.dev-api-token", __DIR__), 22 + endpoint: "http://localhost:7150", 23 + state_directory: Path.expand("../_build", __DIR__), 24 + subscriptions: [ 25 + %{name: "deck", seed_type: "nixos"}, 26 + %{name: "deck", seed_type: "home-manager"} 27 + ] 28 + }) 29 + end
+36 -109
openapi.json
··· 36 36 ], 37 37 "title": "Nix Cache", 38 38 "type": "object", 39 - "x-struct": "Elixir.SowerWeb.Schemas.Nix.Cache", 39 + "x-struct": "Elixir.SowerClient.Schemas.Nix.Cache", 40 40 "x-validate": null 41 41 }, 42 42 "Seed": { ··· 44 44 "example": { 45 45 "id": "example4ser3adju75ddusbr", 46 46 "name": "myhost", 47 - "seed_type": "nixos" 47 + "seed_type": "nixos", 48 + "store_path": "/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-nixos" 48 49 }, 49 50 "properties": { 50 51 "name": { ··· 66 67 "x-validate": null 67 68 }, 68 69 "sid": { 69 - "description": "sid of the seed", 70 + "description": "sid of the seed set by the server", 70 71 "readOnly": true, 71 72 "type": "string", 72 73 "x-struct": null, 73 74 "x-validate": null 74 - } 75 - }, 76 - "required": [ 77 - "name", 78 - "seed_type" 79 - ], 80 - "title": "Seed", 81 - "type": "object", 82 - "x-struct": "Elixir.SowerWeb.Schemas.Seed", 83 - "x-validate": null 84 - }, 85 - "StorePath": { 86 - "description": "A store path is a Nix store path that can by installed by a client", 87 - "example": { 88 - "path": "/nix/store/examplehxpf8d7x5ys5p9v0z9x587hs1-...", 89 - "path_digest": "examplehxpf8d7x5ys5p9v0z9x587hs1" 90 - }, 91 - "properties": { 92 - "path": { 93 - "description": "Nix store path", 94 - "type": "string", 95 - "x-struct": null, 96 - "x-validate": null 97 75 }, 98 - "path_digest": { 99 - "description": "id of the store path", 100 - "readOnly": true, 76 + "store_path": { 77 + "description": "Store path of the seed", 101 78 "type": "string", 102 79 "x-struct": null, 103 80 "x-validate": null 104 81 } 105 82 }, 106 83 "required": [ 107 - "path" 84 + "name", 85 + "seed_type", 86 + "store_path" 108 87 ], 109 - "title": "StorePath", 88 + "title": "Seed", 110 89 "type": "object", 111 - "x-struct": "Elixir.SowerWeb.Schemas.StorePath", 90 + "x-struct": "Elixir.SowerClient.Schemas.Seed", 112 91 "x-validate": null 113 92 } 114 93 }, ··· 329 308 "tags": [] 330 309 } 331 310 }, 332 - "/api/seeds/{sid}": { 311 + "/api/seeds/latest": { 333 312 "get": { 334 313 "callbacks": {}, 335 - "operationId": "GetSeed", 314 + "operationId": "LatestSeed", 336 315 "parameters": [ 337 316 { 338 - "description": "Seed SID", 339 - "example": "example4ser3adju75ddusbr", 340 - "in": "path", 341 - "name": "sid", 342 - "required": true, 317 + "description": "Seed name", 318 + "example": "host1", 319 + "in": "query", 320 + "name": "name", 321 + "required": false, 322 + "schema": { 323 + "type": "string", 324 + "x-struct": null, 325 + "x-validate": null 326 + } 327 + }, 328 + { 329 + "description": "Seed type", 330 + "example": "nixos", 331 + "in": "query", 332 + "name": "seed_type", 333 + "required": false, 343 334 "schema": { 344 335 "type": "string", 345 336 "x-struct": null, ··· 397 388 "description": "Seed error response" 398 389 } 399 390 }, 400 - "summary": "Get Seed", 391 + "summary": "Find latest Seed", 401 392 "tags": [] 402 393 } 403 394 }, 404 - "/api/seeds/{sid}/paths": { 405 - "post": { 406 - "callbacks": {}, 407 - "operationId": "NewSeedStorePath", 408 - "parameters": [ 409 - { 410 - "description": "Seed SID", 411 - "example": "example4ser3adju75ddusbr", 412 - "in": "path", 413 - "name": "sid", 414 - "required": true, 415 - "schema": { 416 - "type": "string", 417 - "x-struct": null, 418 - "x-validate": null 419 - } 420 - } 421 - ], 422 - "requestBody": { 423 - "content": { 424 - "application/json": { 425 - "schema": { 426 - "$ref": "#/components/schemas/StorePath" 427 - } 428 - } 429 - }, 430 - "description": "Seed params", 431 - "required": false 432 - }, 433 - "responses": { 434 - "201": { 435 - "content": { 436 - "application/json": { 437 - "schema": { 438 - "$ref": "#/components/schemas/StorePath" 439 - } 440 - } 441 - }, 442 - "description": "Seed response" 443 - }, 444 - "401": { 445 - "content": { 446 - "application/json": { 447 - "schema": { 448 - "properties": { 449 - "error": { 450 - "type": "string", 451 - "x-struct": null, 452 - "x-validate": null 453 - } 454 - }, 455 - "type": "object", 456 - "x-struct": null, 457 - "x-validate": null 458 - } 459 - } 460 - }, 461 - "description": "Unauthorized" 462 - } 463 - }, 464 - "summary": "New Seed Store Path", 465 - "tags": [] 466 - } 467 - }, 468 - "/api/seeds/{sid}/paths/latest": { 395 + "/api/seeds/{sid}": { 469 396 "get": { 470 397 "callbacks": {}, 471 - "operationId": "LatestStorePathBySeed", 398 + "operationId": "GetSeed", 472 399 "parameters": [ 473 400 { 474 401 "description": "Seed SID", ··· 488 415 "content": { 489 416 "application/json": { 490 417 "schema": { 491 - "$ref": "#/components/schemas/StorePath" 418 + "$ref": "#/components/schemas/Seed" 492 419 } 493 420 } 494 421 }, ··· 530 457 } 531 458 } 532 459 }, 533 - "description": "Store Path error response" 460 + "description": "Seed error response" 534 461 } 535 462 }, 536 - "summary": "Get latest Store Path for a Seed", 463 + "summary": "Get Seed", 537 464 "tags": [] 538 465 } 539 466 }