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.
- checksums.yaml +4 -4
- data/{LICENSE.txt → LICENSE} +0 -0
- data/README.md +718 -127
- data/bin/chamber +277 -0
- data/lib/chamber.rb +63 -65
- data/lib/chamber/file.rb +84 -0
- data/lib/chamber/file_set.rb +265 -0
- data/lib/chamber/namespace_set.rb +141 -0
- data/lib/chamber/rails.rb +3 -0
- data/lib/chamber/rails/railtie.rb +11 -0
- data/lib/chamber/settings.rb +152 -0
- data/lib/chamber/system_environment.rb +145 -0
- data/lib/chamber/version.rb +2 -2
- data/spec/lib/chamber/file_set_spec.rb +212 -0
- data/spec/lib/chamber/file_spec.rb +94 -0
- data/spec/lib/chamber/namespace_set_spec.rb +81 -0
- data/spec/lib/chamber/settings_spec.rb +135 -0
- data/spec/lib/chamber/system_environment_spec.rb +121 -0
- data/spec/lib/chamber_spec.rb +279 -243
- metadata +58 -59
- data/.gitignore +0 -17
- data/.ruby-gemset +0 -1
- data/.ruby-version +0 -1
- data/.travis.yml +0 -5
- data/Gemfile +0 -4
- data/Rakefile +0 -6
- data/chamber.gemspec +0 -36
- data/spec/spec_helper.rb +0 -12
data/bin/chamber
ADDED
@@ -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
|
data/lib/chamber.rb
CHANGED
@@ -1,90 +1,88 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
require '
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
30
|
-
|
31
|
-
load!
|
39
|
+
def filenames
|
40
|
+
self.files.filenames
|
32
41
|
end
|
33
42
|
|
34
|
-
def
|
35
|
-
|
43
|
+
def namespaces
|
44
|
+
settings.namespaces
|
36
45
|
end
|
37
46
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
47
|
+
def settings
|
48
|
+
@settings ||= -> do
|
49
|
+
@settings = Settings.new
|
41
50
|
|
42
|
-
|
51
|
+
files.to_settings do |parsed_settings|
|
52
|
+
@settings.merge! parsed_settings
|
53
|
+
end
|
43
54
|
|
44
|
-
|
55
|
+
@settings
|
56
|
+
end.call
|
57
|
+
end
|
45
58
|
|
46
|
-
def
|
47
|
-
|
59
|
+
def method_missing(name, *args)
|
60
|
+
if settings.respond_to?(name)
|
61
|
+
return settings.public_send(name, *args)
|
62
|
+
end
|
48
63
|
|
49
|
-
|
64
|
+
super
|
50
65
|
end
|
51
66
|
|
52
|
-
def
|
53
|
-
|
67
|
+
def respond_to_missing?(name, include_private = false)
|
68
|
+
settings.respond_to?(name, include_private)
|
54
69
|
end
|
55
70
|
|
56
|
-
def
|
57
|
-
|
71
|
+
def to_s(*args)
|
72
|
+
settings.to_s(*args)
|
58
73
|
end
|
59
74
|
|
60
|
-
|
61
|
-
return unless File.exists?(filename)
|
75
|
+
protected
|
62
76
|
|
63
|
-
|
64
|
-
if options[:override_from_environment]
|
65
|
-
override_from_environment!(hash)
|
66
|
-
end
|
77
|
+
attr_writer :settings
|
67
78
|
|
68
|
-
|
79
|
+
def files
|
80
|
+
@files ||= FileSet.new files: []
|
69
81
|
end
|
70
82
|
|
71
|
-
|
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
|
-
|
77
|
-
|
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
|
data/lib/chamber/file.rb
ADDED
@@ -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
|