senv 0.4.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,11 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # Figure out where this script is located.
5
+ SELFDIR="`dirname \"$0\"`"
6
+ SELFDIR="`cd \"$SELFDIR\" && pwd`"
7
+
8
+ # Run the actual app using the bundled Ruby interpreter.
9
+ exec "$SELFDIR/lib/ruby/bin/ruby" "$SELFDIR/lib/app/senv.rb" "$@"
10
+
11
+ # Thanks @FooBarWidget! ref: https://github.com/phusion/traveling-ruby
@@ -0,0 +1,663 @@
1
+ #
2
+ require 'erb'
3
+ require 'yaml'
4
+ require 'json'
5
+ require 'rbconfig'
6
+ require 'pp'
7
+ require 'time'
8
+ require 'fileutils'
9
+ require 'pathname'
10
+ require 'thread'
11
+ require 'openssl'
12
+ require 'tmpdir'
13
+ require 'securerandom'
14
+
15
+ #
16
+ module Senv
17
+ #
18
+ VERSION = '0.4.2'.freeze
19
+
20
+ def Senv.version
21
+ VERSION
22
+ end
23
+
24
+ #
25
+ LICENSE = 'MIT'.freeze
26
+
27
+ def Senv.license
28
+ LICENSE
29
+ end
30
+
31
+ #
32
+ SUMMARY = ''
33
+
34
+ #
35
+ DEFAULT = 'development'.freeze
36
+
37
+ def Senv.default
38
+ DEFAULT
39
+ end
40
+
41
+ #
42
+ class Error < StandardError
43
+ end
44
+
45
+ def Senv.error!(*args, &block)
46
+ raise Error.new(*args, &block)
47
+ end
48
+
49
+ #
50
+ def Senv.env
51
+ ENV['SENV']
52
+ end
53
+
54
+ def Senv.env=(env)
55
+ if env
56
+ ENV['SENV'] = env.to_s.strip
57
+ else
58
+ ENV.delete('SENV')
59
+ end
60
+ end
61
+
62
+ #
63
+ def Senv.load(*args)
64
+ Senv.thread_safe do
65
+ #
66
+ env, options = Senv.parse_load_args(*args)
67
+
68
+ #
69
+ force = !!(options['force'] || options[:force])
70
+
71
+ #
72
+ a_parent_process_has_already_loaded_the_senv = (
73
+ ENV['SENV'] == env &&
74
+ ENV['SENV_LOADED'] &&
75
+ ENV['SENV_ENVIRONMENT']
76
+ )
77
+
78
+ if(a_parent_process_has_already_loaded_the_senv && !force)
79
+ Senv.env = env
80
+ Senv.loaded = JSON.parse(ENV['SENV_LOADED'])
81
+ Senv.environment = JSON.parse(ENV['SENV_ENVIRONMENT'])
82
+ return env
83
+ end
84
+
85
+ #
86
+ unless Senv.loading
87
+ loading = Senv.loading
88
+
89
+ begin
90
+ Senv.loading = env
91
+
92
+ Senv.loaded.clear
93
+ Senv.environment.clear
94
+
95
+ Senv.load_config_paths_for(env)
96
+
97
+ Senv.env = env
98
+
99
+ ENV['SENV'] = Senv.env
100
+ ENV['SENV_LOADED'] = JSON.generate(Senv.loaded)
101
+ ENV['SENV_ENVIRONMENT'] = JSON.generate(Senv.environment)
102
+
103
+ return env
104
+ ensure
105
+ Senv.loading = loading
106
+ end
107
+ else
108
+ a_config_file_imports_another_senv = (
109
+ Senv.loading != env
110
+ )
111
+
112
+ if a_config_file_imports_another_senv
113
+ Senv.load_config_paths_for(env)
114
+ return env
115
+ end
116
+
117
+ a_config_file_imports_itself_recursively = (
118
+ Senv.loading == env
119
+ )
120
+
121
+ if a_config_file_imports_itself_recursively
122
+ :cowardly_refuse_to_infinitely_recurse
123
+ return nil
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ def Senv.load!(*args)
130
+ env, options = Senv.parse_load_args(*args)
131
+ options['force'] = options[:force] = true
132
+ Senv.load(env, options)
133
+ end
134
+
135
+ def Senv.parse_load_args(*args)
136
+ env = Senv.env || Senv.default
137
+ options = Hash.new
138
+
139
+ case args.first
140
+ when String, Symbol
141
+ env = args.shift.to_s
142
+ end
143
+
144
+ case args.first
145
+ when Hash
146
+ options = args.shift
147
+ end
148
+
149
+ [env, options]
150
+ end
151
+
152
+ def Senv.thread_safe(&block)
153
+ THREAD_SAFE.synchronize(&block)
154
+ end
155
+ THREAD_SAFE = ::Monitor.new
156
+
157
+ def Senv.load_config_paths_for(env)
158
+ paths = Senv.config_paths_for(env)
159
+ Senv.load_config_paths(*paths)
160
+ end
161
+
162
+ def Senv.config_paths_for(env)
163
+ glob = "**/#{ env }.{rb,enc.rb}"
164
+
165
+ Senv.directory.glob(glob).sort_by do |path|
166
+ ext = path.basename.extname.split('.')
167
+ [path.basename.to_s.size, ext.size]
168
+ end
169
+ end
170
+
171
+ def Senv.load_config_paths(*paths)
172
+ libs = []
173
+ configs = []
174
+
175
+ paths.each do |path|
176
+ exts = path.extname.split('.')[1..-1]
177
+
178
+ case
179
+ when exts.include?('rb')
180
+ libs << path
181
+ else
182
+ configs << path
183
+ end
184
+ end
185
+
186
+ {
187
+ libs => :load_lib,
188
+ configs => :load_config,
189
+ }.each do |list, loader|
190
+ list.each do |path|
191
+ path = path.to_s
192
+
193
+ Senv.debug({'loading' => path})
194
+
195
+ if Senv.loaded.has_key?(path)
196
+ Senv.debug({'skipping' => path})
197
+ next
198
+ end
199
+
200
+ Senv.loaded[path] = nil
201
+
202
+ captured =
203
+ Senv.capturing_environment_changes do
204
+ Senv.send(loader, path)
205
+ end
206
+
207
+ changes = captured.changes
208
+
209
+ Senv.debug({path => changes})
210
+
211
+ Senv.loaded[path] = changes
212
+
213
+ captured.apply(Senv.environment)
214
+ end
215
+ end
216
+ end
217
+
218
+ #
219
+ def Senv.loading
220
+ @loading
221
+ end
222
+
223
+ def Senv.loading=(env)
224
+ @loading = env.to_s.strip
225
+ end
226
+
227
+ def Senv.loaded
228
+ @loaded ||= {}
229
+ end
230
+
231
+ def Senv.loaded=(hash)
232
+ @loaded = hash
233
+ end
234
+
235
+ def Senv.environment
236
+ @environment ||= {}
237
+ end
238
+
239
+ def Senv.environment=(hash)
240
+ @environment = hash
241
+ end
242
+
243
+ #
244
+ def Senv.realpath(path)
245
+ Pathname.new(path.to_s).realpath
246
+ end
247
+
248
+ def Senv.expand_path(path)
249
+ (realpath(path) rescue File.expand_path(path)).to_s
250
+ end
251
+
252
+ #
253
+ def Senv.root
254
+ determine_root! unless @root
255
+ @root
256
+ end
257
+
258
+ def Senv.root=(root)
259
+ @root = realpath(root)
260
+ end
261
+
262
+ def Senv.determine_root!
263
+ if ENV['SENV_ROOT']
264
+ Senv.root = ENV['SENV_ROOT']
265
+ return @root
266
+ else
267
+ Senv.search_path.each do |dirname|
268
+ if test(?d, dirname)
269
+ Senv.root = dirname
270
+ return @root
271
+ end
272
+ end
273
+ end
274
+
275
+ msg = "[SENV] no `.senv` directory found via `#{ Senv.search_path.join(' | ') }`"
276
+ Senv.error!(msg)
277
+ end
278
+
279
+ def Senv.directory
280
+ Senv.root.join('.senv')
281
+ end
282
+
283
+ def Senv.search_path
284
+ search_path = []
285
+
286
+ if ENV['SENV_PATH']
287
+ ENV['SENV_PATH'].split(':').each do |path|
288
+ search_path << Senv.expand_path(path).to_s
289
+ end
290
+ else
291
+ Pathname.pwd.realpath.ascend do |path|
292
+ search_path << path.to_s
293
+ end
294
+ end
295
+
296
+ search_path
297
+ end
298
+
299
+ def Senv.key_path
300
+ Senv.directory.join('_key')
301
+ end
302
+
303
+ def Senv.key
304
+ if ENV['SENV_KEY']
305
+ ENV['SENV_KEY']
306
+ else
307
+ if Senv.key_path.exist?
308
+ Senv.key_path.binread.strip
309
+ else
310
+ msg = "Senv.key not found in : #{ Senv.key_path }"
311
+ Senv.error!(msg)
312
+ end
313
+ end
314
+ end
315
+
316
+ def Senv.key=(key)
317
+ ENV['SENV_KEY'] = key.to_s.strip
318
+ end
319
+
320
+ def Senv.key_source
321
+ if ENV['SENV_KEY']
322
+ "ENV['SENV_KEY']"
323
+ else
324
+ Senv.key_path rescue '(no key source)'
325
+ end
326
+ end
327
+
328
+ #
329
+ ENCRYPTED_PATH_RE = Regexp.union(/\.enc(rypted)$/, /\.enc(rypted)?\./)
330
+
331
+ def Senv.is_encrypted?(path)
332
+ path.to_s =~ ENCRYPTED_PATH_RE
333
+ end
334
+
335
+ #
336
+ def Senv.binread(path, options = {})
337
+ data = IO.binread(path)
338
+
339
+ encrypted =
340
+ if options.has_key?('encrypted')
341
+ options['encrypted']
342
+ else
343
+ Senv.is_encrypted?(path)
344
+ end
345
+
346
+ if encrypted
347
+ data =
348
+ begin
349
+ Blowfish.decrypt(Senv.key, data)
350
+ rescue
351
+ abort "could not decrypt `#{ path }` with key `#{ Senv.key }` from `#{ Senv.key_source }`"
352
+ end
353
+ end
354
+
355
+ data
356
+ end
357
+
358
+ def Senv.read(path, options = {})
359
+ Senv.binread(path)
360
+ end
361
+
362
+ def Senv.binwrite(path, data, options = {})
363
+ encrypted =
364
+ if options.has_key?('encrypted')
365
+ options['encrypted']
366
+ else
367
+ Senv.is_encrypted?(path)
368
+ end
369
+
370
+ if encrypted
371
+ data =
372
+ begin
373
+ Blowfish.encrypt(Senv.key, data)
374
+ rescue
375
+ abort "could not encrypt `#{ data.to_s.split("\n").first }...` with key `#{ Senv.key }` from `#{ Senv.key_source }`"
376
+ end
377
+ end
378
+
379
+ IO.binwrite(path, data)
380
+ end
381
+
382
+ def Senv.write(path, data, options = {})
383
+ Senv.binwrite(path, data, options)
384
+ end
385
+
386
+ def Senv.load_lib(path)
387
+ #
388
+ code = Senv.binread(path.to_s)
389
+ binding = ::TOPLEVEL_BINDING
390
+ filename = path.to_s
391
+
392
+ #
393
+ Kernel.eval(code, binding, filename)
394
+ end
395
+
396
+ def Senv.load_config(path)
397
+ #
398
+ erb = Senv.binread(path)
399
+ expanded = ERB.new(erb).result(::TOPLEVEL_BINDING)
400
+ buf = expanded
401
+
402
+ #
403
+ encoded = buf
404
+
405
+ config =
406
+ case
407
+ when path =~ /yml|yaml/
408
+ YAML.load(encoded)
409
+ when path =~ /json/
410
+ JSON.parse(encoded)
411
+ else
412
+ abort "unknown config format in #{ path }"
413
+ end
414
+
415
+ #
416
+ unless config && config.is_a?(Hash)
417
+ abort "[SENV] failed to load #{ path }"
418
+ end
419
+
420
+ #
421
+ config.each do |key, val|
422
+ ENV[key.to_s] = val.to_s
423
+ end
424
+
425
+ #
426
+ config
427
+ end
428
+
429
+ #
430
+ def Senv.debug(*args, &block)
431
+ if args.empty? && block.nil?
432
+ return Senv.debug?
433
+ end
434
+
435
+ return nil unless Senv.debug?
436
+
437
+ lines = []
438
+
439
+ args.each do |arg|
440
+ case
441
+ when arg.is_a?(String)
442
+ lines << arg.strip
443
+ else
444
+ lines << arg.inspect.strip
445
+ end
446
+ end
447
+
448
+ return nil if(lines.empty? && block.nil?)
449
+
450
+ if lines
451
+ lines.each do |line|
452
+ STDERR.puts "# [SENV=#{ Senv.env }] : #{ line }"
453
+ end
454
+ end
455
+
456
+ if block
457
+ return block.call
458
+ else
459
+ true
460
+ end
461
+ end
462
+
463
+ def Senv.debug?
464
+ !!ENV['SENV_DEBUG']
465
+ end
466
+
467
+ def Senv.debug=(arg)
468
+ if arg
469
+ ENV['SENV_DEBUG'] = 'true'
470
+ else
471
+ ENV.delete('SENV_DEBUG')
472
+ end
473
+ end
474
+
475
+ #
476
+ def Senv.senvs
477
+ @senvs ||= Hash.new
478
+ end
479
+
480
+ def Senv.for_senv!(senv)
481
+ senv = senv.to_s
482
+
483
+ IO.popen('-', 'w+') do |io|
484
+ child = io.nil?
485
+
486
+ if child
487
+ Senv.load(senv)
488
+ puts Senv.environment.to_yaml
489
+ exit
490
+ else
491
+ YAML.load(io.read)
492
+ end
493
+ end
494
+ end
495
+
496
+ def Senv.for_senv(senv)
497
+ senv = senv.to_s
498
+
499
+ if Senv.senvs.has_key?(senv)
500
+ return Senv.senvs[senv]
501
+ end
502
+
503
+ Senv.senvs[senv] = Senv.for_senv!(senv)
504
+ end
505
+
506
+ def Senv.get(senv, var)
507
+ Senv.for_senv(senv)[var.to_s]
508
+ end
509
+
510
+ def Senv.get!(senv, var)
511
+ Senv.for_senv!(senv)[var.to_s]
512
+ end
513
+
514
+ #
515
+ module Blowfish
516
+ def cipher(senv, key, data)
517
+ cipher = OpenSSL::Cipher.new('bf-cbc').send(senv)
518
+ cipher.key = Digest::SHA256.digest(key.to_s).slice(0,16)
519
+ cipher.update(data) << cipher.final
520
+ end
521
+
522
+ def encrypt(key, data)
523
+ cipher(:encrypt, key, data)
524
+ end
525
+
526
+ def decrypt(key, text)
527
+ cipher(:decrypt, key, text)
528
+ end
529
+
530
+ def cycle(key, data)
531
+ decrypt(key, encrypt(key, data))
532
+ end
533
+
534
+ def recrypt(old_key, new_key, data)
535
+ encrypt(new_key, decrypt(old_key, data))
536
+ end
537
+
538
+ extend(self)
539
+ end
540
+
541
+ #
542
+ def Senv.capturing_environment_changes(&block)
543
+ EnvChangeTracker.track(&block)
544
+ end
545
+
546
+ class EnvChangeTracker < ::BasicObject
547
+ def initialize(env)
548
+ @env = env
549
+
550
+ @changes = {
551
+ :deleted => [],
552
+ :updated => [],
553
+ :created => [],
554
+ }
555
+
556
+ @change_for = proc do |key, val|
557
+ if @env.has_key?(key)
558
+ case
559
+ when val.nil?
560
+ {:type => :deleted, :info => [key, val]}
561
+ when val.to_s != @env[key].to_s
562
+ {:type => :updated, :info => [key, val]}
563
+ else
564
+ nil
565
+ end
566
+ else
567
+ {:type => :created, :info => [key, val]}
568
+ end
569
+ end
570
+
571
+ @track_change = proc do |key, val|
572
+ change = @change_for[key, val]
573
+
574
+ if change
575
+ @changes[change[:type]].push(change[:info])
576
+ end
577
+ end
578
+ end
579
+
580
+ def changes
581
+ @changes
582
+ end
583
+
584
+ def method_missing(method, *args, &block)
585
+ @env.send(method, *args, &block)
586
+ end
587
+
588
+ def []=(key, val)
589
+ @track_change[key, val]
590
+ @env[key] = val
591
+ end
592
+
593
+ def replace(hash)
594
+ hash.each do |key, val|
595
+ @track_change[key, val]
596
+ end
597
+ @env.replace(hash)
598
+ end
599
+
600
+ def store(key, val)
601
+ @track_change[key, val]
602
+ @env.store(key, val)
603
+ end
604
+
605
+ def delete(key)
606
+ @track_change[key, nil]
607
+ @env.delete(key)
608
+ end
609
+
610
+ def apply(env)
611
+ @changes[:created].each do |k, v|
612
+ env[k] = v
613
+ end
614
+ @changes[:updated].each do |k, v|
615
+ env[k] = v
616
+ end
617
+ @changes[:deleted].each do |k, v|
618
+ env.delete(k)
619
+ end
620
+ @changes
621
+ end
622
+
623
+ THREAD_SAFE = ::Monitor.new
624
+
625
+ def EnvChangeTracker.track(&block)
626
+ THREAD_SAFE.synchronize do
627
+ env = EnvChangeTracker.new(::ENV)
628
+
629
+ ::Object.send(:remove_const, :ENV)
630
+ ::Object.send(:const_set, :ENV, env)
631
+
632
+ begin
633
+ block.call
634
+ env
635
+ ensure
636
+ ::Object.send(:remove_const, :ENV)
637
+ ::Object.send(:const_set, :ENV, env)
638
+ end
639
+ end
640
+ end
641
+ end
642
+
643
+ #
644
+ class ::Pathname
645
+ unless ::Pathname.pwd.respond_to?(:glob)
646
+ def glob(glob, &block)
647
+ paths = []
648
+
649
+ Dir.glob("#{ self }/#{ glob }") do |entry|
650
+ path = Pathname.new(entry)
651
+
652
+ if block
653
+ block.call(path)
654
+ else
655
+ paths.push(path)
656
+ end
657
+ end
658
+
659
+ block ? nil : paths
660
+ end
661
+ end
662
+ end
663
+ end