rrx_config 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RrxConfig
4
+ module Aws
5
+ PROFILE_VARIABLE = 'RRX_AWS_PROFILE'
6
+ REGION_VARIABLE = 'RRX_AWS_REGION'
7
+ REGION_DEFAULT = 'us-west-2'
8
+ PROFILE_DEFAULT = 'default'
9
+ ECS_METADATA_VARIABLE = 'ECS_CONTAINER_METADATA_URI_V4'
10
+
11
+ class << self
12
+ def profile?
13
+ ENV.include?(PROFILE_VARIABLE)
14
+ end
15
+
16
+ def profile
17
+ ENV.fetch(PROFILE_VARIABLE, PROFILE_DEFAULT) unless Rails.env.production?
18
+ end
19
+
20
+ def credentials
21
+ @credentials ||= profile_credentials || environment_credentials || ecs_credentials || ec2_credentials
22
+ end
23
+
24
+ def client_args
25
+ { region:, credentials: }
26
+ end
27
+
28
+ def region
29
+ ENV.fetch(REGION_VARIABLE, REGION_DEFAULT)
30
+ end
31
+
32
+ def profile_credentials
33
+ if profile?
34
+ RrxConfig.logger.info 'Using shared credentials'
35
+ require 'aws-sdk-core/shared_credentials'
36
+ ::Aws::SharedCredentials.new(profile_name: profile)
37
+ end
38
+ end
39
+
40
+ def environment_credentials
41
+ if ENV.include?('AWS_ACCESS_KEY_ID')
42
+ require 'aws-sdk-core/credentials'
43
+ RrxConfig.logger.info 'Using explicit credentials'
44
+ ::Aws::Credentials.new(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY'])
45
+ end
46
+ end
47
+
48
+ def ecs_credentials
49
+ if ENV.include?(ECS_METADATA_VARIABLE)
50
+ RrxConfig.logger.info 'Using ECS credentials'
51
+ require 'aws-sdk-core/ecs_credentials'
52
+ ::Aws::ECSCredentials.new
53
+ end
54
+ end
55
+
56
+ def ec2_credentials
57
+ RrxConfig.logger.info 'Using EC2 credentials'
58
+ require 'aws-sdk-core/instance_profile_credentials'
59
+ ::Aws::InstanceProfileCredentials.new
60
+ end
61
+ end
62
+ end
63
+ end
@@ -25,15 +25,21 @@ module RrxConfig
25
25
 
26
26
  def sources
27
27
  [
28
- Sources::LocalSource,
28
+ Sources::EnvironmentSource,
29
29
  Sources::AwsSecretSource,
30
- Sources::EnvironmentSource
30
+ Sources::LocalSource
31
31
  ]
32
32
  end
33
33
 
34
34
  def default_config
35
35
  Data.define.new
36
36
  end
37
+
38
+ class << self
39
+ def hash_data(hash)
40
+ Data.define(*hash.keys).new(**hash.transform_values { |v| v.is_a?(Hash) ? hash_data(v) : v })
41
+ end
42
+ end
37
43
  end
38
44
 
39
45
  def self.respond_to_missing?(...)
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/database_configurations'
4
+
5
+ module RrxConfig
6
+ module DatabaseConfig
7
+ class IamHashConfig < ActiveRecord::DatabaseConfigurations::HashConfig
8
+ GLOBAL_PEM_URL = 'https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem'
9
+ PASSWORD_EXPIRATION = 10.minutes
10
+
11
+ alias raw_configuration_hash configuration_hash
12
+
13
+ # @param [Hash] configuration_hash
14
+ def initialize(env_name, name, configuration_hash)
15
+ config = configuration_hash.except(:iam)
16
+ case config[:adapter]
17
+ when 'mysql2'
18
+ config[:enable_cleartext_plugin] = true
19
+ end
20
+ super(env_name, name, config)
21
+ end
22
+
23
+ def configuration_hash
24
+ { password:, sslca:, ssl_mode: :required }.reverse_merge!(raw_configuration_hash).freeze.tap do |it|
25
+ if RrxConfig.logger.respond_to?(:with_tags)
26
+ RrxConfig.logger.with_tags(**it) { RrxConfig.debug 'Generated IAM DB config' }
27
+ else
28
+ RrxConfig.debug "Generated IAM DB config: #{JSON(it)}"
29
+ end
30
+ end
31
+ end
32
+
33
+ def password
34
+ if password_expired?
35
+ @password = generate_password
36
+ @password_expiration = PASSWORD_EXPIRATION.from_now
37
+ end
38
+ @password
39
+ end
40
+
41
+ def password_expired?
42
+ !(@password && @password_expiration && (@password_expiration > Time.now))
43
+ end
44
+
45
+ def endpoint
46
+ "#{raw_configuration_hash[:host]}:#{raw_configuration_hash[:port]}"
47
+ end
48
+
49
+ def region
50
+ raw_configuration_hash.fetch(:region, Aws.region)
51
+ end
52
+
53
+ def user_name
54
+ raw_configuration_hash[:username] || raw_configuration_hash[:user]
55
+ end
56
+
57
+ def generate_password
58
+ generator.auth_token(endpoint:, region:, user_name:)
59
+ end
60
+
61
+ def generator
62
+ require 'aws-sdk-rds'
63
+ require_relative '../aws'
64
+ @generator ||= ::Aws::RDS::AuthTokenGenerator.new(credentials: Aws.credentials)
65
+ end
66
+
67
+ def sslca
68
+ sslca_download unless sslca_path.exist?
69
+ sslca_path.to_s
70
+ end
71
+
72
+ def sslca_path
73
+ @sslca_path ||= Rails.root.join('tmp/aws-rds-ca.pem')
74
+ end
75
+
76
+ def sslca_download
77
+ require 'open-uri'
78
+ download = URI.open(GLOBAL_PEM_URL)
79
+ sslca_path.truncate(0) if sslca_path.exist?
80
+ IO.copy_stream download, sslca_path
81
+
82
+ RrxConfig.info "Downloaded AWS certs to #{sslca_path}"
83
+ end
84
+ end
85
+
86
+ end
87
+ end
@@ -1,14 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if defined?(ActiveRecord)
4
- require 'active_record/database_configurations'
5
- ActiveRecord::DatabaseConfigurations.register_db_config_handler do |env_name, name, url, config|
6
- if url
7
- ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, name, url, config)
8
- elsif RrxConfig.database?
9
- ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, name, RrxConfig.database.to_h)
10
- else
11
- ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, name, config)
3
+ require_relative './database_config/iam_hash_config'
4
+
5
+ module RrxConfig
6
+ module DatabaseConfig
7
+ class << self
8
+ def db_config_handler(env_name, name, url, config)
9
+ case
10
+ when url
11
+ # Pass to default handler
12
+ nil
13
+ when RrxConfig.database?
14
+ # Use config from RrxConfig
15
+ if RrxConfig.database.try(:iam)
16
+ config = RrxConfig.database.to_h
17
+ RrxConfig.info "Using AWS IAM config for #{obfuscate(config)}"
18
+ IamHashConfig.new(env_name, name, config)
19
+ else
20
+ ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, name, RrxConfig.database.to_h)
21
+ end
22
+ when config.fetch(:iam, false)
23
+ # Use standard config with IAM support
24
+ IamHashConfig.new(env_name, name, config)
25
+ end
26
+ end
27
+
28
+ protected
29
+
30
+ def obfuscate(config)
31
+ if config.include?(:password)
32
+ # @type {String}
33
+ password = config[:password]
34
+ config[:password] = "#{password.length > 1 ? password[0..1] : ''}*********"
35
+ end
36
+ config
37
+ end
12
38
  end
