chamber 0.0.4 → 1.0.0

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,277 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pp'
4
+ require 'thor'
5
+ require 'bundler'
6
+ require 'chamber'
7
+ require 'yaml'
8
+ require 'tempfile'
9
+
10
+ class Chamber
11
+ module Commands
12
+ class Heroku < Thor
13
+ class_option :app,
14
+ type: :string,
15
+ aliases: '-a',
16
+ desc: 'The name of the Heroku application whose config values will be affected'
17
+
18
+ desc 'clear', 'Removes all Heroku environment variables which match settings that Chamber knows about'
19
+ method_option :dry_run,
20
+ type: :boolean,
21
+ aliases: '-d',
22
+ desc: 'Does not actually remove anything, but instead displays what would change if cleared'
23
+ def clear
24
+ ::Chamber::Commands::Chamber.init(options)
25
+
26
+ keys = ::Chamber.to_environment.keys
27
+
28
+ if options[:dry_run]
29
+ puts "Running without --dry-run will remove the following Heroku environment variables"
30
+
31
+ keys_regex = keys.join('|')
32
+ current_heroku_config = heroku("config --shell")
33
+
34
+ current_heroku_config.each_line do |line|
35
+ puts line if line.match(keys_regex)
36
+ end
37
+ else
38
+ puts heroku("config:unset #{keys.join(' ')}")
39
+ end
40
+ end
41
+
42
+ desc 'push', 'Sends settings to Heroku so that they may be used in the application once it is deployed'
43
+ method_option :dry_run,
44
+ type: :boolean,
45
+ aliases: '-d',
46
+ desc: 'Does not actually push anything to Heroku, but instead displays what would change if pushed'
47
+ method_option :only_ignored,
48
+ type: :boolean,
49
+ aliases: '-o',
50
+ default: true,
51
+ desc: 'Does not push settings up to Heroku unless the file that the setting is contained in has been gitignored'
52
+ def push
53
+ ::Chamber::Commands::Chamber.init(options)
54
+
55
+ if options[:only_ignored]
56
+ ignored_settings_files = `git ls-files --other --ignored --exclude-from=.gitignore | sed -e "s|^|${PWD}/|" | grep --colour=never -E '#{::Chamber.filenames.join('|')}'`
57
+
58
+ ::Chamber.load files: ignored_settings_files.split("\n"),
59
+ namespaces: options[:namespaces]
60
+ end
61
+
62
+ if options[:dry_run]
63
+ puts "Running without --dry-run will set the following Heroku environment variables"
64
+
65
+ puts ::Chamber.to_s(pair_separator: "\n")
66
+ else
67
+ heroku("config:set #{::Chamber.to_s}")
68
+ end
69
+ end
70
+
71
+ desc 'pull', 'Retrieves the environment variables for the application and stores them in a temporary file'
72
+ method_option :into,
73
+ type: :string,
74
+ desc: 'The file into which the Heroku config information should be stored. This file WILL BE OVERRIDDEN.'
75
+ def pull
76
+ current_heroku_config = heroku("config --shell")
77
+
78
+ if options[:into]
79
+ ::File.open(options[:into], 'w+') do |file|
80
+ file.write current_heroku_config
81
+ end
82
+ end
83
+ end
84
+
85
+ desc 'diff', 'Displays the difference between what is currently stored in the Heroku application\'s config and what Chamber knows about locally'
86
+ def diff
87
+ ::Chamber::Commands::Chamber.init(options)
88
+
89
+ current_heroku_config = heroku("config --shell")
90
+ current_local_config = ::Chamber.to_s(pair_separator: "\n",
91
+ value_surrounder: '')
92
+
93
+ heroku_config_file = Tempfile.open('heroku') do |file|
94
+ file.write current_heroku_config.chomp
95
+ file.to_path
96
+ end
97
+ local_config_file = Tempfile.open('local') do |file|
98
+ file.write current_local_config
99
+ file.to_path
100
+ end
101
+
102
+ system("git diff --no-index #{local_config_file} #{heroku_config_file}")
103
+ end
104
+
105
+ no_commands do
106
+ def app
107
+ options[:app] ? " --app #{options[:app]}" : ""
108
+ end
109
+
110
+ def heroku(command)
111
+ Bundler.with_clean_env { `heroku #{command}#{app}` }
112
+ end
113
+ end
114
+ end
115
+
116
+ class Travis < Thor
117
+ desc 'secure', 'Uses your Travis CI public key to encrypt the settings you have chosen not to commit to the repo'
118
+ method_option :dry_run,
119
+ type: :boolean,
120
+ aliases: '-d',
121
+ desc: 'Does not actually encrypt anything to .travis.yml, but instead displays what values would be encrypted'
122
+ method_option :only_ignored,
123
+ type: :boolean,
124
+ aliases: '-o',
125
+ default: true,
126
+ desc: 'Does not encrpyt settings into .travis.yml unless the file that the setting is contained in has been gitignored'
127
+ def secure
128
+ ::Chamber::Commands::Chamber.init(options)
129
+
130
+ if options[:only_ignored]
131
+ ignored_settings_files = `git ls-files --other --ignored --exclude-from=.gitignore | sed -e "s|^|${PWD}/|" | grep --colour=never -E '#{::Chamber.filenames.join('|')}'`
132
+
133
+ ::Chamber.load files: ignored_settings_files.split("\n"),
134
+ namespaces: options[:namespaces]
135
+ end
136
+
137
+ setting_values = ::Chamber.to_s(pair_separator: "\n")
138
+
139
+ if options[:dry_run]
140
+ puts "Running without --dry-run will encrypt the following Travis CI environment variables"
141
+
142
+ puts setting_values
143
+ else
144
+ travis_encrypt('--override ryterutoiuewrtoiuweorit="invalid"')
145
+
146
+ setting_values.each_line do |line|
147
+ puts "Encrypting: #{line}"
148
+ travis_encrypt("--append #{line}")
149
+ end
150
+ end
151
+ end
152
+
153
+ no_commands do
154
+ def travis_encrypt(command)
155
+ Bundler.with_clean_env { `travis encrypt --add 'env.global' #{command}` }
156
+ end
157
+ end
158
+ end
159
+
160
+ class Settings < Thor
161
+ desc 'show', 'Displays the list of settings and their values'
162
+ method_option :as_env,
163
+ type: :boolean,
164
+ aliases: '-e',
165
+ desc: 'Whether the displayed settings should be environment variable compatible'
166
+ def show
167
+ ::Chamber::Commands::Chamber.init(options)
168
+
169
+ if options[:as_env]
170
+ puts ::Chamber.to_s(pair_separator: "\n")
171
+ else
172
+ pp ::Chamber.to_hash
173
+ end
174
+ end
175
+
176
+ desc 'files', 'Lists the settings files which are parsed with the given options'
177
+ def files
178
+ ::Chamber::Commands::Chamber.init(options)
179
+
180
+ pp ::Chamber.filenames
181
+ end
182
+
183
+ desc 'compare', 'Displays the difference between what is currently stored in the Heroku application\'s config and what Chamber knows about locally'
184
+ method_option :keys_only,
185
+ type: :boolean,
186
+ default: true,
187
+ desc: 'Whether or not to only compare the keys but not the values of the two sets of settings'
188
+ method_option :first,
189
+ type: :array,
190
+ desc: 'The list of namespaces which will be used as the source of the comparison'
191
+ method_option :second,
192
+ type: :array,
193
+ desc: 'The list of namespaces which will be used as the destination of the comparison'
194
+ def compare
195
+ ::Chamber::Commands::Chamber.init(options)
196
+
197
+ ::Chamber.load basepath: options[:basepath],
198
+ files: options[:files],
199
+ namespaces: options[:first]
200
+
201
+ first_config = if options[:keys_only]
202
+ ::Chamber.to_environment.keys.join("\n")
203
+ else
204
+ ::Chamber.to_s(pair_separator: "\n")
205
+ end
206
+
207
+ ::Chamber.load basepath: options[:basepath],
208
+ files: options[:files],
209
+ namespaces: options[:second]
210
+
211
+ second_config = if options[:keys_only]
212
+ ::Chamber.to_environment.keys.join("\n")
213
+ else
214
+ ::Chamber.to_s(pair_separator: "\n")
215
+ end
216
+
217
+ first_config_file = Tempfile.open('first') do |file|
218
+ file.write first_config
219
+ file.to_path
220
+ end
221
+ second_config_file = Tempfile.open('second') do |file|
222
+ file.write second_config
223
+ file.to_path
224
+ end
225
+
226
+ system("git diff --no-index #{first_config_file} #{second_config_file}")
227
+ end
228
+ end
229
+
230
+ class Chamber < Thor
231
+ class_option :basepath,
232
+ type: :string,
233
+ aliases: '-b',
234
+ desc: 'The base filepath where Chamber will look for the conventional settings files'
235
+ class_option :files,
236
+ type: :array,
237
+ aliases: '-f',
238
+ desc: 'The set of file globs that Chamber will use for processing'
239
+ class_option :namespaces,
240
+ type: :array,
241
+ aliases: '-n',
242
+ default: [],
243
+ desc: 'The set of namespaces that Chamber will use for processing'
244
+ class_option :preset,
245
+ type: :string,
246
+ aliases: '-p',
247
+ enum: ['rails'],
248
+ desc: 'Used to quickly assign a given scenario to the chamber command (eg Rails apps)'
249
+
250
+ desc 'settings SUBCOMMAND ...ARGS', 'For working with Chamber settings'
251
+ subcommand 'settings', ::Chamber::Commands::Settings
252
+
253
+ desc 'travis SUBCOMMAND ...ARGS', 'For manipulating Travis CI environment variables'
254
+ subcommand 'travis', ::Chamber::Commands::Travis
255
+
256
+ desc 'heroku SUBCOMMAND ...ARGS', 'For manipulating Heroku environment variables'
257
+ subcommand 'heroku', ::Chamber::Commands::Heroku
258
+
259
+ no_commands do
260
+ def self.init(options)
261
+ if options[:preset] == 'rails'
262
+ require ::File.expand_path('./config/environment')
263
+
264
+ options[:basepath] = Rails.root.join('config')
265
+ options[:namespaces] = options[:namespaces].empty? ? [Rails.env] : options[:namespaces]
266
+ end
267
+
268
+ ::Chamber.load basepath: options[:basepath],
269
+ files: options[:files],
270
+ namespaces: options[:namespaces]
271
+ end
272
+ end
273
+ end
274
+ end
275
+ end
276
+
277
+ Chamber::Commands::Chamber.start
@@ -1,90 +1,88 @@
1
- require 'erb'
2
- require 'hashie'
3
- require 'yaml'
4
-
5
- require 'chamber/version'
6
-
7
- module Chamber
8
- class ChamberInvalidOptionError < ArgumentError; end
9
-
10
- def source(filename, options={})
11
- assert_valid_keys(options)
12
-
13
- add_source(filename, options)
1
+ require 'singleton'
2
+ require 'forwardable'
3
+ require 'chamber/file_set'
4
+ require 'chamber/rails'
5
+
6
+ class Chamber
7
+ include Singleton
8
+
9
+ class << self
10
+ extend Forwardable
11
+
12
+ def_delegators :instance, :[],
13
+ :basepath,
14
+ :load,
15
+ :filenames,
16
+ :namespaces,
17
+ :settings,
18
+ :to_environment,
19
+ :to_hash,
20
+ :to_s
21
+
22
+ alias_method :env, :instance
14
23
  end
