chamber 0.0.4 → 1.0.0

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