13
39
  end
14
40
  end
41
+
42
+ ActiveRecord::DatabaseConfigurations.register_db_config_handler do |env_name, name, url, config|
43
+ RrxConfig::DatabaseConfig.db_config_handler env_name, name, url, config
44
+ end
@@ -1,6 +1,13 @@
1
1
  # frozen_string_literal: true
2
+ require_relative './error'
2
3
 
3
4
  module RrxConfig
5
+ class EnvironmentError < Error
6
+ def initialize(msg)
7
+ super("Invalid environment '#{msg}'")
8
+ end
9
+ end
10
+
4
11
  class Environment < ActiveSupport::StringInquirer
5
12
  RRX_ENVIRONMENT_VARIABLE = 'RRX_ENVIRONMENT'
6
13
  RRX_ENVIRONMENT_DEFAULT = 'development'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RrxConfig
4
+ class Error < StandardError; end
5
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+ require 'active_support'
5
+
6
+ module RrxConfig
7
+ class Railtie < Rails::Railtie
8
+ initializer 'rrx_config.initialize_database', before: 'active_record.initialize_database' do
9
+ ActiveSupport.on_load(:active_record) do
10
+ # Make sure our config handler is registered before Rails initializes
11
+ require_relative './database_config'
12
+ end
13
+ end
14
+
15
+ rake_tasks do
16
+ namespace :db do
17
+ task rrx_init_config: :environment do
18
+ # Make sure our config handler is registered before the Rails rake task runs
19
+ require_relative './database_config'
20
+ end
21
+
22
+ task load_config: 'db:rrx_init_config'
23
+
24
+ task print_config: 'db:load_config' do
25
+ puts JSON.pretty_generate(ActiveRecord::Base.connection_db_config.configuration_hash)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative './base'
4
+ require_relative '../aws'
5
+ require_relative '../error'
4
6
 