15
24
 
16
- def load!
17
- sources.each do |source|
18
- filename, options = source
19
-
20
- load_source!(filename, options)
21
- end
22
- end
23
-
24
- def clear!
25
- @chamber_instance = nil
26
- @chamber_sources = nil
25
+ attr_accessor :basepath,
26
+ :files
27
+
28
+ def load(options)
29
+ self.settings = nil
30
+ self.basepath = options[:basepath] || ''
31
+ file_patterns = options[:files] || [
32
+ self.basepath + 'credentials*.yml',
33
+ self.basepath + 'settings*.yml',
34
+ self.basepath + 'settings' ]
35
+ self.files = FileSet.new files: file_patterns,
36
+ namespaces: options.fetch(:namespaces, {})
27
37
  end
28
38
 
29
- def reload!
30
- @chamber_instance = nil
31
- load!
39
+ def filenames
40
+ self.files.filenames
32
41
  end
33
42
 
34
- def instance
35
- @chamber_instance ||= Hashie::Mash.new
43
+ def namespaces
44
+ settings.namespaces
36
45
  end
37
46
 
38
- def method_missing(method_name, *args, &block)
39
- instance.public_send(method_name, *args, &block)
40
- end
47
+ def settings
48
+ @settings ||= -> do
49
+ @settings = Settings.new
41
50
 
