Skip to content
This repository has been archived by the owner on Jun 30, 2021. It is now read-only.

Add Settings system #493

Merged
merged 46 commits into from
Nov 14, 2018
Merged

Add Settings system #493

merged 46 commits into from
Nov 14, 2018

Conversation

T-Dnzt
Copy link

@T-Dnzt T-Dnzt commented Oct 8, 2018

Issue/Task Number: #436
closes #436

Overview

This PR adds a settings system to the eWallet. What started simple ended up being a very complex feature to many unexpected problems. In order to manage settings/configuration of the eWallet in a way that users can update it without updating the ENV and restarting, a new sub-application had to be created: ewallet_config.

Changes

  • Created ewallet_config
  • Added the EWalletConfig.Config that acts as an entry point to the configuration system.
  • Turn that config module into a GenServer in order to allow other applications to register themselves as settings users. See implementation details for flow.
  • Added tests in ewallet_config
  • Added test helpers to allow the Config GenServer to be used in the tests of other sub applications

Implementation Details

Flow

Due to some of our libraries requiring their own Application env (arc, etc.), we needed a way to keep using the famous Application.get_env(). In order to do that, the implemented system works in the following way:

  1. Seeds have to be run first, to insert the current set of 19 settings used in the eWallet
  2. ewallet_config boots up and starts a GenServer allowing other apps to register themselves
  3. Other apps register themselves, specifying which settings they want to use
  4. The Application envs of those apps are set using the values from the DB
  5. When a setting is updated, the GenServer loops through the registered apps and refresh all their configuration. The current implementation doesn't optimize what gets updated, all apps get all their configuration refreshed.
  6. To prevent frequent reloads, it is possible to update multiple settings at once.

Settings

Settings are a module interface to the StoredSetting Ecto schema. This interface was needed to allow any data to be stored inside a data field (or an encrypted one encrypted_data) which is a JSON field.

From outside, it only looks like you're passing value (a property of the Setting struct), but in the background, it gets translated into the format supported in StoredSetting.

A stored setting has the following fields:

  • uuid
  • id
  • key: the human-readable identifier of the setting
  • data: the unencrypted data of the setting (stored as {value: "something"})
  • encrypted_data: the encrypted data of the setting (stored as {value: "something"})
  • type: the type of the data stored in data/encrypted_data. The following types are available: "string", "integer", "map", "boolean", "array"
  • description: a description of the setting
  • options: a list of potential values for the setting
  • parent: the key of the parent setting
  • parent_value: the value of the parent setting for this one to be enabled
  • secret: if the setting data should be encrypted or not
  • position: the position of the setting to maintain order and prevent chaos

Here's an example of a setting:

%{
      key: "email_adapter",
      value: "smtp",
      type: "select",
      options: ["smtp", "local", "test"],
      description:
        "When set to local, a local email adapter will be used. Perfect for testing and development."
}

Usage (example with the ewallet_db sub app)

  1. Add the list of settings in config.exs for the sub app
config :ewallet_db,
  ecto_repos: [EWalletDB.Repo],
  env: Mix.env(),
  schemas_to_audit_types: audits,
  audit_types_to_schemas: Enum.into(audits, %{}, fn {key, value} -> {value, key} end),
  settings: [
    :base_url,
    :min_password_length,
    :file_storage_adapter,
    :aws_bucket,
    :aws_region,
    :aws_access_key_id,
    :aws_secret_access_key,
    :gcs_bucket,
    :gcs_credentials
  ]
  1. Update application.ex to register the app
settings = Application.get_env(:ewallet, :settings)
Config.register_and_load(:ewallet, settings)
  1. Done! Anytime, a setting is updated in the DB, the Application env will be reloaded.

Impact

We need to do a good amount of testing in staging to ensure the settings are properly refreshed in all nodes. Other than that, Application.get_env can still be used to retrieve config in other sub apps.

Seeds need to be run before this feature can be used. The setting seeding is ran as part of the usual mix seed command but can also be ran specifically with mix seed --settings. A prompt will ask the user for the base_url value to set - this will be skipped when specifying the auto-yes option -y (it will use the default value).

