diff --git a/CHANGELOG.md b/CHANGELOG.md index f1dcd6f3..bb1416c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,25 @@ # Changelog All notable changes to this project will be documented in this file. +## Unreleased +### Added +- Added file data resolver functionality: + - Added resolver for `file` + - Added resolver for `vault` + - Added DSL methods for defining custom resolvers +- Added support for kv storage for vault +- Fix using of non-kv storage + +```ruby +Qonfig.define_resolver(:https) do |file_path| + Net::HTTP.get(URI("https:://#{file_path}")) +end + +class Config < Qonfig::DataSet + load_from_yaml "https://yamlhost.com/cool.yaml" +end +``` + ## [0.27.0] - 2022-01-12 ### Changed - Drop Ruby 2.5 support. diff --git a/README.md b/README.md index 92e722f1..1f540db9 100644 --- a/README.md +++ b/README.md @@ -2049,6 +2049,8 @@ end - **Daily work** - [Save to JSON file](#save-to-json-file) (`save_to_json`) - [Save to YAML file](#save-to-yaml-file) (`save_to_yaml`) +- **Define file data resolvers** + - [Working with file data resolvers](#working-with-file-data-resolvers) --- @@ -3155,6 +3157,35 @@ dynamic: 10 --- +### Working with file data resolvers + +You can define custom file data resolver + +```ruby +Qonfig.define_resolver(:http) do |file_path| + final_url = URI("http://#{file_path}") + Net::HTTP.get(final_url) +rescue SocketError => error + raise Qonfig::FileNotFoundError, error.message +end + +class AppConfig < Qonfig::DataSet + load_from_yaml "http://content-holder.com/settings.yml" +end +``` + +Also, you can set this file data resolver to default resolver. + +```ruby +Qonfig.set_default_resolver :http + +class AppConfig < Qonfig::DataSet + load_from_yaml "content-holder.com/settings.yml" # same as previous example +end +``` + +--- + ### Plugins - [toml](#plugins-toml) (provides `load_from_toml`, `save_to_toml`, `expose_toml`); @@ -3290,6 +3321,9 @@ config = Config.new - depends on `vault` gem ([link](https://github.com/hashicorp/vault-ruby)) (tested on `>= 0.1`); - provides `.load_from_vault` (works in `.load_from_yaml` manner ([doc](#load-from-yaml-file))); - provides `.expose_vault` (works in `.expose_yaml` manner ([doc](#expose-yaml))); +- provides custom file data resolver `vault://path/to/file/key.yml`; +- you can use version option to use specific version of config in kv storage (dsl and resolver); +- kv storage is used by default, but you can use logical storage by setting `use_kv` to `false`; ```ruby # 1) require external dependency @@ -3323,6 +3357,9 @@ Qonfig.plugin(:vault) - External validation class with an importing api for better custom validations; - Setting value changement trace (in `anyway_config` manner); - Instantiation and reloading callbacks; + - Add supported formats for resolvers: for example, if you use vault resolver, you can't use json format; + - Refactor params passed to methods: use value objects instead; + - Refactor loaders: use base class for them; ## Build diff --git a/lib/qonfig.rb b/lib/qonfig.rb index 6bbdb00f..dd4af8fc 100644 --- a/lib/qonfig.rb +++ b/lib/qonfig.rb @@ -22,6 +22,7 @@ module Qonfig require_relative 'qonfig/imports' require_relative 'qonfig/plugins' require_relative 'qonfig/compacted' + require_relative 'qonfig/file_data_resolving' # @api public # @since 0.4.0 @@ -31,6 +32,10 @@ module Qonfig # @since 0.20.0 extend Validation::PredefinitionMixin + # @api public + # @since 0.26.0 + extend FileDataResolving::Mixin + # @since 0.20.0 define_validator(:integer) { |value| value.is_a?(Integer) } # @since 0.20.0 @@ -60,6 +65,15 @@ module Qonfig # @since 0.20.0 define_validator(:not_nil) { |value| value == nil } + # @since 0.26.0 + define_resolver(:file) do |file_path| + ::File.read(file_path) + rescue Errno::ENOENT => error + raise Qonfig::FileNotFoundError, error.message + end + # @since 0.26.0 + set_default_resolver :file + # @since 0.12.0 register_plugin('toml', Qonfig::Plugins::TOML) # @since 0.19.0 diff --git a/lib/qonfig/commands/definition/load_from_json.rb b/lib/qonfig/commands/definition/load_from_json.rb index e64fff5b..3230837f 100644 --- a/lib/qonfig/commands/definition/load_from_json.rb +++ b/lib/qonfig/commands/definition/load_from_json.rb @@ -18,14 +18,22 @@ class Qonfig::Commands::Definition::LoadFromJSON < Qonfig::Commands::Base # @sicne 0.5.0 attr_reader :strict + # @return [Hash] + # + # @api private + # @since 0.26.0 + attr_reader :file_resolve_options + # @param file_path [String, Pathname] # @option strict [Boolean] + # @option file_resolve_options [Hash] # # @api private # @since 0.5.0 - def initialize(file_path, strict: true) + def initialize(file_path, strict: true, file_resolve_options: {}) @file_path = file_path @strict = strict + @file_resolve_options = file_resolve_options end # @param data_set [Qonfig::DataSet] diff --git a/lib/qonfig/commands/definition/load_from_yaml.rb b/lib/qonfig/commands/definition/load_from_yaml.rb index a393048c..a1ec3833 100644 --- a/lib/qonfig/commands/definition/load_from_yaml.rb +++ b/lib/qonfig/commands/definition/load_from_yaml.rb @@ -18,14 +18,22 @@ class Qonfig::Commands::Definition::LoadFromYAML < Qonfig::Commands::Base # @since 0.2.0 attr_reader :strict + # @return [Hash] + # + # @api private + # @since 0.26.0 + attr_reader :file_resolve_options + # @param file_path [String, Pathname] # @option strict [Boolean] + # @option file_resolve_options [Hash] # # @api private # @since 0.2.0 - def initialize(file_path, strict: true) + def initialize(file_path, strict: true, file_resolve_options: {}) @file_path = file_path @strict = strict + @file_resolve_options = file_resolve_options end # @param data_set [Qonfig::DataSet] @@ -37,7 +45,8 @@ def initialize(file_path, strict: true) # @api private # @since 0.2.0 def call(data_set, settings) - yaml_data = Qonfig::Loaders::YAML.load_file(file_path, fail_on_unexist: strict) + yaml_data = Qonfig::Loaders::YAML + .load_file(file_path, fail_on_unexist: strict, **file_resolve_options) raise( Qonfig::IncompatibleYAMLStructureError, diff --git a/lib/qonfig/commands/instantiation/values_file.rb b/lib/qonfig/commands/instantiation/values_file.rb index 6ac83420..733c104c 100644 --- a/lib/qonfig/commands/instantiation/values_file.rb +++ b/lib/qonfig/commands/instantiation/values_file.rb @@ -61,22 +61,30 @@ class Qonfig::Commands::Instantiation::ValuesFile < Qonfig::Commands::Base # @since 0.17.0 attr_reader :expose + # @return [Hash] + # + # @api private + # @since 0.26.0 + attr_reader :file_resolve_options + # @param file_path [String, Symbol, Pathname] # @param caller_location [String] # @option format [String, Symbol] # @option strict [Boolean] # @option expose [NilClass, String, Symbol] + # @option file_resolve_options [Hash] # @return [void] # # @api private # @since 0.17.0 - # @version 0.22.0 + # @version 0.26.0 def initialize( file_path, caller_location, format: DEFAULT_FORMAT, strict: DEFAULT_STRICT_BEHAVIOR, - expose: NO_EXPOSE + expose: NO_EXPOSE, + file_resolve_options: {} ) prevent_incompatible_attributes!(file_path, format, strict, expose) @@ -85,6 +93,7 @@ def initialize( @format = format @strict = strict @expose = expose + @file_resolve_options = file_resolve_options end # @param data_set [Qonfig::DataSet] @@ -117,7 +126,8 @@ def load_settings_values # @api private # @since 0.17.0 def load_from_file - Qonfig::Loaders.resolve(format).load_file(file_path, fail_on_unexist: strict).tap do |values| + load_options = { fail_on_unexist: strict, **file_resolve_options } + Qonfig::Loaders.resolve(format).load_file(file_path, **load_options).tap do |values| raise( Qonfig::IncompatibleDataStructureError, 'Setting values must be a hash-like structure' diff --git a/lib/qonfig/data_set.rb b/lib/qonfig/data_set.rb index 61313bb9..115b49b0 100644 --- a/lib/qonfig/data_set.rb +++ b/lib/qonfig/data_set.rb @@ -101,6 +101,7 @@ def reload!(settings_map = {}, &configurations) # @option format [String, Symbol] # @option strict [Boolean] # @option expose [NilClass, String, Symbol] Environment key + # @option **file_resolve_options [Hash] # @param configurations [Block] # @return [void] # @@ -109,10 +110,18 @@ def reload!(settings_map = {}, &configurations) # @api public # @since 0.17.0 # @version 0.22.0 - def load_from_file(file_path, format: :dynamic, strict: true, expose: nil, &configurations) + def load_from_file( + file_path, + format: :dynamic, + strict: true, + expose: nil, + **file_resolve_options, + &configurations + ) thread_safe_access do load_setting_values_from_file( - file_path, format: format, strict: strict, expose: expose, &configurations + file_path, format: format, strict: strict, + expose: expose, file_resolve_options: file_resolve_options, &configurations ) end end @@ -120,6 +129,7 @@ def load_from_file(file_path, format: :dynamic, strict: true, expose: nil, &conf # @param file_path [String, Symbol, Pathname] # @option strict [Boolean] # @option expose [NilClass, String, Symbol] Environment key + # @option **file_resolve_options [Hash] # @param configurations [Block] # @return [void] # @@ -128,8 +138,11 @@ def load_from_file(file_path, format: :dynamic, strict: true, expose: nil, &conf # @api public # @since 0.17.0 # @version 0.22.0 - def load_from_yaml(file_path, strict: true, expose: nil, &configurations) - load_from_file(file_path, format: :yml, strict: strict, expose: expose, &configurations) + def load_from_yaml(file_path, strict: true, expose: nil, **file_resolve_options, &configurations) + load_from_file( + file_path, format: :yml, strict: strict, + expose: expose, **file_resolve_options, &configurations + ) end # @param file_path [String, Symbol, Pathname] @@ -521,6 +534,7 @@ def load!(settings_map = {}, &configurations) # @option strict [Boolean] # @option expose [NilClass, String, Symbol] # @option callcer_location [NilClass, String] + # @option **file_resolve_options [Hash] # @param configurations [Block] # @return [void] # @@ -535,10 +549,12 @@ def load_setting_values_from_file( strict: true, expose: nil, caller_location: nil, + file_resolve_options: {}, &configurations ) Qonfig::Commands::Instantiation::ValuesFile.new( - file_path, caller_location, format: format, strict: strict, expose: expose + file_path, caller_location, format: format, + strict: strict, expose: expose, file_resolve_options: file_resolve_options ).call(self, settings) apply_settings(&configurations) end diff --git a/lib/qonfig/dsl.rb b/lib/qonfig/dsl.rb index a9432430..40eb6275 100644 --- a/lib/qonfig/dsl.rb +++ b/lib/qonfig/dsl.rb @@ -156,15 +156,16 @@ def compose(data_set_klass) # @param file_path [String, Pathname] # @option strict [Boolean] + # @option **file_resolve_options [Hash] # @return [void] # # @see Qonfig::Commands::Definition::LoadFromYAML # # @api public # @since 0.2.0 - def load_from_yaml(file_path, strict: true) + def load_from_yaml(file_path, strict: true, **file_resolve_options) definition_commands << Qonfig::Commands::Definition::LoadFromYAML.new( - file_path, strict: strict + file_path, strict: strict, file_resolve_options: file_resolve_options ) end @@ -202,14 +203,17 @@ def load_from_env(convert_values: false, prefix: nil, trim_prefix: false) # @param file_path [String, Pathname] # @option strict [Boolean] + # @option **file_resolve_options [Hash] # @return [void] # # @see Qonfig::Commands::Definition::LoadFromJSON # # @api public # @since 0.5.0 - def load_from_json(file_path, strict: true) - definition_commands << Qonfig::Commands::Definition::LoadFromJSON.new(file_path, strict: strict) + def load_from_json(file_path, strict: true, **file_resolve_options) + definition_commands << Qonfig::Commands::Definition::LoadFromJSON.new( + file_path, strict: strict, file_resolve_options: file_resolve_options + ) end # @param file_path [String, Pathname] diff --git a/lib/qonfig/file_data_resolving.rb b/lib/qonfig/file_data_resolving.rb new file mode 100644 index 00000000..8a5b171f --- /dev/null +++ b/lib/qonfig/file_data_resolving.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# @api private +# @since 0.26.0 +module Qonfig::FileDataResolving + require_relative 'file_data_resolving/resolver' + require_relative 'file_data_resolving/mixin' +end diff --git a/lib/qonfig/file_data_resolving/mixin.rb b/lib/qonfig/file_data_resolving/mixin.rb new file mode 100644 index 00000000..39b8c58a --- /dev/null +++ b/lib/qonfig/file_data_resolving/mixin.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# @api public +# @since 0.26.0 +module Qonfig::FileDataResolving::Mixin + # @param scheme_name [Symbol,String] + # @param block [Block] + # @return [void] + # + # @api public + # @since 0.26.0 + def define_resolver(scheme_name, &block) + Qonfig::FileDataResolving::Resolver.add_resolver!(scheme_name, block) + end + + # @param scheme_name [Symbol,String] + # @return [void] + # + # @api public + # @since 0.26.0 + def set_default_resolver(scheme_name) + Qonfig::FileDataResolving::Resolver.set_default_resolver!(scheme_name) + end +end diff --git a/lib/qonfig/file_data_resolving/resolver.rb b/lib/qonfig/file_data_resolving/resolver.rb new file mode 100644 index 00000000..51694a15 --- /dev/null +++ b/lib/qonfig/file_data_resolving/resolver.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# @api private +# @since 0.26.0 +class Qonfig::FileDataResolving::Resolver + class << self + # @param scheme [Symbol,String] + # @param resolver_proc [Proc] + # @return [void] + # + # @api private + # @since 0.26.0 + def add_resolver!(scheme, resolver_proc) + @resolvers ||= {} + @resolvers[scheme.to_sym] = resolver_proc + end + + # @param scheme_name [Symbol,String] + # @return [void] + # + # @api private + # @since 0.26.0 + def set_default_resolver!(scheme_name) + @default_resolver = resolvers.fetch(scheme_name.to_sym) + end + + # @param file_path [String,Pathname] + # @return [String] + # @raise [Qonfig::FileNotFoundError] + # + # @api private + # @since 0.26.0 + def resolve!(file_path, **options) + scheme_name = URI(file_path.to_s).scheme + scheme_name = scheme_name.to_sym unless scheme_name == nil + resolver = resolvers[scheme_name] || default_resolver + resolver.call(file_path.to_s.split('://').last, **options) + end + + private + + # @return [Array] + # + # @api private + # @since 0.26.0 + attr_reader :resolvers + + # @return [Proc] + # + # @api private + # @since 0.26.0 + attr_reader :default_resolver + end +end diff --git a/lib/qonfig/loaders/basic.rb b/lib/qonfig/loaders/basic.rb index d6d45196..262ad1a5 100644 --- a/lib/qonfig/loaders/basic.rb +++ b/lib/qonfig/loaders/basic.rb @@ -30,10 +30,11 @@ def load_empty_data # # @api private # @since 0.5.0 - def load_file(file_path, fail_on_unexist: true) - load(::File.read(file_path)) - rescue Errno::ENOENT => error - fail_on_unexist ? (raise Qonfig::FileNotFoundError, error.message) : load_empty_data + def load_file(file_path, fail_on_unexist: true, **options) + data = Qonfig::FileDataResolving::Resolver.resolve!(file_path, **options) + load(data) + rescue Qonfig::FileNotFoundError + fail_on_unexist ? raise : load_empty_data end end end diff --git a/lib/qonfig/plugins/toml/commands/definition/load_from_toml.rb b/lib/qonfig/plugins/toml/commands/definition/load_from_toml.rb index 48752b80..f95d3078 100644 --- a/lib/qonfig/plugins/toml/commands/definition/load_from_toml.rb +++ b/lib/qonfig/plugins/toml/commands/definition/load_from_toml.rb @@ -19,14 +19,21 @@ class Qonfig::Commands::Definition::LoadFromTOML < Qonfig::Commands::Base # @since 0.12.0 attr_reader :strict + # @return [Hash] + # + # @api private + # @since 0.26.0 + attr_reader :file_resolve_options + # @param file_path [String] # @option strict [Boolean] # # @api private # @since 0.12.0 - def initialize(file_path, strict: true) + def initialize(file_path, strict: true, file_resolve_options: {}) @file_path = file_path @strict = strict + @file_resolve_options = file_resolve_options end # @param data_set [Qonfig::DataSet] @@ -36,7 +43,8 @@ def initialize(file_path, strict: true) # @api private # @since 0.12.0 def call(data_set, settings) - toml_data = Qonfig::Loaders::TOML.load_file(file_path, fail_on_unexist: strict) + toml_data = Qonfig::Loaders::TOML + .load_file(file_path, fail_on_unexist: strict, **file_resolve_options) toml_based_settings = build_data_set_klass(toml_data).new.settings settings.__append_settings__(toml_based_settings) end diff --git a/lib/qonfig/plugins/toml/data_set.rb b/lib/qonfig/plugins/toml/data_set.rb index 67b6eee8..4ce94a00 100644 --- a/lib/qonfig/plugins/toml/data_set.rb +++ b/lib/qonfig/plugins/toml/data_set.rb @@ -20,6 +20,7 @@ def save_to_toml(path:, options: Qonfig::Uploaders::TOML::DEFAULT_OPTIONS, &valu # @param file_path [String, Pathmame] # @option strict [Boolean] # @option expose [NilClass, String, Symbol] Environment key + # @option **file_resolve_options [Hash] # @param configuration [Block] # @return [void] # @@ -28,7 +29,10 @@ def save_to_toml(path:, options: Qonfig::Uploaders::TOML::DEFAULT_OPTIONS, &valu # @api public # @since 0.17.0 # @version 0.21.0 - def load_from_toml(file_path, strict: true, expose: nil, &configuration) - load_from_file(file_path, format: :toml, strict: strict, expose: expose, &configuration) + def load_from_toml(file_path, strict: true, expose: nil, **file_resolve_options, &configuration) + load_from_file( + file_path, format: :toml, strict: strict, + expose: expose, **file_resolve_options, &configuration + ) end end diff --git a/lib/qonfig/plugins/toml/dsl.rb b/lib/qonfig/plugins/toml/dsl.rb index 8a3e97d4..abda57a2 100644 --- a/lib/qonfig/plugins/toml/dsl.rb +++ b/lib/qonfig/plugins/toml/dsl.rb @@ -12,9 +12,9 @@ module Qonfig::DSL # @api public # @since 0.12.0 # @version 0.20.0 - def load_from_toml(file_path, strict: true) + def load_from_toml(file_path, strict: true, **file_resolve_options) definition_commands << Qonfig::Commands::Definition::LoadFromTOML.new( - file_path, strict: strict + file_path, strict: strict, file_resolve_options: file_resolve_options ) end diff --git a/lib/qonfig/plugins/vault.rb b/lib/qonfig/plugins/vault.rb index a1e88b5e..6d932168 100644 --- a/lib/qonfig/plugins/vault.rb +++ b/lib/qonfig/plugins/vault.rb @@ -19,6 +19,28 @@ def install! require_relative 'vault/commands/definition/load_from_vault' require_relative 'vault/commands/definition/expose_vault' require_relative 'vault/dsl' + + define_resolvers! + end + + private + + # @return [void] + # + # @since 0.26.0 + # @api private + def define_resolvers! + ::Qonfig.define_resolver(:vault) do |file_path, **options| + *vault_path, file_name = file_path.split(File::SEPARATOR) + vault_path = vault_path.join(File::SEPARATOR) + files = Qonfig::Loaders::Vault + .load_file(vault_path, **options, transform_values: false) + result = files[file_name.to_sym] + if result == nil + raise Qonfig::FileNotFoundError, "Can't load file with name #{file_name}" + end + result + end end end end diff --git a/lib/qonfig/plugins/vault/commands/definition/expose_vault.rb b/lib/qonfig/plugins/vault/commands/definition/expose_vault.rb index ec8d1b9f..7452fd85 100644 --- a/lib/qonfig/plugins/vault/commands/definition/expose_vault.rb +++ b/lib/qonfig/plugins/vault/commands/definition/expose_vault.rb @@ -42,6 +42,12 @@ class Qonfig::Commands::Definition::ExposeVault < Qonfig::Commands::Base # @since 0.25.0 attr_reader :env + # @return [Hash] + # + # @api private + # @since 0.26.0 + attr_reader :file_resolve_options + # @param path [String Pathname] # @option strict [Boolean] # @option via [Symbol] @@ -50,7 +56,7 @@ class Qonfig::Commands::Definition::ExposeVault < Qonfig::Commands::Base # # @api private # @since 0.25.0 - def initialize(path, strict: true, via:, env:) + def initialize(path, strict: true, via:, env:, file_resolve_options: {}) unless env.is_a?(Symbol) || env.is_a?(String) || env.is_a?(Numeric) raise Qonfig::ArgumentError, ':env should be a string or a symbol' end @@ -58,10 +64,11 @@ def initialize(path, strict: true, via:, env:) raise Qonfig::ArgumentError, ':env should be provided' if env.to_s.empty? raise Qonfig::ArgumentError, 'used :via is unsupported' unless EXPOSERS.key?(via) - @path = path - @strict = strict - @via = via - @env = env + @path = path + @strict = strict + @via = via + @env = env + @file_resolve_options = file_resolve_options end # @param data_set [Qonfig::DataSet] @@ -128,7 +135,7 @@ def expose_env_key!(settings) # @api private # @since 0.25.0 def load_vault_data(path) - Qonfig::Loaders::Vault.load_file(path, fail_on_unexist: strict) + Qonfig::Loaders::Vault.load_file(path, fail_on_unexist: strict, **file_resolve_options) end # @param vault_data [Hash] diff --git a/lib/qonfig/plugins/vault/commands/definition/load_from_vault.rb b/lib/qonfig/plugins/vault/commands/definition/load_from_vault.rb index 7236744a..575e8ce7 100644 --- a/lib/qonfig/plugins/vault/commands/definition/load_from_vault.rb +++ b/lib/qonfig/plugins/vault/commands/definition/load_from_vault.rb @@ -18,14 +18,22 @@ class Qonfig::Commands::Definition::LoadFromVault < Qonfig::Commands::Base # @since 0.25.0 attr_reader :strict + # @return [Hash] + # + # @api private + # @since 0.26.0 + attr_reader :file_resolve_options + # @param path [String] # @option strict [Boolean] + # @option file_resolve_options [Hash] # # @api private # @since 0.25.0 - def initialize(path, strict: true) + def initialize(path, strict: true, file_resolve_options: {}) @path = path @strict = strict + @file_resolve_options = file_resolve_options end # @param data_set [Qonfig::DataSet] @@ -35,7 +43,8 @@ def initialize(path, strict: true) # @api private # @since 0.25.0 def call(_data_set, settings) - vault_data = Qonfig::Loaders::Vault.load_file(path, fail_on_unexist: strict) + vault_data = Qonfig::Loaders::Vault + .load_file(path, fail_on_unexist: strict, **file_resolve_options) vault_based_settings = build_data_set_klass(vault_data).new.settings settings.__append_settings__(vault_based_settings) end diff --git a/lib/qonfig/plugins/vault/dsl.rb b/lib/qonfig/plugins/vault/dsl.rb index 010af2c9..e0bea51f 100644 --- a/lib/qonfig/plugins/vault/dsl.rb +++ b/lib/qonfig/plugins/vault/dsl.rb @@ -5,15 +5,16 @@ module Qonfig::DSL # @param path [String, Pathname] # @option strict [Boolean] + # @option **file_resolve_options [Hash] # @return [void] # # @see Qonfig::Commands::Definition::LoadFromVault # # @api public # @since 0.25.0 - def load_from_vault(path, strict: true) + def load_from_vault(path, strict: true, **file_resolve_options) definition_commands << Qonfig::Commands::Definition::LoadFromVault.new( - path, strict: strict + path, strict: strict, file_resolve_options: file_resolve_options ) end @@ -21,15 +22,16 @@ def load_from_vault(path, strict: true) # @option strict [Boolean] # @option via [Symbol] # @option env [Symbol, String] + # @option **resolve_options [Hash] # @return [void] # # @see Qonfig::Commands::Definition::ExposeVault # # @api public # @since 0.25.0 - def expose_vault(path, strict: true, via:, env:) + def expose_vault(path, strict: true, via:, env:, **file_resolve_options) definition_commands << Qonfig::Commands::Definition::ExposeVault.new( - path, strict: strict, via: via, env: env + path, strict: strict, via: via, env: env, file_resolve_options: file_resolve_options ) end end diff --git a/lib/qonfig/plugins/vault/loaders/vault.rb b/lib/qonfig/plugins/vault/loaders/vault.rb index 9a43f52a..aa223afe 100644 --- a/lib/qonfig/plugins/vault/loaders/vault.rb +++ b/lib/qonfig/plugins/vault/loaders/vault.rb @@ -15,18 +15,19 @@ class Qonfig::Loaders::Vault < Qonfig::Loaders::Basic class << self # @param path [String, Pathname] # @option fail_on_unexist [Boolean] + # @option version [String, Integer] # @return [Object] # # @raise [Qonfig::FileNotFoundError] # # @api private # @since 0.25.0 - def load_file(path, fail_on_unexist: true) - data = ::Vault.with_retries(Vault::HTTPError) do - ::Vault.logical.read(path.to_s)&.data&.dig(:data) - end - raise Qonfig::FileNotFoundError, "Path #{path} not exist" if data.nil? && fail_on_unexist + def load_file(path, fail_on_unexist: true, transform_values: true, version: nil, use_kv: true) + data = load_data(path, version, use_kv) + raise Qonfig::FileNotFoundError, "Path #{path} not exist" if data == nil && fail_on_unexist result = data || empty_data + return result unless transform_values + deep_transform_values(result) rescue Vault::VaultError => error raise(Qonfig::VaultLoaderError.new(error.message).tap do |exception| @@ -44,6 +45,25 @@ def empty_data private + # @param file_path [String] + # @param version [Integer] + # @return [Object] + # + # @api private + # @since 0.26.0 + def load_data(file_path, version, use_kv) + response = ::Vault.with_retries(::Vault::HTTPError) do + if use_kv + mount_path, secret_path = file_path.to_s.split(::File::Separator, 2) + ::Vault.kv(mount_path).read(secret_path, version) + else + ::Vault.logical.read(file_path.to_s) + end + end + + response&.data + end + # @param vault_data [Hash] # @return [Object] # diff --git a/spec/features/plugins/vault/context.rb b/spec/features/plugins/vault/context.rb new file mode 100644 index 00000000..c3d505dd --- /dev/null +++ b/spec/features/plugins/vault/context.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +shared_context 'vault context' do + before { allow(Vault).to receive(:logical).and_return(logical_double) } + + before { allow(Vault).to receive(:kv).and_return(kv_double) } + + let(:logical_double) { instance_double(Vault::Logical) } + let(:kv_double) { instance_double(Vault::KV) } +end diff --git a/spec/features/plugins/vault/expose_vault_spec.rb b/spec/features/plugins/vault/expose_vault_spec.rb index 854ed2e1..a580a825 100644 --- a/spec/features/plugins/vault/expose_vault_spec.rb +++ b/spec/features/plugins/vault/expose_vault_spec.rb @@ -1,29 +1,27 @@ # frozen_string_literal: true +require_relative 'context' + describe 'Plugins(vault): expose vault', plugin: :vault do before { stub_const('VaultConfig', vault_class) } - before { allow(Vault).to receive(:logical).and_return(logical_double) } - - let(:logical_double) { instance_double(Vault::Logical) } - let(:returned_data) do instance_double(Vault::Secret).tap do |instance| allow(instance).to receive(:data).and_return(secret_data) end end let(:secret_data) do - { data: { production: { kek: 'pek', cheburek: true }, other_key: '<%= 1 + 1 %>' } } + { production: { kek: 'pek', cheburek: true }, other_key: '<%= 1 + 1 %>' } end let(:vault_class) do Class.new(Qonfig::DataSet) do setting :based_on_path do - expose_vault 'kv/data/path_based', via: :path, env: :production + expose_vault 'kv/data/path_based', via: :path, env: :production, use_kv: false end setting :based_on_env_key do - expose_vault 'kv/data/env_key', via: :env_key, env: 'production' + expose_vault 'kv/data/env_key', via: :env_key, env: 'production', use_kv: false end end end @@ -45,7 +43,7 @@ specify 'raises an error' do expect do Class.new(Qonfig::DataSet) do - expose_vault 'kv/data/path_based', via: Object.new, env: :production + expose_vault 'kv/data/path_based', via: Object.new, env: :production, use_kv: false end end.to raise_error(Qonfig::ArgumentError) end @@ -55,7 +53,7 @@ specify 'raises an error' do expect do Class.new(Qonfig::DataSet) do - expose_vault 'kv/data/path_based', via: :path, env: Object.new + expose_vault 'kv/data/path_based', via: :path, env: Object.new, use_kv: false end end.to raise_error(Qonfig::ArgumentError) end @@ -65,7 +63,7 @@ specify 'raises an error' do expect do Class.new(Qonfig::DataSet) do - expose_vault 'kv/data/path_based', via: :kek, env: :production + expose_vault 'kv/data/path_based', via: :kek, env: :production, use_kv: false end end.to raise_error(Qonfig::ArgumentError) end @@ -75,7 +73,7 @@ specify 'raises an error' do expect do Class.new(Qonfig::DataSet) do - expose_vault 'kv/data/path_based', via: :path, env: '' + expose_vault 'kv/data/path_based', via: :path, env: '', use_kv: false end end.to raise_error(Qonfig::ArgumentError) end @@ -84,7 +82,7 @@ context "when provided key doesn't exist" do let(:vault_class) do Class.new(Qonfig::DataSet) do - expose_vault 'kv/data/env_key', via: :env_key, env: 'kekduction' + expose_vault 'kv/data/env_key', via: :env_key, env: 'kekduction', use_kv: false end end @@ -99,7 +97,8 @@ let(:vault_class) do Class.new(Qonfig::DataSet) do setting :based_on_env_key do - expose_vault 'kv/data/env_key', via: :env_key, env: 'production', strict: false + expose_vault 'kv/data/env_key', + via: :env_key, env: 'production', strict: false, use_kv: false end end end diff --git a/spec/features/plugins/vault/load_from_vault_spec.rb b/spec/features/plugins/vault/load_from_vault_spec.rb index 690751b3..491dc16d 100644 --- a/spec/features/plugins/vault/load_from_vault_spec.rb +++ b/spec/features/plugins/vault/load_from_vault_spec.rb @@ -1,21 +1,22 @@ # frozen_string_literal: true +require_relative 'context' + describe 'Plugins(vault): Load from vault kv store', plugin: :vault do - before { stub_const('VaultConfig', vault_class) } + include_context 'vault context' - before { allow(Vault).to receive(:logical).and_return(logical_double) } + before { stub_const('VaultConfig', vault_class) } let(:returned_data) do instance_double(Vault::Secret).tap do |instance| allow(instance).to receive(:data).and_return(secret_data) end end - let(:logical_double) { instance_double(Vault::Logical) } - let(:secret_data) { Hash[data: { kek: true, pek: 'cheburek', nested: Hash[key: 123] }] } + let(:secret_data) { Hash[kek: true, pek: 'cheburek', nested: { key: 123 }] } let(:vault_class) do Class.new(Qonfig::DataSet) do - load_from_vault 'kv/data/development' + load_from_vault 'kv/data/development', use_kv: false end end @@ -42,7 +43,7 @@ context 'with Pathname at path argument' do let(:vault_class) do Class.new(Qonfig::DataSet) do - load_from_vault Pathname('kv/data/development') + load_from_vault Pathname('kv/data/development'), use_kv: false end end @@ -56,7 +57,7 @@ context 'when strict set to false' do let(:vault_class) do Class.new(Qonfig::DataSet) do - load_from_vault 'kv/data/development', strict: false + load_from_vault 'kv/data/development', strict: false, use_kv: false end end @@ -77,4 +78,24 @@ expect { VaultConfig.new }.to raise_error(Qonfig::VaultLoaderError, 'Cool error') end end + + context 'when version specified' do + let(:vault_class) do + Class.new(Qonfig::DataSet) do + load_from_vault 'kv/data/development', version: 2, use_kv: true + end + end + + let(:expected_path) { 'data/development' } + + specify 'uses kv store engine' do + expect(Vault).to receive(:kv).with('kv').and_return(kv_double) + expect(kv_double).to receive(:read).with(expected_path, 2).and_return(returned_data) + + VaultConfig.new.settings.tap do |conf| + expect(conf).to have_attributes(kek: true, pek: 'cheburek') + expect(conf.nested.key).to eq(123) + end + end + end end diff --git a/spec/features/plugins/vault/load_from_yaml_spec.rb b/spec/features/plugins/vault/load_from_yaml_spec.rb new file mode 100644 index 00000000..3edf8f39 --- /dev/null +++ b/spec/features/plugins/vault/load_from_yaml_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require_relative 'context' + +describe 'Plugins(vault): Load yaml from vault kv store', plugin: :vault do + include_context 'vault context' + + before { stub_const('VaultConfig', vault_class) } + + let(:returned_data) do + instance_double(Vault::Secret).tap do |instance| + allow(instance).to receive(:data).and_return(secret_data) + end + end + + let(:secret_data) { Hash['file.yml': yaml_content] } + let(:yaml_content) { YAML.dump(kek: 'pek') } + + let(:vault_class) do + Class.new(Qonfig::DataSet) do + load_from_yaml 'vault://kv/data/development/file.yml', use_kv: false + end + end + + specify 'defines config object by yaml instructions' do + expect(Vault.logical).to receive(:read).with('kv/data/development').and_return(returned_data) + VaultConfig.new.settings.tap do |conf| + expect(conf).to have_attributes(kek: 'pek') + end + end + + context "when key doesn't exist" do + let(:secret_data) { Hash[data: { 'other_file.yml': yaml_content }] } + + specify 'raises error' do + expect(Vault.logical).to receive(:read).with('kv/data/development').and_return(returned_data) + expect { VaultConfig.new }.to raise_error(Qonfig::FileNotFoundError) + end + end + + context 'when version specified' do + let(:vault_class) do + Class.new(Qonfig::DataSet) do + load_from_yaml 'vault://kv/data/development/file.yml', version: 2 + end + end + + let(:expected_path) { 'data/development' } + + specify 'uses kv store engine' do + expect(Vault).to receive(:kv).with('kv').and_return(kv_double) + expect(kv_double).to receive(:read).with(expected_path, 2).and_return(returned_data) + + VaultConfig.new.settings.tap do |conf| + expect(conf).to have_attributes(kek: 'pek') + end + end + end +end