diff --git a/docs/setup.md b/docs/setup.md new file mode 100644 index 00000000..9caab2dd --- /dev/null +++ b/docs/setup.md @@ -0,0 +1,16 @@ +--- +title: Setup +--- + +## Configuration + +By default, Sagittarius loads `config/sagittarius.yml`. Set +`SAGITTARIUS_CONFIG_FILES` to a comma-separated list of configuration file +paths to load multiple files: + +```shell +SAGITTARIUS_CONFIG_FILES=config/sagittarius.yml,/etc/sagittarius/config.yml,/run/secrets/sagittarius.yml +``` + +Files are deep merged from left to right. Values in later files take +precedence over earlier files and the built-in defaults. diff --git a/lib/sagittarius/configuration.rb b/lib/sagittarius/configuration.rb index 3157139f..aee9f800 100644 --- a/lib/sagittarius/configuration.rb +++ b/lib/sagittarius/configuration.rb @@ -4,15 +4,28 @@ module Sagittarius class Configuration extend Code0::ZeroTrack::Memoize + CONFIG_FILES_ENV = 'SAGITTARIUS_CONFIG_FILES' + def self.config memoize(:config) do - file_config = YAML.safe_load_file(Rails.root.join('config/sagittarius.yml')).deep_symbolize_keys - defaults.deep_merge(file_config) - rescue Errno::ENOENT # config file does not exist + configured_files = ENV.fetch(CONFIG_FILES_ENV, nil) + config_files(configured_files).reduce(defaults) do |config, config_file| + file_config = YAML.safe_load_file(config_file).deep_symbolize_keys + config.deep_merge(file_config) + end + rescue Errno::ENOENT + raise if configured_files.present? + defaults end end + def self.config_files(configured_files = ENV.fetch(CONFIG_FILES_ENV, nil)) + return [Rails.root.join('config/sagittarius.yml')] if configured_files.blank? + + configured_files.split(',').map(&:strip).reject(&:empty?) + end + def self.application_setting_overrides config[:application_setting_overrides] end diff --git a/spec/lib/sagittarius/configuration_spec.rb b/spec/lib/sagittarius/configuration_spec.rb index 7da594e9..654ed718 100644 --- a/spec/lib/sagittarius/configuration_spec.rb +++ b/spec/lib/sagittarius/configuration_spec.rb @@ -4,25 +4,22 @@ RSpec.describe Sagittarius::Configuration do describe '.config' do - let(:yaml_config) do - <<~CONFIG - rails: - web: - threads: 4 - grpc: - threads: 8 - CONFIG - end + let(:default_config_file) { Rails.root.join('config/sagittarius.yml') } before do - allow(File).to receive(:open).and_call_original - allow(File).to receive(:open) - .with(Rails.root.join('config/sagittarius.yml'), instance_of(String)) - .and_yield(yaml_config) + allow(ENV).to receive(:fetch).and_call_original + allow(ENV).to receive(:fetch).with('SAGITTARIUS_CONFIG_FILES', nil).and_return(nil) described_class.clear_memoize(:config) end - it 'loads the config' do + it 'loads the default config file' do + allow(YAML).to receive(:safe_load_file).with(default_config_file).and_return( + 'rails' => { + 'web' => { 'threads' => 4 }, + 'grpc' => { 'threads' => 8 }, + } + ) + expect(described_class.config).to include( rails: a_hash_including( web: a_hash_including(threads: 4), @@ -30,6 +27,60 @@ ) ) end + + it 'uses built-in defaults when the default config file does not exist' do + allow(YAML).to receive(:safe_load_file).with(default_config_file).and_raise(Errno::ENOENT) + + expect(described_class.config).to eq(described_class.defaults) + end + + context 'when SAGITTARIUS_CONFIG_FILES contains multiple paths' do + before do + configured_files = ' config/base.yml,config/environment.yml, config/runtime.yml ' + allow(ENV).to receive(:fetch).with('SAGITTARIUS_CONFIG_FILES', nil) + .and_return(configured_files) + allow(YAML).to receive(:safe_load_file).with('config/base.yml').and_return( + 'rails' => { + 'web' => { 'threads' => 4, 'port' => 4000 }, + 'grpc' => { 'threads' => 4 }, + } + ) + allow(YAML).to receive(:safe_load_file).with('config/environment.yml').and_return( + 'rails' => { + 'web' => { 'threads' => 8 }, + 'grpc' => { 'host' => 'environment:50051' }, + } + ) + allow(YAML).to receive(:safe_load_file).with('config/runtime.yml').and_return( + 'rails' => { + 'web' => { 'threads' => 16 }, + } + ) + end + + it 'deep merges files in order with the last file taking precedence' do + expect(described_class.config).to include( + rails: a_hash_including( + web: a_hash_including(threads: 16, port: 4000), + grpc: a_hash_including(threads: 4, host: 'environment:50051') + ) + ) + end + + it 'loads each configured file in order' do + described_class.config + + expect(YAML).to have_received(:safe_load_file).ordered.with('config/base.yml') + expect(YAML).to have_received(:safe_load_file).ordered.with('config/environment.yml') + expect(YAML).to have_received(:safe_load_file).ordered.with('config/runtime.yml') + end + + it 'raises when a configured file does not exist' do + allow(YAML).to receive(:safe_load_file).with('config/environment.yml').and_raise(Errno::ENOENT) + + expect { described_class.config }.to raise_error(Errno::ENOENT) + end + end end describe '.defaults' do