Changes coming to this PR

  • Removing the select type, instead the options field will define if the value has to match some specific values
  • Adding validators for the type and the select values
  • Changing the options to an array in the database

To Do in future PRs

  • Add the endpoints to update those settings
  • Add a test testing that config in all nodes get reloaded properly. I currently have the following test that doesn't work - it seems the nodes aren't connecting to the same database, and checking out the repo for each node doesn't seem to help.
defmodule EWalletConfig.MultiNodeTest do
  use ExUnit.Case, async: false
  alias EWalletConfig.{Config, Repo, ConfigTestHelper}
  alias Ecto.Adapters.SQL.Sandbox

  describe "reload_config/1" do
    test "reloads all settings for all nodes" do
      Sandbox.checkout(Repo)

      ConfigTestHelper.spawn([:test1, :test2, :test3])
      nodes = [Node.self() | Node.list()]

      # Not sure if this is needed, doesn't work anyway
      # Enum.each(nodes, fn node ->
      #  :rpc.block_call(node, Sandbox, :checkout, [Repo])
      # end)

      Sandbox.mode(Repo, {:shared, self()})

      Config.insert(%{key: "my_setting", value: "some_value", type: "string"})

      Enum.each(nodes, fn node ->
        :ok = Config.register_and_load(:my_app, [:my_setting], {Config, node})
        value = :rpc.block_call(node, Application, :get_env, [:my_app, :my_setting])
        assert value == "some_value"
      end)

      Config.update("my_setting", %{value: "new_value"})

      Enum.each(nodes, fn node ->
        value = :rpc.block_call(node, Application, :get_env, [:my_app, :my_setting])
        assert value == "new_value"
      end)
    end
  end

@ghost ghost assigned T-Dnzt Oct 8, 2018
@ghost ghost added the s2/wip 🚧 label Oct 8, 2018
Copy link
Contributor

@unnawut unnawut left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sending first batch of comments. Just managed to get half way through. - -"

apps/admin_api/test/support/channel_case.ex Show resolved Hide resolved
apps/ewallet/priv/repo/report_sample.exs Outdated Show resolved Hide resolved
apps/ewallet/priv/repo/report_minimum.exs Outdated Show resolved Hide resolved
apps/ewallet/priv/repo/report_sample.exs Outdated Show resolved Hide resolved
apps/ewallet_config/lib/ewallet_config/config.ex Outdated Show resolved Hide resolved
apps/ewallet_config/lib/ewallet_config/config.ex Outdated Show resolved Hide resolved
apps/ewallet_config/lib/ewallet_config/setting.ex Outdated Show resolved Hide resolved
apps/ewallet_config/lib/ewallet_config/setting.ex Outdated Show resolved Hide resolved
field(:type, :string)
field(:description, :string)
field(:options, :map)
field(:parent, :string)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do parent, parent_value and position work?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • position: Position is used to have a constant order for settings that we define. It cannot be updated and is only set at creation. We can add more settings in the seeds later and fix their positions.
  • parent and parent_value: those are used to link settings together in a very simple way. No logic is actually implemented for those, it's mostly intended to be used by clients (like the admin panel) to show settings in a logical way. So if someone selects gcs for file_storage, you can show all settings that have file_storage as a parent were parent_value=gcs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add this as a code comment? I think this will be easily lost

@@ -0,0 +1,170 @@
defmodule EWalletConfig.Validator do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should schema validators be part of EWalletDB and not EWalletConfig? :S

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's part of EWalletDB, then we can't use those validation functions in EWalletConfig. EWalletConfig has no dependencies on any other app, but all other apps depend on it directly or indirectly. I could move back some of the functions to EWalletDB tho, since they are not used in the validations in EWalletConfig anymore.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving back would be nice I think. I feel EWalletDB having to depend on validators from EWalletConfig is a bit counterintuitive.

Also, if the EWalletConfig's validators are the same as EWalletDB's, having duplicate code in this is case fine?

@unnawut
Copy link
Contributor

unnawut commented Oct 31, 2018

One final note is I'm not sure a race condition will be a problem here. Would be nice to have @sirn have a look at this PR as well.