5
7
  module RrxConfig
6
8
  module Sources
7
9
  class AwsSecretSource < Base
8
10
  SECRET_VARIABLE = 'RRX_AWS_CONFIG_SECRET_NAME'
9
- PROFILE_VARIABLE = 'RRX_AWS_PROFILE'
10
- REGION_VARIABLE = 'RRX_AWS_REGION'
11
- REGION_DEFAULT = 'us-west-2'
11
+
12
+ class GetAwsSecretError < Error; end
12
13
 
13
14
  def read
14
15
  read_secret if secret_id
@@ -19,14 +20,14 @@ module RrxConfig
19
20
  def write(value)
20
21
  raise NotImplementedError unless Rails.env.test?
21
22
 
22
- puts "Writing secret #{secret_id}"
23
+ RrxConfig.info "Writing secret #{secret_id}"
23
24
  result = client.create_secret({
24
25
  name: secret_id,
25
26
  secret_string: value,
26
27
  force_overwrite_replica_secret: true,
27
28
  description: 'Integration test'
28
29
  })
29
- puts "Secret created: #{result.arn}"
30
+ RrxConfig.info "Secret created: #{result.arn}"
30
31
  end
31
32
 
32
33
  ##
@@ -34,7 +35,7 @@ module RrxConfig
34
35
  def delete
35
36
  raise NotImplementedError unless Rails.env.test?
36
37
 
37
- puts "Deleting secret #{secret_id}"
38
+ RrxConfig.info "Deleting secret #{secret_id}"
38
39
  client.delete_secret({ secret_id:, force_delete_without_recovery: true }) rescue nil
39
40
  end
40
41
 
@@ -45,25 +46,20 @@ module RrxConfig
45
46
  @secret_name == :- ? nil : @secret_name
46
47
  end
47
48
 
48
- def credentials_profile
49
- profile = Rails.env.production? ? nil : ENV.fetch(PROFILE_VARIABLE, nil)
50
- profile || 'default'
51
- end
52
-
53
49
  ##
54
50
  # @return [Aws::SecretsManager::Client]
55
51
  def client
56
52
  @client ||= begin
57
53
  require 'aws-sdk-secretsmanager'
58
- Aws::SecretsManager::Client.new(
59
- region: ENV.fetch(REGION_VARIABLE, REGION_DEFAULT),
60
- profile: credentials_profile
61
- )
54
+ ::Aws::SecretsManager::Client.new(**Aws.client_args)
62
55
  end
63
56
  end
64
57
 
65
58
  def read_secret
66
59
  read_json client.get_secret_value({secret_id:}).secret_string
60
+ rescue x
61
+ RrxConfig.error "Failed to read AWS secret #{secret_id}: #{x}"
62
+ raise GetAwsSecretError, "Failed to read AWS secret #{secret_id}", x.backtrace
67
63
  end
68
64
  end
69
65
  end
@@ -12,16 +12,21 @@ module RrxConfig
12
12
  # @param [String, nil] value
13
13
  # @return [Data, nil]
14
14
  def read_json(value)
15
- value ? json_to_data(JSON.parse(value, { symbolize_keys: true })) : nil
15
+ if value
16
+ result = json_to_data(JSON.parse(value, { symbolize_keys: true }))
17
+ RrxConfig.info 'Successfully read config from %s (%s)' % [
18
+ self.class.name.split('::').last.sub('Source', ''),
19
+ result.members.join(', ')
20
+ ]
21
+ result
22
+ else
23
+ nil
24
+ end
16
25
  end
17
26
 
18
27
  # @param [Hash] json
19
28
  def json_to_data(json)
20
- json = json.transform_values do |v|
21
- v.is_a?(Hash) ? json_to_data(v) : v
22
- end
23
-
24
- Data.define(*json.keys).new(**json)
29
+ Configuration.hash_data(json)
25
30
  end
26
31
  end
27
32
  end
@@ -7,7 +7,6 @@ module RrxConfig
7
7
  class EnvironmentSource < Base
