skull_island 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +39 -0
  4. data/.travis.yml +9 -2
  5. data/Gemfile +3 -1
  6. data/Gemfile.lock +127 -0
  7. data/README.md +348 -2
  8. data/Rakefile +13 -3
  9. data/bin/console +4 -3
  10. data/lib/core_extensions/string/transformations.rb +30 -0
  11. data/lib/skull_island/api_client.rb +36 -0
  12. data/lib/skull_island/api_client_base.rb +86 -0
  13. data/lib/skull_island/api_exception.rb +7 -0
  14. data/lib/skull_island/exceptions/api_client_not_configured.rb +9 -0
  15. data/lib/skull_island/exceptions/immutable_modification.rb +9 -0
  16. data/lib/skull_island/exceptions/invalid_arguments.rb +9 -0
  17. data/lib/skull_island/exceptions/invalid_cache_size.rb +9 -0
  18. data/lib/skull_island/exceptions/invalid_options.rb +9 -0
  19. data/lib/skull_island/exceptions/invalid_property.rb +9 -0
  20. data/lib/skull_island/exceptions/invalid_where_query.rb +9 -0
  21. data/lib/skull_island/exceptions/new_instance_with_id.rb +9 -0
  22. data/lib/skull_island/helpers/api_client.rb +64 -0
  23. data/lib/skull_island/helpers/resource.rb +178 -0
  24. data/lib/skull_island/helpers/resource_class.rb +74 -0
  25. data/lib/skull_island/lru_cache.rb +175 -0
  26. data/lib/skull_island/resource.rb +198 -0
  27. data/lib/skull_island/resource_collection.rb +193 -0
  28. data/lib/skull_island/resources/certificate.rb +36 -0
  29. data/lib/skull_island/resources/consumer.rb +20 -0
  30. data/lib/skull_island/resources/plugin.rb +144 -0
  31. data/lib/skull_island/resources/route.rb +83 -0
  32. data/lib/skull_island/resources/service.rb +94 -0
  33. data/lib/skull_island/resources/upstream.rb +129 -0
  34. data/lib/skull_island/resources/upstream_target.rb +86 -0
  35. data/lib/skull_island/rspec/fake_api_client.rb +63 -0
  36. data/lib/skull_island/rspec.rb +3 -0
  37. data/lib/skull_island/simple_api_client.rb +18 -0
  38. data/lib/skull_island/validations/api_client.rb +45 -0
  39. data/lib/skull_island/validations/resource.rb +24 -0
  40. data/lib/skull_island/version.rb +3 -1
  41. data/lib/skull_island.rb +47 -1
  42. data/skull_island.gemspec +16 -13
  43. metadata +66 -7
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkullIsland
4
+ # The API Client Base class
5
+ class APIClientBase
6
+ attr_reader :server, :base_uri
7
+ attr_accessor :username, :password
8
+
9
+ include Validations::APIClient
10
+ include Helpers::APIClient
11
+
12
+ def api_uri
13
+ @api_uri ||= URI.parse(server)
14
+ @api_uri.path = base_uri if base_uri
15
+ @api_uri
16
+ end
17
+
18
+ def authenticated?
19
+ raise Exceptions::APIClientNotConfigured unless configured?
20
+
21
+ @username && @password ? true : false
22
+ end
23
+
24
+ def configured?
25
+ @configured ? true : false
26
+ end
27
+
28
+ def json_headers
29
+ { content_type: :json, accept: :json }
30
+ end
31
+
32
+ def get(uri, data = nil)
33
+ client_action do |client|
34
+ if data
35
+ JSON.parse client[uri].get(json_headers.merge(params: data))
36
+ else
37
+ JSON.parse client[uri].get(json_headers)
38
+ end
39
+ end
40
+ end
41
+
42
+ def post(uri, data = nil)
43
+ client_action do |client|
44
+ if data
45
+ JSON.parse client[uri].post(json_escape(data.to_json), json_headers)
46
+ else
47
+ JSON.parse client[uri].post(nil, json_headers)
48
+ end
49
+ end
50
+ end
51
+
52
+ def patch(uri, data)
53
+ client_action do |client|
54
+ response = client[uri].patch(json_escape(data.to_json), json_headers)
55
+ if response && !response.empty?
56
+ JSON.parse(response)
57
+ else
58
+ true
59
+ end
60
+ end
61
+ end
62
+
63
+ def put(uri, data)
64
+ client_action do |client|
65
+ client[uri].put(json_escape(data.to_json), json_headers)
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def client_action
72
+ raise Exceptions::APIClientNotConfigured unless configured?
73
+
74
+ yield connection
75
+ end
76
+
77
+ # Don't bother creating a connection until we need one
78
+ def connection
79
+ @connection ||= if authenticated?
80
+ RestClient::Resource.new(api_uri.to_s, @username, @password)
81
+ else
82
+ RestClient::Resource.new(api_uri.to_s)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkullIsland
4
+ # Generic Exception definition
5
+ class APIException < StandardError
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkullIsland
4
+ module Exceptions
5
+ # The client must be configured before it can be used
6
+ class APIClientNotConfigured < APIException
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkullIsland
4
+ module Exceptions
5
+ # Provided when an attempt is made to modify an immutable resource
6
+ class ImmutableModification < APIException
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkullIsland
4
+ module Exceptions
5
+ # Missing, incorrect argument(s) were passed
6
+ class InvalidArguments < APIException
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkullIsland
4
+ module Exceptions
5
+ # Provided when a cache size is invalid
6
+ class InvalidCacheSize < APIException
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkullIsland
4
+ module Exceptions
5
+ # Missing, incorrect, or extra options were passed
6
+ class InvalidOptions < APIException
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkullIsland
4
+ module Exceptions
5
+ # Provided when property definitions use inappropriate names
6
+ class InvalidProperty < APIException
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkullIsland
4
+ module Exceptions
5
+ # Provided when a query is constructed but is invalid
6
+ class InvalidWhereQuery < APIException
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkullIsland
4
+ module Exceptions
5
+ # ID can not be manually specified on resources
6
+ class NewInstanceWithID < APIException
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkullIsland
4
+ module Helpers
5
+ # Simple helper methods for the API Client
6
+ module APIClient
7
+ def about_service
8
+ get '/'
9
+ end
10
+
11
+ def server_status
12
+ get '/status'
13
+ end
14
+
15
+ def cache(key)
16
+ symbolized_key = key.to_sym
17
+ if !@cache.has?(symbolized_key) && block_given?
18
+ result = yield(self)
19
+ @cache.store(symbolized_key, result)
20
+ elsif !@cache.has?(symbolized_key)
21
+ return nil
22
+ end
23
+ @cache.retrieve(symbolized_key)
24
+ end
25
+
26
+ def invalidate_cache_for(key)
27
+ symbolized_key = key.to_sym
28
+ @cache.invalidate(symbolized_key)
29
+ end
30
+
31
+ def lru_cache
32
+ @cache
33
+ end
34
+
35
+ # Substitute characters with their JSON-supported versions
36
+ # @return [String]
37
+ def json_escape(string)
38
+ json_escape = {
39
+ '&' => '\u0026',
40
+ '>' => '\u003e',
41
+ '<' => '\u003c',
42
+ '%' => '\u0025',
43
+ "\u2028" => '\u2028',
44
+ "\u2029" => '\u2029'
45
+ }
46
+ json_escape_regex = /[\u2028\u2029&><%]/u
47
+
48
+ string.to_s.gsub(json_escape_regex, json_escape)
49
+ end
50
+
51
+ # Provides access to the "raw" underlying rest-client
52
+ # @return [RestClient::Resource]
53
+ def raw
54
+ connection
55
+ end
56
+
57
+ # The API Client version (uses Semantic Versioning)
58
+ # @return [String]
59
+ def version
60
+ VERSION
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkullIsland
4
+ module Helpers
5
+ # Simple helper methods for Resources
6
+ module Resource
7
+ def datetime_from_params(params, actual_key)
8
+ DateTime.new(
9
+ params["#{actual_key}(1i)"].to_i,
10
+ params["#{actual_key}(2i)"].to_i,
11
+ params["#{actual_key}(3i)"].to_i,
12
+ params["#{actual_key}(4i)"].to_i,
13
+ params["#{actual_key}(5i)"].to_i
14
+ )
15
+ end
16
+
17
+ def fresh?
18
+ !tainted?
19
+ end
20
+
21
+ def host_regex
22
+ /^(([\w]|[\w][\w\-]*[\w])\.)*([\w]|[\w][\w\-]*[\w])$/
23
+ end
24
+
25
+ def id_property
26
+ self.class.properties.select { |_, opts| opts[:id_property] }.keys.first || 'id'
27
+ end
28
+
29
+ def id
30
+ @entity[id_property.to_s]
31
+ end
32
+
33
+ def immutable?
34
+ self.class.immutable?
35
+ end
36
+
37
+ # ActiveRecord ActiveModel::Name compatibility method
38
+ def model_name
39
+ self.class
40
+ end
41
+
42
+ def new?
43
+ !@entity.key?(id_property.to_s)
44
+ end
45
+
46
+ # ActiveRecord ActiveModel::Model compatibility method
47
+ def persisted?
48
+ !new?
49
+ end
50
+
51
+ def postprocess_created_at(value)
52
+ Time.at(value).utc.to_datetime
53
+ end
54
+
55
+ def postprocess_updated_at(value)
56
+ Time.at(value).utc.to_datetime
57
+ end
58
+
59
+ def properties
60
+ self.class.properties
61
+ end
62
+
63
+ def required_properties
64
+ properties.select { |_key, value| value[:required] }
65
+ end
66
+
67
+ def tainted?
68
+ @tainted ? true : false
69
+ end
70
+
71
+ # ActiveRecord ActiveModel::Conversion compatibility method
72
+ def to_key
73
+ new? ? [] : [id]
74
+ end
75
+
76
+ # ActiveRecord ActiveModel::Conversion compatibility method
77
+ def to_model
78
+ self
79
+ end
80
+
81
+ # ActiveRecord ActiveModel::Conversion compatibility method
82
+ def to_param
83
+ new? ? nil : id.to_s
84
+ end
85
+
86
+ def destroy
87
+ raise Exceptions::ImmutableModification if immutable?
88
+
89
+ unless new?
90
+ @api_client.delete(relative_uri.to_s)
91
+ @api_client.invalidate_cache_for(relative_uri.to_s)
92
+ @lazy = false
93
+ @tainted = true
94
+ @entity.delete('id')
95
+ end
96
+ true
97
+ end
98
+
99
+ def reload
100
+ if new?
101
+ # Can't reload a new resource
102
+ false
103
+ else
104
+ @api_client.invalidate_cache_for(relative_uri.to_s)
105
+ entity_data = @api_client.cache(relative_uri.to_s) do |client|
106
+ client.get(relative_uri.to_s)
107
+ end
108
+ @entity = entity_data
109
+ @lazy = false
110
+ @tainted = false
111
+ true
112
+ end
113
+ end
114
+
115
+ def save
116
+ saveable_data = prune_for_save(@entity)
117
+ validate_required_properties(saveable_data)
118
+
119
+ if new?
120
+ @entity = @api_client.post(save_uri.to_s, saveable_data)
121
+ @lazy = true
122
+ else
123
+ @api_client.invalidate_cache_for(relative_uri.to_s)
124
+ @entity = @api_client.put(relative_uri, saveable_data)
125
+ end
126
+ @tainted = false
127
+ true
128
+ end
129
+
130
+ def save_uri
131
+ self.class.relative_uri
132
+ end
133
+
134
+ # ActiveRecord ActiveModel compatibility method
135
+ def update(params)
136
+ new_params = {}
137
+ # need to convert multi-part datetime params
138
+ params.each do |key, value|
139
+ if /([^(]+)\(1i/.match?(key)
140
+ actual_key = key.match(/([^(]+)\(/)[1]
141
+ new_params[actual_key] = datetime_from_params(params, actual_key)
142
+ else
143
+ new_params[key] = value
144
+ end
145
+ end
146
+
147
+ new_params.each do |key, value|
148
+ setter_key = "#{key}=".to_sym
149
+ raise Exceptions::InvalidProperty unless respond_to?(setter_key)
150
+
151
+ send(setter_key, value)
152
+ end
153
+ save
154
+ end
155
+
156
+ def <=>(other)
157
+ if id < other.id
158
+ -1
159
+ elsif id > other.id
160
+ 1
161
+ elsif id == other.id
162
+ 0
163
+ else
164
+ raise Exceptions::InvalidArguments
165
+ end
166
+ end
167
+
168
+ def prune_for_save(data)
169
+ data.reject do |k, v|
170
+ k.to_sym == id_property ||
171
+ !properties[k.to_sym] ||
172
+ properties[k.to_sym][:read_only] ||
173
+ v.nil?
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkullIsland
4
+ module Helpers
5
+ # Simple helper class methods for Resource
6
+ module ResourceClass
7
+ # Determine a list of names to use to access a resource entity attribute
8
+ # @param original_name [String,Symbol] the name of the underlying attribute
9
+ # @param opts [Hash] property options as defined in a {Resource} subclass
10
+ # @return [Array<Symbol>] the list of names
11
+ def determine_getter_names(original_name, opts)
12
+ names = []
13
+ names << (opts[:type] == :boolean ? "#{original_name}?" : original_name)
14
+ if opts[:as]
15
+ Array(opts[:as]).each do |new_name|
16
+ names << (opts[:type] == :boolean ? "#{new_name}?" : new_name)
17
+ end
18
+ end
19
+ names.map(&:to_sym).uniq
20
+ end
21
+
22
+ # Determine a list of names to use to set a resource entity attribute
23
+ # @param original_name [String,Symbol] the name of the underlying attribute
24
+ # @param opts [Hash] property options as defined in a {Resource} subclass
25
+ # @return [Array<Symbol>] the list of names
26
+ def determine_setter_names(original_name, opts)
27
+ names = ["#{original_name}="]
28
+ names.concat(Array(opts[:as]).map { |new_name| "#{new_name}=" }) if opts[:as]
29
+ names.map(&:to_sym).uniq
30
+ end
31
+
32
+ # Produce a more human-readable representation of {#i18n_key}
33
+ # @note ActiveRecord ActiveModel::Name compatibility method
34
+ # @return [String]
35
+ def human
36
+ i18n_key.humanize
37
+ end
38
+
39
+ # Check if a resource class is immutable
40
+ def immutable?
41
+ @immutable ||= false
42
+ end
43
+
44
+ # A mock internationalization key based on the class name
45
+ # @note ActiveRecord ActiveModel::Name compatibility method
46
+ # @return [String]
47
+ def i18n_key
48
+ name.split('::').last.to_underscore
49
+ end
50
+
51
+ alias singular_route_key i18n_key
52
+
53
+ # A symbolized version of {#i18n_key}
54
+ # @note ActiveRecord ActiveModel::Name compatibility method
55
+ # @return [Symbol]
56
+ def param_key
57
+ i18n_key.to_sym
58
+ end
59
+
60
+ # All the properties defined for this Resource class
61
+ # @return [Hash{Symbol => Hash}]
62
+ def properties
63
+ @properties ||= {}
64
+ end
65
+
66
+ # A route key for building URLs
67
+ # @note ActiveRecord ActiveModel::Name compatibility method
68
+ # @return [String]
69
+ def route_key
70
+ i18n_key.en.plural
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkullIsland
4
+ # A very simple Least Recently Used (LRU) cache implementation. Stores data
5
+ # in a Hash, uses a dedicated Array for storing and sorting keys (and
6
+ # implementing the LRU algorithm), and doesn't bother storing access
7
+ # information for cache data. It stores hit and miss counts for the
8
+ # entire cache (not for individual keys). It also uses three mutexes for
9
+ # thread-safety: a write lock, a read lock, and a metadata lock.
10
+ class LRUCache
11
+ attr_reader :max_size, :keys
12
+
13
+ # @raise [Exceptions::InvalidCacheSize] if the max_size isn't an Integer
14
+ def initialize(max_size = 100)
15
+ raise Exceptions::InvalidCacheSize unless max_size.is_a?(Integer)
16
+
17
+ @max_size = max_size
18
+ @hits = 0
19
+ @misses = 0
20
+ @keys = []
21
+ @data = {}
22
+ @read_mutex = Mutex.new
23
+ @write_mutex = Mutex.new
24
+ @meta_mutex = Mutex.new
25
+ end
26
+
27
+ # Does the cache contain the requested item?
28
+ # This doesn't count against cache misses
29
+ # @param key [Symbol] the index of the potentially cached object
30
+ def has?(key)
31
+ @meta_mutex.synchronize { @keys.include?(key) }
32
+ end
33
+
34
+ alias has_key? has?
35
+ alias include? has?
36
+
37
+ # The number of items in the cache
38
+ # @return [Fixnum] key count
39
+ def size
40
+ @meta_mutex.synchronize { @keys.size }
41
+ end
42
+
43
+ # Convert the contents of the cache to a Hash
44
+ # @return [Hash] the cached data
45
+ def to_hash
46
+ @read_mutex.synchronize { @data.dup }
47
+ end
48
+
49
+ # Return a raw Array of the cache data without its keys.
50
+ # Not particularly useful but it may be useful in the future.
51
+ # @return [Array] just the cached values
52
+ def values
53
+ @read_mutex.synchronize { @data.values }
54
+ end
55
+
56
+ # Allow iterating over the cached items, represented as key+value pairs
57
+ def each(&block)
58
+ to_hash.each(&block)
59
+ end
60
+
61
+ # Invalidate a cached item by its index / key. Returns `nil` if the object
62
+ # doesn't exist.
63
+ # @param key [Symbol] the cached object's index
64
+ def invalidate(key)
65
+ invalidate_key(key)
66
+ @write_mutex.synchronize { @data.delete(key) }
67
+ end
68
+
69
+ alias delete invalidate
70
+
71
+ # Remove all items from the cache without clearing statistics
72
+ # @return [Boolean] was the truncate operation successful?
73
+ def truncate
74
+ @read_mutex.synchronize do
75
+ @write_mutex.synchronize do
76
+ @meta_mutex.synchronize { @keys = [] }
77
+ @data = {}
78
+ end
79
+ @data.empty?
80
+ end
81
+ end
82
+
83
+ # Similar to {#truncate} (in fact, it calls it) but it also clears the
84
+ # statistical metadata.
85
+ # @return [Boolean] was the flush operation successful?
86
+ def flush
87
+ if truncate
88
+ @meta_mutex.synchronize do
89
+ @hits = 0
90
+ @misses = 0
91
+ end
92
+ true
93
+ else
94
+ false
95
+ end
96
+ end
97
+
98
+ # Provides a hash of the current metadata for the cache. It provides the
99
+ # current cache size (`:size`),the number of cache hits (`:hits`), and
100
+ # the number of cache misses (`:misses`).
101
+ # @return [Hash] cache statistics
102
+ def statistics
103
+ {
104
+ size: size,
105
+ hits: @meta_mutex.synchronize { @hits },
106
+ misses: @meta_mutex.synchronize { @misses }
107
+ }
108
+ end
109
+
110
+ # Store some data (`value`) indexed by a `key`. If an object exists with
111
+ # the same key, and the value is different, it will be overwritten.
112
+ # Storing a value causes its key to be moved to the end of the keys array
113
+ # (meaning it is the __most recently used__ item), and this happens on
114
+ # #store regardless of whether or not the key previously existed.
115
+ # This behavior is relied upon by {#retrieve} to allow reorganization of
116
+ # the keys without necessarily modifying the data it indexes.
117
+ # Uses recursion for overwriting existing items.
118
+ #
119
+ # @param key [Symbol] the index to use for referencing this cached item
120
+ # @param value [Object] the data to cache
121
+ def store(key, value)
122
+ if has?(key)
123
+ if @read_mutex.synchronize { @data[key] == value }
124
+ invalidate_key(key)
125
+ @meta_mutex.synchronize { @keys << key }
126
+ value
127
+ else
128
+ invalidate(key)
129
+ store(key, value)
130
+ end
131
+ else
132
+ invalidate(@keys.first) until size < @max_size
133
+
134
+ @write_mutex.synchronize do
135
+ @meta_mutex.synchronize { @keys << key }
136
+ @data[key] = value
137
+ end
138
+ end
139
+ end
140
+
141
+ alias []= store
142
+
143
+ # Retrieve an item from the cache. Returns `nil` if the item does not
144
+ # exist. Relies on {#store} returning the stored value to ensure the LRU
145
+ # algorithm is maintained safely.
146
+ # @param key [Symbol] the index to retrieve
147
+ def retrieve(key)
148
+ if has?(key)
149
+ @meta_mutex.synchronize { @hits += 1 }
150
+ # Looks dumb, but it actually only reorganizes the keys Array
151
+ store(key, @read_mutex.synchronize { @data[key] })
152
+ else
153
+ @meta_mutex.synchronize { @misses += 1 }
154
+ nil
155
+ end
156
+ end
157
+
158
+ alias [] retrieve
159
+
160
+ def marshal_dump
161
+ [@max_size, @hits, @misses, @keys, @data]
162
+ end
163
+
164
+ def marshal_load(array)
165
+ @max_size, @hits, @misses, @keys, @data = array
166
+ end
167
+
168
+ private
169
+
170
+ # Invalidate just the key of a cached item. Dangerous if used incorrectly.
171
+ def invalidate_key(key)
172
+ @meta_mutex.synchronize { @keys.delete(key) }
173
+ end
174
+ end
175
+ end