envoku 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.simplecov +3 -3
- data/README.md +31 -6
- data/lib/envoku.rb +28 -9
- data/lib/envoku/adapters/s3.rb +38 -11
- data/lib/envoku/feature.rb +27 -5
- data/lib/envoku/logger.rb +23 -0
- data/lib/envoku/utils.rb +27 -0
- data/lib/envoku/version.rb +1 -1
- data/lib/tasks/envoku.rake +2 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: df8eba959ce87373465892dd6c02893c3ed62a24
|
4
|
+
data.tar.gz: 590152bb2183123ba32ff460276cec05993ac7db
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9e7ab915b62d44ba83a0db455d6b38182e7a17d5c4b11e230a4dbab2eab496a712794d8977e1db68f980d8c1574d94798ada72f4df29bebe795bead0f369afdc
|
7
|
+
data.tar.gz: 847edf89174d7f138277aa5e56711daf812863abbf03201abe87801df9d07d63ff2739f516e855a3cb8d436edade4e2acf95d1b1a9f7ccd88014806444149ed8
|
data/.simplecov
CHANGED
@@ -2,10 +2,10 @@ require "codeclimate-test-reporter"
|
|
2
2
|
require 'codecov'
|
3
3
|
|
4
4
|
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([
|
5
|
-
CodeClimate::TestReporter::Formatter,
|
6
|
-
SimpleCov::Formatter::Codecov,
|
5
|
+
(CodeClimate::TestReporter::Formatter if ENV['CODECLIMATE_REPO_TOKEN']),
|
6
|
+
(SimpleCov::Formatter::Codecov if ENV['CODECOV_TOKEN']),
|
7
7
|
SimpleCov::Formatter::HTMLFormatter,
|
8
|
-
])
|
8
|
+
].compact)
|
9
9
|
SimpleCov.start do
|
10
10
|
add_filter "spec"
|
11
11
|
add_group "Adapters", "lib/envoku/adapters/"
|
data/README.md
CHANGED
@@ -6,7 +6,9 @@
|
|
6
6
|
[![Code Climate](https://codeclimate.com/github/marcqualie/envoku-ruby/badges/gpa.svg)](https://codeclimate.com/github/marcqualie/envoku-ruby)
|
7
7
|
[![Issue Count](https://codeclimate.com/github/marcqualie/envoku-ruby/badges/issue_count.svg)](https://codeclimate.com/github/marcqualie/envoku-ruby)
|
8
8
|
|
9
|
-
|
9
|
+
**WIP:** This gem is currently under active development. Until **1.0** version backwards-incompatible changes may be introduced with each **0.x** minor version.
|
10
|
+
|
11
|
+
Configuration and feature management that is pre-loaded into your application at boot-time.
|
10
12
|
|
11
13
|
|
12
14
|
## Installation
|
@@ -19,21 +21,23 @@ gem 'envoku'
|
|
19
21
|
|
20
22
|
And then execute:
|
21
23
|
|
22
|
-
$ bundle
|
23
|
-
|
24
|
-
Or install it yourself as:
|
25
|
-
|
26
|
-
$ gem install envoku
|
24
|
+
$ bundle install
|
27
25
|
|
28
26
|
|
29
27
|
## Usage
|
30
28
|
|
31
29
|
When Envoku is loaded it automatically uses Dotenv in the background to preload and other configuration mechanisms you may have to make transitioning seamless.
|
32
30
|
|
31
|
+
|
33
32
|
### Rails
|
34
33
|
|
35
34
|
Envoku automatically pre-loads itself during `before_configuration` phase so no configuration is required.
|
36
35
|
|
36
|
+
### Sinatra
|
37
|
+
|
38
|
+
Use the plain ruby method as early as possible, before any `ENV` variables are accessed. Usually at the top of `app.rb`.
|
39
|
+
|
40
|
+
|
37
41
|
### Plain ruby
|
38
42
|
|
39
43
|
Run the following code before you need access to the environment variables
|
@@ -45,6 +49,12 @@ Envoku.load
|
|
45
49
|
|
46
50
|
## Features
|
47
51
|
|
52
|
+
Feature are defined as standard environment variables as YAML objects in the following format:
|
53
|
+
|
54
|
+
``` shell
|
55
|
+
ENVOKU_FEATURE_FEATURE1="description: 'Example feature', attribute1: 'something'"
|
56
|
+
```
|
57
|
+
|
48
58
|
``` ruby
|
49
59
|
all_features = Envoku::Feature.all
|
50
60
|
feature = Envoku::Feature.new('FEATURE1')
|
@@ -52,11 +62,19 @@ feature.enabled? # global for all resources
|
|
52
62
|
feature.enabled_for?(current_user) # does current_user have this feature enabled
|
53
63
|
feature.enable_for!(current_user) # enable feature for current_user
|
54
64
|
feature.disable_for!(current_user) # disable feature for current_user
|
65
|
+
feature.attributes # {"attribute1" => "something"}
|
66
|
+
feature.permitted_for?('User') # whether or not this feature is designed for a resource
|
55
67
|
resource.feature_enabled?('FEATURE1')
|
56
68
|
resource.toggle_feature!('FEATURE1')
|
57
69
|
resource.features_enabled
|
58
70
|
```
|
59
71
|
|
72
|
+
By default, all features can be used against all resources. To restrict a feature to a certain resource:
|
73
|
+
|
74
|
+
``` shell
|
75
|
+
ENVOKU_FEATURE_FEATURE1="permitted_resources: 'Organization,User'"
|
76
|
+
```
|
77
|
+
|
60
78
|
Per-resource features are stored in Redis via the following keys:
|
61
79
|
|
62
80
|
```
|
@@ -65,6 +83,13 @@ Per-resource features are stored in Redis via the following keys:
|
|
65
83
|
```
|
66
84
|
|
67
85
|
|
86
|
+
## Todo
|
87
|
+
|
88
|
+
- ~~Logging~~
|
89
|
+
- ~~Show warning when toggling features for non-permitted resources~~
|
90
|
+
- Add `Envoku.configure {}` functionality
|
91
|
+
|
92
|
+
|
68
93
|
## Development
|
69
94
|
|
70
95
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/lib/envoku.rb
CHANGED
@@ -1,32 +1,51 @@
|
|
1
1
|
require "redis"
|
2
|
+
require "logger"
|
2
3
|
|
3
4
|
require "envoku/version"
|
4
5
|
require "envoku/adapters/s3"
|
5
6
|
require "envoku/feature"
|
7
|
+
require "envoku/logger"
|
6
8
|
require "envoku/resource"
|
9
|
+
require "envoku/utils"
|
10
|
+
|
11
|
+
Dotenv.load("#{Dir.pwd}/.env") if defined?(Dotenv) && ENV['RAILS_ENV'] != "test"
|
7
12
|
|
8
13
|
require "envoku/rails" if defined?(Rails)
|
9
14
|
|
10
15
|
module Envoku
|
11
16
|
|
17
|
+
URL = Envoku::Utils.parsed_url
|
18
|
+
URI = Envoku::Utils.parsed_uri
|
19
|
+
|
12
20
|
module_function
|
13
21
|
|
14
22
|
def load(options = {})
|
15
|
-
|
23
|
+
Envoku.logger.info("load using S3 adapter")
|
24
|
+
instance = Envoku::Adapters::S3.new(options)
|
16
25
|
instance.load
|
17
26
|
end
|
18
27
|
|
19
|
-
def
|
20
|
-
|
21
|
-
|
22
|
-
|
28
|
+
def get_all
|
29
|
+
adapter = Envoku::Adapters::S3.new
|
30
|
+
adapter.load
|
31
|
+
adapter.get_all
|
23
32
|
end
|
24
33
|
|
25
|
-
def
|
26
|
-
|
34
|
+
def get(key)
|
35
|
+
adapter = Envoku::Adapters::S3.new
|
36
|
+
adapter.load
|
37
|
+
adapter.get(key)
|
27
38
|
end
|
28
39
|
|
29
|
-
def
|
30
|
-
|
40
|
+
def set(key, value)
|
41
|
+
adapter = Envoku::Adapters::S3.new
|
42
|
+
adapter.load
|
43
|
+
adapter.set(key, value)
|
44
|
+
end
|
45
|
+
|
46
|
+
def redis
|
47
|
+
@redis ||= ::Redis.new(
|
48
|
+
url: (ENV['ENVOKU_REDIS_URL'] || ENV['REDIS_URL']),
|
49
|
+
)
|
31
50
|
end
|
32
51
|
end
|
data/lib/envoku/adapters/s3.rb
CHANGED
@@ -12,7 +12,7 @@ module Envoku
|
|
12
12
|
|
13
13
|
attr_reader :options
|
14
14
|
|
15
|
-
def initialize
|
15
|
+
def initialize(custom_options = {})
|
16
16
|
default_options = {
|
17
17
|
filename: nil,
|
18
18
|
bucket_name: nil,
|
@@ -21,19 +21,47 @@ module Envoku
|
|
21
21
|
}
|
22
22
|
@options = OpenStruct.new default_options.merge(custom_options)
|
23
23
|
@local_file_name = "/tmp/envoku-#{SecureRandom.hex 16}.env"
|
24
|
+
@data = {}
|
24
25
|
end
|
25
26
|
|
26
27
|
def load
|
27
|
-
|
28
|
+
Envoku.logger.debug("Loading via S3 Adapter")
|
28
29
|
apply_environment_options
|
29
30
|
return unless options.bucket_name && options.filename && options.access_key_id && options.secret_access_key
|
31
|
+
Envoku.logger.debug("Downloading \"#{options.bucket_name}/#{options.filename}\" from S3")
|
30
32
|
FileUtils.rm @local_file_name if File.exists? @local_file_name
|
31
33
|
return unless clone_s3_file
|
32
|
-
|
34
|
+
@_env_before = ENV.to_h
|
35
|
+
Envoku.logger.debug("Applying ENV vars from S3")
|
36
|
+
@data = Dotenv.load(@local_file_name) || {}
|
37
|
+
@_env_after = ENV.to_h
|
38
|
+
# TODO: Abstract the env diff to adapter base
|
39
|
+
@_env_after.each do |key, value|
|
40
|
+
if !@_env_before.has_key?(key)
|
41
|
+
Envoku.logger.debug("- ADD #{key}")
|
42
|
+
# @data[key] = value
|
43
|
+
elsif @_env_before[key] != value
|
44
|
+
Envoku.logger.debug("- MOD #{key}")
|
45
|
+
# @data[key] = value
|
46
|
+
end
|
47
|
+
end
|
33
48
|
FileUtils.rm @local_file_name
|
34
49
|
ENV['ENVOKU_REFRESHED_AT'] = Time.now.to_s
|
35
50
|
end
|
36
51
|
|
52
|
+
def get_all
|
53
|
+
@data
|
54
|
+
end
|
55
|
+
|
56
|
+
def get(key)
|
57
|
+
@data[key]
|
58
|
+
end
|
59
|
+
|
60
|
+
def set(key, value)
|
61
|
+
# TODO: Not yet implemented
|
62
|
+
@data[key] = value
|
63
|
+
end
|
64
|
+
|
37
65
|
private
|
38
66
|
|
39
67
|
def clone_s3_file
|
@@ -48,7 +76,7 @@ module Envoku
|
|
48
76
|
"Expires=#{s3_expires_at.to_i}",
|
49
77
|
"Signature=#{CGI.escape Base64.encode64(hmac).strip}"
|
50
78
|
]
|
51
|
-
s3_signed_uri = URI.parse "#{s3_direct_url}?#{query_params.join('&')}"
|
79
|
+
s3_signed_uri = ::URI.parse "#{s3_direct_url}?#{query_params.join('&')}"
|
52
80
|
s3_response = Net::HTTP.get_response s3_signed_uri
|
53
81
|
if s3_response.is_a? Net::HTTPSuccess
|
54
82
|
File.write @local_file_name, s3_response.body
|
@@ -56,13 +84,12 @@ module Envoku
|
|
56
84
|
end
|
57
85
|
|
58
86
|
def apply_environment_options
|
59
|
-
if
|
60
|
-
|
61
|
-
|
62
|
-
@options.
|
63
|
-
@options.
|
64
|
-
@options.
|
65
|
-
@options.secret_access_key ||= envoku_uri.password
|
87
|
+
if Envoku::URL
|
88
|
+
return nil unless Envoku::URI && Envoku::URI.host && Envoku::URI.path && Envoku::URI.user && Envoku::URI.password
|
89
|
+
@options.filename ||= Envoku::URI.path[1..-1]
|
90
|
+
@options.bucket_name ||= Envoku::URI.host
|
91
|
+
@options.access_key_id ||= Envoku::URI.user
|
92
|
+
@options.secret_access_key ||= ::URI.unescape(Envoku::URI.password)
|
66
93
|
else
|
67
94
|
@options.filename ||= ENV['ENVOKU_FILENAME']
|
68
95
|
@options.bucket_name ||= ENV['ENVOKU_BUCKET']
|
data/lib/envoku/feature.rb
CHANGED
@@ -1,12 +1,24 @@
|
|
1
1
|
require 'yaml'
|
2
2
|
|
3
3
|
module Envoku
|
4
|
+
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def feature_enabled_for?(feature_name, resource)
|
8
|
+
Feature.new(feature_name).enabled_for?(resource)
|
9
|
+
end
|
10
|
+
|
11
|
+
def features_enabled_for(resource)
|
12
|
+
redis.smembers("#{Feature::REDIS_NAMESPACE}#{resource.class.name}:#{resource.id}")
|
13
|
+
end
|
14
|
+
|
4
15
|
class Feature
|
5
16
|
ENV_NAMESPACE = "ENVOKU_FEATURE_"
|
6
17
|
REDIS_NAMESPACE = "envoku:features:"
|
7
18
|
|
8
19
|
attr_reader :name
|
9
20
|
attr_reader :description
|
21
|
+
attr_reader :attributes
|
10
22
|
|
11
23
|
def self.all
|
12
24
|
keys = ENV.select { |key, value| key.index(ENV_NAMESPACE) == 0 }
|
@@ -18,13 +30,15 @@ module Envoku
|
|
18
30
|
def initialize(name)
|
19
31
|
@name = name
|
20
32
|
@enabled = false
|
33
|
+
@attributes = {}
|
34
|
+
@description = nil
|
21
35
|
var_string = ENV["#{ENV_NAMESPACE}#{name}"]
|
22
36
|
return nil unless var_string
|
23
|
-
attributes = ::YAML.safe_load(var_string) rescue {}
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
@enabled
|
37
|
+
@attributes = ::YAML.safe_load(var_string) rescue {}
|
38
|
+
@description = @attributes['description']
|
39
|
+
@enabled = !!@attributes['enabled']
|
40
|
+
@attributes.delete('description')
|
41
|
+
@attributes.delete('enabled')
|
28
42
|
end
|
29
43
|
|
30
44
|
def enabled?
|
@@ -37,6 +51,7 @@ module Envoku
|
|
37
51
|
end
|
38
52
|
|
39
53
|
def enable_for!(resource)
|
54
|
+
Envoku.logger.warn("feature #{name} is not permitted for #{resource.class.name}") unless permitted_for?(resource.class.name)
|
40
55
|
Envoku.redis.multi do
|
41
56
|
Envoku.redis.sadd("#{REDIS_NAMESPACE}#{@name}:#{resource.class.name}", resource.id.to_s)
|
42
57
|
Envoku.redis.sadd("#{REDIS_NAMESPACE}#{resource.class.name}:#{resource.id}", @name)
|
@@ -44,6 +59,7 @@ module Envoku
|
|
44
59
|
end
|
45
60
|
|
46
61
|
def disable_for!(resource)
|
62
|
+
Envoku.logger.warn("feature #{name} is not permitted for #{resource.class.name}") unless permitted_for?(resource.class.name)
|
47
63
|
Envoku.redis.multi do
|
48
64
|
Envoku.redis.del("#{REDIS_NAMESPACE}#{@name}:#{resource.class.name}")
|
49
65
|
Envoku.redis.del("#{REDIS_NAMESPACE}#{resource.class.name}:#{resource.id}")
|
@@ -54,6 +70,12 @@ module Envoku
|
|
54
70
|
enabled_for?(resource) ? disable_for!(resource) : enable_for!(resource)
|
55
71
|
end
|
56
72
|
|
73
|
+
def permitted_for?(resource_klass)
|
74
|
+
return true unless @attributes.has_key?('permitted_resources')
|
75
|
+
resource_klass = resource_klass.class.name unless resource_klass.is_a?(String)
|
76
|
+
@attributes['permitted_resources'].split(',').include?(resource_klass)
|
77
|
+
end
|
78
|
+
|
57
79
|
def resources
|
58
80
|
list = []
|
59
81
|
feature_prefix = "#{REDIS_NAMESPACE}#{@name}"
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "logger"
|
2
|
+
|
3
|
+
module Envoku
|
4
|
+
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def logger
|
8
|
+
@_logger ||= Envoku::Logger.new(STDOUT)
|
9
|
+
end
|
10
|
+
|
11
|
+
def logger=(logger)
|
12
|
+
@_logger = logger
|
13
|
+
end
|
14
|
+
|
15
|
+
class Logger < ::Logger
|
16
|
+
def initialize(*args)
|
17
|
+
super
|
18
|
+
@progname = "envoku"
|
19
|
+
log_level = (ENV["ENVOKU_LOG_LEVEL"] || ENV["LOG_LEVEL"] || "").upcase
|
20
|
+
@level = ["DEBUG", "INFO", "WARN", "ERROR", "FATAL", "UNKNOWN"].index(log_level) != nil ? Logger.const_get(log_level) : Logger::INFO
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/envoku/utils.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require "uri"
|
2
|
+
|
3
|
+
module Envoku
|
4
|
+
module Utils
|
5
|
+
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def parsed_url
|
9
|
+
return nil if ENV['ENVOKU_URL'] == nil || ENV['ENVOKU_URL'] == ""
|
10
|
+
ENV['ENVOKU_URL']
|
11
|
+
end
|
12
|
+
|
13
|
+
def parsed_uri
|
14
|
+
return nil unless parsed_url
|
15
|
+
parser = ::URI::RFC2396_Parser.new
|
16
|
+
uri = parser.parse(parsed_url)
|
17
|
+
uri
|
18
|
+
rescue Exception => error
|
19
|
+
Envoku.logger.error("URI Parse Error: URL = #{parsed_url || '[not set]'}")
|
20
|
+
Envoku.logger.error(" #{error.message}")
|
21
|
+
(0..2).each do |index|
|
22
|
+
Envoku.logger.error(" #{error.backtrace[index]}")
|
23
|
+
end
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/envoku/version.rb
CHANGED
data/lib/tasks/envoku.rake
CHANGED
@@ -9,7 +9,8 @@ namespace :envoku do
|
|
9
9
|
|
10
10
|
desc "Show Envoku Info"
|
11
11
|
task :info => :load do
|
12
|
-
puts "URL: #{
|
12
|
+
puts "URL: #{Envoku::URL || '[not set]'}"
|
13
|
+
puts "URI: #{Envoku::URI.to_yaml}"
|
13
14
|
end
|
14
15
|
|
15
16
|
desc "List Envoku Features"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: envoku
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marc Qualie
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-11-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dotenv
|
@@ -132,8 +132,10 @@ files:
|
|
132
132
|
- lib/envoku/adapters.rb
|
133
133
|
- lib/envoku/adapters/s3.rb
|
134
134
|
- lib/envoku/feature.rb
|
135
|
+
- lib/envoku/logger.rb
|
135
136
|
- lib/envoku/rails.rb
|
136
137
|
- lib/envoku/resource.rb
|
138
|
+
- lib/envoku/utils.rb
|
137
139
|
- lib/envoku/version.rb
|
138
140
|
- lib/tasks/envoku.rake
|
139
141
|
- script/console
|