42
- alias_method :env, :instance
51
+ files.to_settings do |parsed_settings|
52
+ @settings.merge! parsed_settings
53
+ end
43
54
 
44
- private
55
+ @settings
56
+ end.call
57
+ end
45
58
 
46
- def assert_valid_keys(options)
47
- unknown_keys = options.keys - [:namespace, :override_from_environment]
59
+ def method_missing(name, *args)
60
+ if settings.respond_to?(name)
61
+ return settings.public_send(name, *args)
62
+ end
48
63
 
49
- raise(ChamberInvalidOptionError, options) unless unknown_keys.empty?
64
+ super
50
65
  end
51
66
 
52
- def sources
53
- @chamber_sources ||= []
67
+ def respond_to_missing?(name, include_private = false)
68
+ settings.respond_to?(name, include_private)
54
69
  end
55
70
 
56
- def add_source(filename, options)
57
- sources << [filename, options]
71
+ def to_s(*args)
72
+ settings.to_s(*args)
58
73
  end
59
74
 
60
- def load_source!(filename, options)
61
- return unless File.exists?(filename)
75
+ protected
62
76
 
63
- hash = hash_from_source(filename, options[:namespace])
64
- if options[:override_from_environment]
65
- override_from_environment!(hash)
66
- end
77
+ attr_writer :settings
67
78
 