@T-Dnzt
Copy link
Author

T-Dnzt commented Nov 5, 2018

@unnawut That's a good point about race conditions. I'm gonna fix it.

@mederic-p
Copy link
Contributor

@@ -9,6 +9,10 @@ defmodule AdminAPI.Application do
def start(_type, _args) do
import Supervisor.Spec
DeferredConfig.populate(:admin_api)

settings = Application.get_env(:admin_api, :settings)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I have a feeling that :settings shouldn't be in config (since it's not exactly configurable). Maybe as a @settings [...] in here (i.e. application.ex) instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or if we do it might make sense to use something among the line of:

config :admin_url,
  sender_email: {:ewallet_config, "default"},
  something_else: {:ewallet_config, nil}

…and iterate over the keys. In this case we can simplify EWalletConfig.Config.register_and_load to just:

EWalletConfig.Config.register_and_load(:admin_api)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, but from my point of view, it is configurable - you can set which settings you need in the app for it to work, we can add more in the future or remove them later. I can't really see how it's different from other config like generators or the mime_types.

About listing the settings, we could do that, but I don't really like the idea of having default in there. I see the settings as a global system, with one default value for all the apps, don't really want to let each app set their own default. At least, that's how I see it with the current implementation, it might evolve and requires apps to each have different default values 🤷‍♂️

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can use {:ewallet_config, "key_to_retrieve"} or something (where nil defaults to the key)?

My issue is mainly with having :settings key in config, which doesn't feel exactly right to me. As in, we're defining the values to be populated into app_env in a different ways than how these settings are normally populated (as in DeferredConfig, or other MFA-style configurations).

Also, if we use {:ewallet_config, ...} we could stub it in tests without involving ewallet_config at all.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sirn I'm not sure why we need that {:ewallet_config, ... part for? Also, one of the reason it's in the settings in that format is that I'm actually using the whole list of settings in the shared test helper function there https://github.com/omisego/ewallet/blob/436-settings-system/apps/ewallet_config/test/support/config_test_helper.ex#L15.

@@ -49,7 +49,11 @@ defmodule Mix.Tasks.Omg.Seed do
#

defp seed_spec([]) do
[{:ewallet_db, :seeds}]
[{:ewallet_db, :seeds}] ++ seed_spec(["--settings"])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be part of the default seeds na (just add it to seed_spec([])'s list rather than a new function to handle it). seed_spec([]) should be the "final" function to get called.

Copy link
Author

@T-Dnzt T-Dnzt Nov 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I tried to do here is give the option to only run the settings seeds for all our systems that are already deployed (thought it'd be nice :D) but also have the settings seed being run with the default ones. Totally fine to change it, but not sure I follow what you mean, can you give me some code?

alias EWalletDB.AuthToken

# :prod environment does not have a default :base_url value and should not have one.
# But we have a fallback value here so we can generate a friendly output message for seeding.
base_url = Application.get_env(:ewallet_db, :base_url) || "https://example.com"
base_url = Application.get_env(:ewallet, :base_url) || "https://example.com"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need the default https://example.com/? 🤔

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess not, removing it!

@sirn
Copy link
Contributor

sirn commented Nov 9, 2018 via email

@sirn
Copy link
Contributor

sirn commented Nov 12, 2018 via email

@T-Dnzt
Copy link
Author

T-Dnzt commented Nov 14, 2018

Merging for now, I've given a try to the {:ewallet_config, nil} approach but it would require too many changes, let's do it in another PR.

@T-Dnzt T-Dnzt merged commit 6f3450f into master Nov 14, 2018
@ghost ghost removed the s2/wip 🚧 label Nov 14, 2018
@sirn
Copy link
Contributor

sirn commented Nov 14, 2018 via email

@mederic-p mederic-p deleted the 436-settings-system branch November 20, 2018 07:18
@T-Dnzt T-Dnzt added this to Review in eWallet Apr 22, 2019
@T-Dnzt T-Dnzt moved this from Review to Done in eWallet Apr 22, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
eWallet
  
5-Done
Development

Successfully merging this pull request may close these issues.

Add settings system to allow provider to manage the configuration of their eWallets
4 participants