8
8
  VARIABLE_NAME = 'RRX_CONFIG'
9
9
 
10
- # @return [Struct, nil]
11
10
  def read
12
11
  read_json ENV.fetch(VARIABLE_NAME, nil)
13
12
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RrxConfig
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/rrx_config.rb CHANGED
@@ -3,14 +3,39 @@
3
3
  require_relative 'rrx_config/version'
4
4
  require_relative 'rrx_config/configuration'
5
5
  require_relative 'rrx_config/environment'
6
- require_relative 'rrx_config/database_config'
6
+ require_relative 'rrx_config/railtie'
7
7
 
8
8
  module RrxConfig
9
- class Error < StandardError; end
9
+ class << self
10
+ def logger
11
+ if defined?(Rails) && Rails.logger
12
+ if Rails.logger.respond_to?(:scoped)
13
+ Rails.logger.scoped(name: 'rrx_config')
14
+ else
15
+ Rails.logger
16
+ end
17
+ end
18
+ end
19
+
20
+ def log(level, msg)
21
+ logger = self.logger
22
+ if logger
23
+ logger.send(level.to_sym, msg)
24
+ else
25
+ puts "[RRX_CONFIG][#{level.to_s.upcase}] #{msg}"
26
+ end
27
+ end
28
+
29
+ def debug(msg)
30
+ log(:debug, msg)
31
+ end
32
+
33
+ def info(msg)
34
+ log(:info, msg)
35
+ end
10
36
 
11
- class EnvironmentError < Error
12
- def initialize(msg)
13
- super("Invalid environment '#{msg}'")
37
+ def error(msg)
38
+ log(:error, msg)
14
39
  end
15
40
  end
16
41
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rrx_config
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Drew
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-01-10 00:00:00.000000000 Z
11
+ date: 2024-02-02 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: railties
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -24,6 +38,76 @@ dependencies:
24
38
  - - ">="
25
39
  - !ruby/object:Gem::Version
26
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: aws-sdk-rds
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: aws-sdk-secretsmanager
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: mysql2
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rrx_dev
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: sqlite3
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
27
111
  description:
28
112
  email:
29
113
  - dan.drew@hotmail.com
@@ -32,11 +116,6 @@ extensions: []
32
116
  extra_rdoc_files: []
33
117
  files:
34
118
  - ".editorconfig"
35
- - ".idea/.gitignore"
36
- - ".idea/inspectionProfiles/Project_Default.xml"
37
- - ".idea/modules.xml"
38
- - ".idea/rrx_config.iml"
39
- - ".idea/vcs.xml"
40
119
  - ".rspec"
41
120
  - ".rubocop.yml"
42
121
  - CODE_OF_CONDUCT.md
@@ -45,10 +124,15 @@ files:
45
124
  - LICENSE.txt
46
125
  - README.md
47
126
  - Rakefile
127
+ - global.pem
48
128
  - lib/rrx_config.rb
129
+ - lib/rrx_config/aws.rb
49
130
  - lib/rrx_config/configuration.rb
50
131
  - lib/rrx_config/database_config.rb
132
+ - lib/rrx_config/database_config/iam_hash_config.rb
51
133
  - lib/rrx_config/environment.rb
134
+ - lib/rrx_config/error.rb
135
+ - lib/rrx_config/railtie.rb
52
136
  - lib/rrx_config/sources/aws_secret_source.rb
53
137
  - lib/rrx_config/sources/base.rb
54
138
  - lib/rrx_config/sources/environment_source.rb
data/.idea/.gitignore DELETED
@@ -1,8 +0,0 @@
1
- # Default ignored files
2
- /shelf/
3
- /workspace.xml
4
- # Editor-based HTTP Client requests
5
- /httpRequests/
6
- # Datasource local storage ignored files
7
- /dataSources/
8
- /dataSources.local.xml
@@ -1,7 +0,0 @@
1
- <component name="InspectionProjectProfileManager">
2
- <profile version="1.0">
3
- <option name="myName" value="Project Default" />
4
- <inspection_tool class="RbsMissingTypeSignature" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
5
- <inspection_tool class="RubyMismatchedParameterType" enabled="false" level="WARNING" enabled_by_default="false" />
6
- </profile>
7
- </component>
data/.idea/modules.xml DELETED
@@ -1,8 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="ProjectModuleManager">
4
- <modules>
5
- <module fileurl="file://$PROJECT_DIR$/.idea/rrx_config.iml" filepath="$PROJECT_DIR$/.idea/rrx_config.iml" />
6
- </modules>
7
- </component>
8
- </project>