rrx_config 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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>