68
- instance.deep_merge!(hash)
79
+ def files
80
+ @files ||= FileSet.new files: []
69
81
  end
70
82
 
71
- def hash_from_source(filename, namespace)
72
- contents = open(filename).read
73
- hash = YAML.load(ERB.new(contents).result).to_hash || {}
74
- hash = Hashie::Mash.new(hash)
83
+ private
75
84
 
76
- namespace ? hash.fetch(namespace) : hash
77
- end
78
-
79
- def override_from_environment!(hash)
80
- hash.each_pair do |key, value|
81
- next unless value.is_a?(Hash)
82
-
83
- if value.environment
84
- hash[key] = ENV[value.environment]
85
- else
86
- override_from_environment!(value)
87
- end
88
- end
85
+ def basepath=(pathlike)
86
+ @basepath = pathlike == '' ? '' : Pathname.new(::File.expand_path(pathlike))
89
87
  end
90
88
  end
@@ -0,0 +1,84 @@
1
+ require 'pathname'
2
+ require 'yaml'
3
+ require 'erb'
4
+
5
+ ###
6
+ # Internal: Represents a single file containing settings information in a given
7
+ # file set.
8
+ #
9
+ class Chamber
10
+ class File < Pathname
11
+
12
+ ###
13
+ # Internal: Creates a settings file representing a path to a file on the
14
+ # filesystem.
15
+ #
16
+ # Optionally, namespaces may be passed in which will be passed to the Settings
17
+ # object for consideration of which data will be parsed (see the Settings
18
+ # object's documentation for details on how this works).
19
+ #
20
+ # Examples:
21
+ #
22
+ # ###
23
+ # # It can be created by passing namespaces
24
+ # #
25
+ # settings_file = Chamber::File.new path: '/tmp/settings.yml',
26
+ # namespaces: {
27
+ # environment: ENV['RAILS_ENV'] }
28
+ # # => <Chamber::File>
29
+ #
30
+ # settings_file.to_settings
31
+ # # => <Chamber::Settings>
32
+ #
33
+ # ###
34
+ # # It can also be created without passing any namespaces
35
+ # #
36
+ # Chamber::File.new path: '/tmp/settings.yml'
37
+ # # => <Chamber::File>
38
+ #
39
+ def initialize(options = {})
40
+ self.namespaces = options[:namespaces] || {}
41
+
42
+ super options.fetch(:path)
43
+ end
44
+
45
+ ###
46
+ # Internal: Extracts the data from the file on disk. First passing it through
47
+ # ERB to generate any dynamic properties and then passing the resulting data
48
+ # through YAML.
49
+ #
50
+ # Therefore if a settings file contains something like:
51
+ #
52
+ # ```erb
53
+ # test:
54
+ # my_dynamic_value: <%= 1 + 1 %>
55
+ # ```
56
+ #
57
+ # then the resulting settings object would have:
58
+ #
59
+ # ```ruby
60
+ # settings[:test][:my_dynamic_value]
61
+ # # => 2
62
+ # ```
63
+ #
64
+ def to_settings
65
+ @data ||= Settings.new(settings: file_contents_hash,
66
+ namespaces: namespaces)
67
+ end
68
+
69
+ protected
70
+
71
+ attr_accessor :namespaces
72
+
73
+ private
74
+
75
+ def file_contents_hash
76
+ file_contents = self.read
77
+ erb_result = ERB.new(file_contents).result
78
+
79
+ YAML.load(erb_result) || {}
80
+ rescue Errno::ENOENT
81
+ {}
82
+ end
83
+ end
84
+ end