Deployment and lifecycle management for Nix
0
fork

Configure Feed

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

move config module to runtime and fix everything

+236 -236
+211 -16
config/runtime.exs
··· 1 1 import Config 2 2 3 - # config/runtime.exs is executed for all environments, including 4 - # during releases. It is executed after compilation and before the 5 - # system starts, so it is typically used to load production configuration 6 - # and secrets from environment variables or elsewhere. Do not define 7 - # any compile-time configuration in here, as it won't be applied. 8 - # The block below contains prod specific runtime configuration. 9 - 10 - # ## Using releases 11 - # 12 - # If you use `mix release`, you need to explicitly enable the server 13 - # by passing the PHX_SERVER=true when you start it: 14 - # 15 - # PHX_SERVER=true bin/sower start 16 - # 17 - # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` 18 - # script that automatically sets the env var above. 19 3 if System.get_env("PHX_SERVER") do 20 4 config :sower, SowerWeb.Endpoint, server: true 21 5 end 6 + 7 + defmodule Sower.Config do 8 + require Logger 9 + 10 + @credentials [ 11 + "SOWER_AUTH_OIDC_CLIENT_SECRET_FILE" 12 + ] 13 + 14 + @schema %{ 15 + "type" => "object", 16 + "required" => ["auth", "database"], 17 + "properties" => %{ 18 + "auth" => %{ 19 + "type" => "object", 20 + "required" => ["oidc_base_url", "oidc_client_id"], 21 + "properties" => %{ 22 + "oidc_base_url" => %{ 23 + "type" => "string" 24 + }, 25 + "oidc_client_id" => %{ 26 + "type" => "string" 27 + }, 28 + "oidc_redirect_uri" => %{ 29 + "type" => "string" 30 + } 31 + } 32 + }, 33 + "database" => %{ 34 + "type" => "object", 35 + "properties" => %{ 36 + "host" => %{ 37 + "type" => "string" 38 + }, 39 + "database" => %{ 40 + "type" => "string" 41 + }, 42 + "pass_file" => %{ 43 + "type" => "string" 44 + }, 45 + "port" => %{ 46 + "type" => "integer", 47 + "minimum" => 80, 48 + "maximum" => 65535 49 + }, 50 + "socket" => %{ 51 + "type" => "string" 52 + }, 53 + "user" => %{ 54 + "type" => "string" 55 + } 56 + } 57 + }, 58 + "public_url" => %{ 59 + "type" => "string", 60 + "format" => "uri" 61 + }, 62 + "listen_address" => %{ 63 + "type" => "string", 64 + "format" => "ipv4" 65 + }, 66 + "listen_port" => %{ 67 + "default" => 4000, 68 + "type" => "integer", 69 + "minimum" => 80, 70 + "maximum" => 65535 71 + } 72 + } 73 + } 74 + 75 + def load() do 76 + Logger.debug("Loading configuration") 77 + {:ok, _} = Application.ensure_all_started(:jason) 78 + 79 + config_file = System.get_env("SOWER_SERVER_CONFIG_FILE", "/etc/sower/server.json") 80 + 81 + json_config = 82 + with {:ok, contents} <- File.read(config_file), 83 + {:ok, json} <- Jason.decode(contents), 84 + :ok <- ExJsonSchema.Validator.validate(ExJsonSchema.Schema.resolve(@schema), json) do 85 + json 86 + else 87 + {:error, err} -> 88 + Logger.error(~s"Failed to read configuration file #{config_file}") 89 + Logger.error(err) 90 + Kernel.exit(1) 91 + end 92 + 93 + Logger.debug("Loaded configuration") 94 + Logger.debug(json_config) 95 + 96 + # load some defaults 97 + public_url = json_config |> Map.get("public_url", "http://127.0.0.1:4000") 98 + put_config(:auth, oidc_redirect_uri: ~s"#{public_url}/auth") 99 + listen_address = json_config |> Map.get("listen_address", "127.0.0.1") 100 + listen_port = json_config |> Map.get("listen_port", 4000) 101 + 102 + json_config |> Enum.map(&load_config(&1)) 103 + @credentials |> Enum.map(&load_credential(&1)) 104 + 105 + # load some non-app namespaced configs 106 + %URI{scheme: scheme, host: host, port: port} = URI.parse(public_url) 107 + 108 + put_config(SowerWeb.Endpoint, 109 + url: [host: host, port: port, scheme: scheme], 110 + http: [ip: ip_to_inet(listen_address), port: listen_port], 111 + secret_key_base: credential!("SOWER_SECRET_KEY_BASE_FILE"), 112 + persistent: true 113 + ) 114 + 115 + Logger.info("Finished loading configuration") 116 + end 117 + 118 + defp load_config({config_atom, values}) when is_map(values) do 119 + config_atom = String.to_atom(config_atom) 120 + values = Keyword.new(values, fn {k, v} -> {String.to_atom(k), v} end) 121 + put_config(config_atom, values) 122 + end 123 + 124 + defp load_config({config_atom, value}) when is_binary(value) do 125 + config_atom = String.to_atom(config_atom) 126 + put_config(config_atom, value) 127 + end 128 + 129 + defp credential(name) do 130 + credential_dir = System.get_env("CREDENTIALS_DIRECTORY") 131 + credential = System.get_env(name) 132 + 133 + case read_credential(name, credential_dir, credential) do 134 + {:ok, value} -> {:ok, value |> String.trim()} 135 + {:error, err} -> {:error, ~s"unable to load credential #{name}, #{err}"} 136 + end 137 + end 138 + 139 + defp load_credential(cred) when is_binary(cred) do 140 + Logger.debug(~s"Loading credential #{cred}") 141 + 142 + captures = 143 + ~r/SOWER_(?<section>[[:alnum:]]+)_(?<key>.+)_FILE/ 144 + |> Regex.named_captures(cred) 145 + 146 + if captures == nil do 147 + Logger.error(~s"Credential #{cred} cannot be parsed") 148 + Kernel.exit(1) 149 + end 150 + 151 + section = captures["section"] |> String.downcase() |> String.to_atom() 152 + 153 + key = captures["key"] |> String.downcase() |> String.to_atom() 154 + 155 + case credential(cred) do 156 + {:ok, path} -> 157 + put_config(section, [{key, path}]) 158 + :ok 159 + 160 + {:error, _err} -> 161 + :error 162 + end 163 + end 164 + 165 + def credential!(name) do 166 + case credential(name) do 167 + {:ok, value} -> value 168 + {:error, err} -> raise err 169 + end 170 + end 171 + 172 + defp read_credential(name, nil, nil) do 173 + Logger.warning(~s"Could not load credential from env: #{name}") 174 + {:error, "not found"} 175 + end 176 + 177 + defp read_credential(_, nil, cred), do: read_credential(cred) 178 + 179 + defp read_credential(name, dir, nil), do: read_credential(~s"#{dir}/#{name}") 180 + defp read_credential(_, dir, cred), do: read_credential(~s"#{dir}/#{cred}") 181 + defp read_credential(path) when is_binary(path), do: path |> File.read() 182 + 183 + defp read_credential(nil) do 184 + Logger.error("Could not find credential") 185 + Kernel.exit(1) 186 + end 187 + 188 + defp put_config(config_atom, new_values) when is_atom(config_atom) and is_list(new_values) do 189 + config = 190 + case Application.fetch_env(:sower, config_atom) do 191 + {:ok, previous_values} -> Keyword.merge(previous_values, new_values) 192 + :error -> new_values 193 + end 194 + 195 + config(:sower, config_atom, config) 196 + end 197 + 198 + defp put_config(config_atom, new_value) when is_atom(config_atom) do 199 + config(:sower, config_atom, new_value) 200 + end 201 + 202 + defp ip_to_inet(ip) do 203 + case ip 204 + |> to_charlist() 205 + |> :inet.parse_address() do 206 + {:ok, ip} -> 207 + ip 208 + 209 + {:error, _err} -> 210 + Logger.error(~s"Failed to parse ip #{ip}") 211 + Kernel.exit(1) 212 + end 213 + end 214 + end 215 + 216 + Sower.Config.load()
+1 -1
dev-server.json
··· 1 1 { 2 2 "public_url": "http://localhost:4000", 3 3 "auth": { 4 - "oidc_base_url": "https://sower.junco.dev", 4 + "oidc_base_url": "https://id.junco.dev/oauth2/openid/sower-dev", 5 5 "oidc_client_id": "sower-dev" 6 6 }, 7 7 "database": {
+8 -2
lib/sower/accounts/secrets.ex
··· 12 12 end 13 13 14 14 def secret_for([:authentication, :strategies, :oidc, :client_id], Sower.Accounts.User, _) do 15 - Application.fetch_env(:sower, :oidc_client_id) 15 + case Application.get_env(:sower, :auth) |> Keyword.get(:oidc_client_id) do 16 + nil -> :error 17 + result -> {:ok, result} 18 + end 16 19 end 17 20 18 21 def secret_for([:authentication, :strategies, :oidc, :client_secret], Sower.Accounts.User, _) do 19 - Application.fetch_env(:sower, :oidc_client_secret) 22 + case Application.get_env(:sower, :auth) |> Keyword.get(:oidc_client_secret) do 23 + nil -> :error 24 + result -> {:ok, result} 25 + end 20 26 end 21 27 end
+15 -2
lib/sower/accounts/user.ex
··· 42 42 strategies do 43 43 oidc :oidc do 44 44 client_id Sower.Accounts.Secrets 45 - base_url fn _, _ -> Application.fetch_env(:sower, :oidc_base_url) end 46 - redirect_uri fn _, _ -> Application.fetch_env(:sower, :oidc_redirect_uri) end 45 + 46 + base_url fn _, _ -> 47 + case Application.get_env(:sower, :auth) |> Keyword.get(:oidc_base_url) do 48 + nil -> :error 49 + val -> {:ok, val} 50 + end 51 + end 52 + 53 + redirect_uri fn _, _ -> 54 + case Application.get_env(:sower, :auth) |> Keyword.get(:oidc_redirect_uri) do 55 + nil -> :error 56 + val -> {:ok, val} 57 + end 58 + end 59 + 47 60 client_secret Sower.Accounts.Secrets 48 61 # TODO: figure out why decoding fails with ES256 49 62 # id_token_signed_response_alg "ES256"
-2
lib/sower/application.ex
··· 19 19 config: %{metadata: [:file, :line]} 20 20 }) 21 21 22 - Sower.Config.load() 23 - 24 22 # See https://hexdocs.pm/elixir/Supervisor.html 25 23 # for other strategies and supported options 26 24 opts = [strategy: :one_for_one, name: Sower.Supervisor]
-212
lib/sower/config.ex
··· 1 - defmodule Sower.Config do 2 - require Logger 3 - 4 - @credentials [ 5 - "SOWER_AUTH_OIDC_CLIENT_SECRET_FILE" 6 - ] 7 - 8 - @schema %{ 9 - "type" => "object", 10 - "required" => ["auth", "database"], 11 - "properties" => %{ 12 - "auth" => %{ 13 - "type" => "object", 14 - "required" => ["oidc_base_url", "oidc_client_id"], 15 - "properties" => %{ 16 - "oidc_base_url" => %{ 17 - "type" => "string" 18 - }, 19 - "oidc_client_id" => %{ 20 - "type" => "string" 21 - }, 22 - "oidc_redirect_url" => %{ 23 - "type" => "string" 24 - } 25 - } 26 - }, 27 - "database" => %{ 28 - "type" => "object", 29 - "properties" => %{ 30 - "host" => %{ 31 - "type" => "string" 32 - }, 33 - "database" => %{ 34 - "type" => "string" 35 - }, 36 - "pass_file" => %{ 37 - "type" => "string" 38 - }, 39 - "port" => %{ 40 - "type" => "integer", 41 - "minimum" => 80, 42 - "maximum" => 65535 43 - }, 44 - "socket" => %{ 45 - "type" => "string" 46 - }, 47 - "user" => %{ 48 - "type" => "string" 49 - } 50 - } 51 - }, 52 - "public_url" => %{ 53 - "type" => "string", 54 - "format" => "uri" 55 - }, 56 - "listen_address" => %{ 57 - "type" => "string", 58 - "format" => "ipv4" 59 - }, 60 - "listen_port" => %{ 61 - "default" => 4000, 62 - "type" => "integer", 63 - "minimum" => 80, 64 - "maximum" => 65535 65 - } 66 - } 67 - } 68 - 69 - def load() do 70 - Logger.debug("Loading configuration") 71 - {:ok, _} = Application.ensure_all_started(:jason) 72 - 73 - config_file = System.get_env("SOWER_SERVER_CONFIG_FILE", "/etc/sower/server.json") 74 - 75 - json_config = 76 - with {:ok, contents} <- File.read(config_file), 77 - {:ok, json} <- Jason.decode(contents), 78 - :ok <- ExJsonSchema.Validator.validate(ExJsonSchema.Schema.resolve(@schema), json) do 79 - json 80 - else 81 - {:error, err} -> 82 - Logger.error(~s"Failed to read configuration file #{config_file}") 83 - Logger.error(err) 84 - Kernel.exit(1) 85 - end 86 - 87 - Logger.debug("Loaded configuration:") 88 - Logger.debug(json_config) 89 - 90 - # load some defaults 91 - public_url = json_config |> Map.get("public_url", "http://127.0.0.1:4000") 92 - put_env(:auth, oidc_redirect_url: ~s"#{public_url}/auth") 93 - put_env(:listen_address, "127.0.0.1") 94 - put_env(:listen_port, 4000) 95 - 96 - json_config |> Enum.map(&load_config(&1)) 97 - @credentials |> Enum.map(&load_credential(&1)) 98 - 99 - # load some non-app namespaced configs 100 - %URI{scheme: scheme, host: host, port: port} = URI.parse(public_url) 101 - 102 - put_env(SowerWeb.Endpoint, 103 - url: [host: host, port: port, scheme: scheme], 104 - http: [ip: ip_to_inet(fetch_env!(:listen_address)), port: fetch_env!(:listen_port)], 105 - secret_key_base: credential!("SOWER_SECRET_KEY_BASE_FILE"), 106 - persistent: true 107 - ) 108 - 109 - Logger.debug("Finished loading configuration") 110 - end 111 - 112 - defp load_config({config_atom, values}) when is_map(values) do 113 - config_atom = String.to_atom(config_atom) 114 - values = Keyword.new(values, fn {k, v} -> {String.to_atom(k), v} end) 115 - put_env(config_atom, values) 116 - end 117 - 118 - defp load_config({config_atom, value}) when is_binary(value) do 119 - config_atom = String.to_atom(config_atom) 120 - put_env(config_atom, value) 121 - end 122 - 123 - defp credential(name) do 124 - credential_dir = System.get_env("CREDENTIALS_DIRECTORY") 125 - credential = System.get_env(name) 126 - 127 - case read_credential(name, credential_dir, credential) do 128 - {:ok, value} -> {:ok, value |> String.trim()} 129 - {:error, err} -> {:error, ~s"unable to load credential #{name}, #{err}"} 130 - end 131 - end 132 - 133 - defp load_credential(cred) when is_binary(cred) do 134 - Logger.debug(~s"Loading credential #{cred}") 135 - 136 - captures = 137 - ~r/SOWER_(?<section>[[:alnum:]]+)_(?<key>.+)_FILE/ 138 - |> Regex.named_captures(cred) 139 - 140 - if captures == nil do 141 - Logger.error(~s"Credential #{cred} cannot be parsed") 142 - Kernel.exit(1) 143 - end 144 - 145 - section = captures["section"] |> String.downcase() |> String.to_atom() 146 - 147 - key = captures["key"] |> String.downcase() |> String.to_atom() 148 - 149 - case credential(cred) do 150 - {:ok, path} -> 151 - put_env(section, [{key, path}]) 152 - :ok 153 - 154 - {:error, _err} -> 155 - :error 156 - end 157 - end 158 - 159 - def credential!(name) do 160 - case credential(name) do 161 - {:ok, value} -> value 162 - {:error, err} -> raise err 163 - end 164 - end 165 - 166 - defp read_credential(name, nil, nil) do 167 - Logger.warning(~s"Could not load credential from env: #{name}") 168 - {:error, "not found"} 169 - end 170 - 171 - defp read_credential(_, nil, cred), do: read_credential(cred) 172 - 173 - defp read_credential(name, dir, nil), do: read_credential(~s"#{dir}/#{name}") 174 - defp read_credential(_, dir, cred), do: read_credential(~s"#{dir}/#{cred}") 175 - defp read_credential(path) when is_binary(path), do: path |> File.read() 176 - 177 - defp read_credential(nil) do 178 - Logger.error("Could not find credential") 179 - Kernel.exit(1) 180 - end 181 - 182 - defp put_env(config_atom, new_values) when is_atom(config_atom) and is_list(new_values) do 183 - config = 184 - case Application.fetch_env(:sower, config_atom) do 185 - {:ok, previous_values} -> Keyword.merge(previous_values, new_values) 186 - :error -> new_values 187 - end 188 - 189 - Application.put_env(:sower, config_atom, config, persistent: true) 190 - end 191 - 192 - defp put_env(config_atom, new_value) when is_atom(config_atom) do 193 - Application.put_env(:sower, config_atom, new_value, persistent: true) 194 - end 195 - 196 - defp fetch_env!(config_atom) when is_atom(config_atom) do 197 - Application.fetch_env!(:sower, config_atom) 198 - end 199 - 200 - defp ip_to_inet(ip) do 201 - case ip 202 - |> to_charlist() 203 - |> :inet.parse_address() do 204 - {:ok, ip} -> 205 - ip 206 - 207 - {:error, _err} -> 208 - Logger.error(~s"Failed to parse ip #{ip}") 209 - Kernel.exit(1) 210 - end 211 - end 212 - end
+1 -1
lib/sower/repo.ex
··· 11 11 end 12 12 13 13 def init(_context, config) do 14 - {:ok, Keyword.merge(config, Application.get_env(:sower, :database))} 14 + {:ok, Keyword.merge(config, Application.get_env(:sower, :database, []))} 15 15 end 16 16 end