skull_island 0.1.0 → 0.1.1

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.
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,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkullIsland
4
+ # Resource classes go here...
5
+ module Resources
6
+ # The Service resource class
7
+ #
8
+ # @see https://docs.konghq.com/0.14.x/admin-api/#service-object Service API definition
9
+ class Service < Resource
10
+ property :name
11
+ property :retries
12
+ property :protocol, validate: true, required: true
13
+ property :host, validate: true, required: true
14
+ property :port, validate: true, required: true
15
+ property :path
16
+ property :connect_timeout, validate: true
17
+ property :write_timeout, validate: true
18
+ property :read_timeout, validate: true
19
+ property :created_at, read_only: true, postprocess: true
20
+ property :updated_at, read_only: true, postprocess: true
21
+
22
+ # Convenience method to add routes
23
+ def add_route!(details)
24
+ r = details.is_a?(Route) ? details : Route.from_hash(details, api_client: api_client)
25
+
26
+ r.service = self
27
+ r.save
28
+ end
29
+
30
+ # Provides a collection of related {Route} instances
31
+ def routes
32
+ Route.where(:service, self, api_client: api_client)
33
+ end
34
+
35
+ # Provides a collection of related {Plugin} instances
36
+ def plugins
37
+ Plugin.where(:service, self, api_client: api_client)
38
+ end
39
+
40
+ def url=(uri_or_string)
41
+ uri_data = URI(uri_or_string)
42
+ self.protocol = uri_data.scheme
43
+ self.host = uri_data.host
44
+ self.port = uri_data.port
45
+ end
46
+
47
+ def url
48
+ u = URI('')
49
+ u.scheme = protocol
50
+ u.host = host
51
+ u.port = port unless [80, 443].include? port
52
+ u.to_s
53
+ end
54
+
55
+ private
56
+
57
+ # Used to validate {#protocol} on set
58
+ def validate_protocol(value)
59
+ # only HTTP and HTTPS are allowed
60
+ %w[http https].include? value
61
+ end
62
+
63
+ # Used to validate {#host} on set
64
+ def validate_host(value)
65
+ # allow only valid hostnames
66
+ value.match?(host_regex) && !value.match?(/_/)
67
+ end
68
+
69
+ # Used to validate {#port} on set
70
+ def validate_port(value)
71
+ # only positive Integers of the right value are allowed
72
+ value.is_a?(Integer) && value.positive? && (1...65_535).cover?(value)
73
+ end
74
+
75
+ # Used to validate {#connect_timeout} on set
76
+ def validate_connect_timeout(value)
77
+ # only positive Integers are allowed
78
+ value.is_a?(Integer) && value.positive?
79
+ end
80
+
81
+ # Used to validate {#write_timeout} on set
82
+ def validate_write_timeout(value)
83
+ # only positive Integers are allowed
84
+ value.is_a?(Integer) && value.positive?
85
+ end
86
+
87
+ # Used to validate {#read_timeout} on set
88
+ def validate_read_timeout(value)
89
+ # only positive Integers are allowed
90
+ value.is_a?(Integer) && value.positive?
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkullIsland
4
+ # Resource classes go here...
5
+ module Resources
6
+ # The Upstream resource class
7
+ #
8
+ # @see https://docs.konghq.com/0.14.x/admin-api/#upstream-objects Upstream API definition
9
+ class Upstream < Resource
10
+ property :name, required: true, validate: true
11
+ property :slots, validate: true
12
+ property :hash_on, validate: true
13
+ property :hash_fallback, validate: true
14
+ property :hash_on_header, validate: true
15
+ property :hash_fallback_header, validate: true
16
+ property :hash_on_cookie, validate: true
17
+ property :hash_on_cookie_path, validate: true
18
+ property :healthchecks, validate: true
19
+ property :created_at, read_only: true, postprocess: true
20
+
21
+ def health
22
+ if new?
23
+ # No health status for new Upstreams
24
+ nil
25
+ else
26
+ health_json = api_client.get("#{relative_uri}/health")
27
+ health_json['data']
28
+ end
29
+ end
30
+
31
+ # Convenience method to add upstream targets
32
+ def add_target!(details)
33
+ r = if details.is_a?(UpstreamTarget)
34
+ details
35
+ else
36
+ UpstreamTarget.from_hash(details, api_client: api_client)
37
+ end
38
+
39
+ r.upstream = self
40
+ r.save
41
+ end
42
+
43
+ def target(target_id)
44
+ UpstreamTarget.new(
45
+ entity: { 'id' => target_id, 'upstream_id' => id },
46
+ lazy: true,
47
+ tainted: false,
48
+ api_client: api_client
49
+ )
50
+ end
51
+
52
+ def targets
53
+ target_list_data = api_client.get("#{relative_uri}/targets")
54
+ root = 'data' # root for API JSON response data
55
+ # TODO: do something with lazy requests...
56
+
57
+ ResourceCollection.new(
58
+ target_list_data[root].each do |record|
59
+ UpstreamTarget.new(
60
+ entity: record,
61
+ lazy: false,
62
+ tainted: false,
63
+ api_client: api_client
64
+ )
65
+ end,
66
+ type: UpstreamTarget,
67
+ api_client: api_client
68
+ )
69
+ end
70
+
71
+ private
72
+
73
+ # Used to validate {#hash_on} on set
74
+ def validate_hash_on(value)
75
+ # only String of an acceptable value are allowed
76
+ %w[none consumer ip header cookie].include?(value)
77
+ end
78
+
79
+ # Used to validate {#hash_fallback} on set
80
+ def validate_hash_fallback(value)
81
+ # only String of an acceptable value are allowed
82
+ %w[none consumer ip header cookie].include?(value)
83
+ end
84
+
85
+ # Used to validate {#hash_on_header} on set
86
+ def validate_hash_on_header(value)
87
+ # only String is allowed and only when {#hash_on} is set to 'header'
88
+ value.is_a?(String) && hash_on == 'header'
89
+ end
90
+
91
+ # Used to validate {#hash_fallback_header} on set
92
+ def validate_hash_fallback_header(value)
93
+ # only String is allowed and only when {#hash_fallback} is set to 'header'
94
+ value.is_a?(String) && hash_fallback == 'header'
95
+ end
96
+
97
+ # Used to validate {#hash_on_cookie} on set
98
+ def validate_hash_on_cookie(value)
99
+ # only String is allowed and only when {#hash_on} or {#hash_fallback} is set to 'cookie'
100
+ value.is_a?(String) && [hash_on, hash_fallback].include?('cookie')
101
+ end
102
+
103
+ # Used to validate {#hash_cookie_path} on set
104
+ def validate_hash_cookie_path(value)
105
+ # only String is allowed and only when {#hash_on} or {#hash_fallback} is set to 'cookie'
106
+ value.is_a?(String) && [hash_on, hash_fallback].include?('cookie')
107
+ end
108
+
109
+ # Used to validate {#name} on set
110
+ def validate_name(value)
111
+ # only String is allowed
112
+ value.is_a?(String)
113
+ end
114
+
115
+ # Used to validate {#slots} on set
116
+ def validate_slots(value)
117
+ # only Integer is allowed
118
+ value.is_a?(Integer)
119
+ end
120
+
121
+ # Used to validate {#healthchecks} on set
122
+ def validate_healthchecks(value)
123
+ # TODO: seriously need to make this better...
124
+ # only Hash is allowed
125
+ value.is_a?(Hash)
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkullIsland
4
+ # Resource classes go here...
5
+ module Resources
6
+ # The Upstream Target resource class
7
+ #
8
+ # @see https://docs.konghq.com/0.14.x/admin-api/#target-object Target API definition
9
+ class UpstreamTarget < Resource
10
+ property :target, required: true, validate: true, preprocess: true
11
+ property(
12
+ :upstream_id,
13
+ required: true, validate: true, preprocess: true, postprocess: true, as: :upstream
14
+ )
15
+ property :weight, validate: true
16
+ property :created_at, read_only: true, postprocess: true
17
+
18
+ def self.get(id, options = {})
19
+ if options[:upstream]&.is_a?(Upstream)
20
+ options[:upstream].target(id)
21
+ elsif options[:upstream]
22
+ upstream_opts = options.merge(lazy: true)
23
+ Upstream.get(options[:upstream], upstream_opts).target(id)
24
+ end
25
+ end
26
+
27
+ def relative_uri
28
+ upstream ? "#{upstream.relative_uri}/targets/#{id}" : nil
29
+ end
30
+
31
+ def save_uri
32
+ upstream ? "#{upstream.relative_uri}/targets" : nil
33
+ end
34
+
35
+ def preprocess_target(input)
36
+ if input.is_a?(URI)
37
+ "#{input.host}:#{input.port || 8000}"
38
+ else
39
+ input
40
+ end
41
+ end
42
+
43
+ def preprocess_upstream_id(input)
44
+ if input.is_a?(Hash)
45
+ input['id']
46
+ elsif input.is_a?(String)
47
+ input
48
+ else
49
+ input.id
50
+ end
51
+ end
52
+
53
+ def postprocess_upstream_id(value)
54
+ if value.is_a?(String)
55
+ Upstream.new(
56
+ entity: { 'id' => value },
57
+ lazy: true,
58
+ tainted: false
59
+ )
60
+ else
61
+ value
62
+ end
63
+ end
64
+
65
+ # Used to validate {#target} on set
66
+ def validate_target(value)
67
+ # only URIs or specific strings
68
+ value.is_a?(URI) || (
69
+ value.is_a?(String) && value.match?(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}/)
70
+ )
71
+ end
72
+
73
+ # Used to validate #upstream on set
74
+ def validate_upstream_id(value)
75
+ # allow either a Upstream object or a String
76
+ value.is_a?(Upstream) || value.is_a?(String)
77
+ end
78
+
79
+ # Used to validate {#weight} on set
80
+ def validate_weight(value)
81
+ # only positive Integers (or zero) are allowed
82
+ value.is_a?(Integer) && (0..1000).cover?(value)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+
5
+ module SkullIsland
6
+ module RSpec
7
+ # A Fake API Client for RSpec testing
8
+ class FakeAPIClient
9
+ attr_reader :server, :base_uri
10
+ attr_accessor :username, :password
11
+
12
+ include Validations::APIClient
13
+ include Helpers::APIClient
14
+
15
+ def initialize(opts = {})
16
+ # validations
17
+ validate_opts(opts)
18
+
19
+ # Set up the client's state
20
+ @server = opts[:server] || 'http://localhost:8001'
21
+ @username = opts[:username] || 'admin'
22
+ @password = opts[:password] || 'admin'
23
+ @cache = LRUCache.new(100) # LRU cache of up to 100 items
24
+ @configured = true
25
+ end
26
+
27
+ def hash(data)
28
+ if data
29
+ Digest::MD5.hexdigest(data.sort.to_s)
30
+ else
31
+ ''
32
+ end
33
+ end
34
+
35
+ def response_for(type, uri, data: nil, response: {})
36
+ @responses ||= {}
37
+ @responses[type.to_s] ||= {}
38
+ key = data ? uri.to_s + hash(data) : uri.to_s
39
+ @responses[type.to_s][key] = response
40
+ end
41
+
42
+ def get(uri, _data = nil)
43
+ @responses ||= {}
44
+ @responses.dig('get', uri.to_s)
45
+ end
46
+
47
+ def post(uri, data = nil)
48
+ @responses ||= {}
49
+ @responses.dig('post', uri.to_s + hash(data))
50
+ end
51
+
52
+ def patch(uri, data)
53
+ @responses ||= {}
54
+ @responses.dig('patch', uri.to_s + hash(data))
55
+ end
56
+
57
+ def put(uri, data)
58
+ @responses ||= {}
59
+ @responses.dig('put', uri.to_s + hash(data))
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'skull_island/rspec/fake_api_client'
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkullIsland
4
+ # The Simple API Client class
5
+ class SimpleAPIClient < APIClientBase
6
+ def initialize(opts = {})
7
+ # validations
8
+ validate_opts(opts)
9
+
10
+ # Set up the client's state
11
+ @server = opts[:server] || 'http://localhost:8001'
12
+ @username = opts[:username]
13
+ @password = opts[:password]
14
+ @cache = LRUCache.new(size: 1000) # LRU cache of up to 1000 items
15
+ @configured = true
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkullIsland
4
+ module Validations
5
+ # APIClient validation methods
6
+ module APIClient
7
+ def validate_creds(opts, exception)
8
+ auto_opts_size = [opts[:username], opts[:password]].compact.size
9
+ raise(exception, 'Invalid Auth Provided') if auto_opts_size == 1
10
+ end
11
+
12
+ # Validate the options passed on initialize
13
+ def validate_opts(opts)
14
+ e = Exceptions::InvalidOptions
15
+ raise(e, 'Is not Hash-like') unless opts.respond_to?(:[]) && opts.respond_to?(:key?)
16
+
17
+ validate_server(opts[:server])
18
+ if opts.key?(:base_uri)
19
+ begin
20
+ URI.parse(opts[:base_uri])
21
+ rescue URI::InvalidURIError => e
22
+ raise(e, "Invalid base_uri: #{e}")
23
+ end
24
+ end
25
+
26
+ validate_creds(opts, e)
27
+ true
28
+ end
29
+
30
+ # Validate the server name provided
31
+ def validate_server(name)
32
+ return true unless name # if no name is provided, just go with the defaults
33
+
34
+ valid = name.is_a? String
35
+ begin
36
+ u = URI.parse(name)
37
+ valid = false unless u.class == URI::HTTP || u.class == URI::HTTPS
38
+ rescue URI::InvalidURIError
39
+ valid = false
40
+ end
41
+ valid || raise(Exceptions::InvalidOptions, 'Invalid server')
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkullIsland
4
+ module Validations
5
+ # Resource validation methods
6
+ module Resource
7
+ def validate_mutability
8
+ raise Exceptions::ImmutableModification if immutable? && @tainted # this shouldn't happen
9
+ end
10
+
11
+ # The 'id' field should not be set manually
12
+ def validate_id
13
+ raise Exceptions::NewInstanceWithID if @entity.key?('id') && @tainted
14
+ end
15
+
16
+ # Ensure that required properties are set before saving
17
+ def validate_required_properties(data)
18
+ required_properties.each do |name, _value|
19
+ raise Exceptions::InvalidArguments if data[name.to_s].nil?
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SkullIsland
2
4
  VERSION = [
3
5
  0, # Major
4
6
  1, # Minor
5
- 0 # Patch
7
+ 1 # Patch
6
8
  ].join('.')
7
9
  end
data/lib/skull_island.rb CHANGED
@@ -1,4 +1,50 @@
1
- require "skull_island/version"
1
+ # frozen_string_literal: true
2
+
3
+ # Standard Library Requirements
4
+ require 'date'
5
+ require 'json'
6
+ require 'singleton'
7
+ require 'uri'
8
+
9
+ # External Library Requirements
10
+ require 'linguistics'
11
+ Linguistics.use(:en)
12
+ require 'rest-client'
13
+ require 'will_paginate'
14
+ require 'will_paginate/array'
15
+
16
+ # Internal Requirements
17
+ require 'core_extensions/string/transformations'
18
+ String.include CoreExtensions::String::Transformations
19
+
20
+ require 'skull_island/version'
21
+ require 'skull_island/api_exception'
22
+ require 'skull_island/exceptions/api_client_not_configured'
23
+ require 'skull_island/exceptions/immutable_modification'
24
+ require 'skull_island/exceptions/invalid_arguments'
25
+ require 'skull_island/exceptions/invalid_cache_size'
26
+ require 'skull_island/exceptions/invalid_options'
27
+ require 'skull_island/exceptions/invalid_property'
28
+ require 'skull_island/exceptions/invalid_where_query'
29
+ require 'skull_island/exceptions/new_instance_with_id'
30
+ require 'skull_island/helpers/api_client'
31
+ require 'skull_island/validations/api_client'
32
+ require 'skull_island/lru_cache'
33
+ require 'skull_island/api_client_base'
34
+ require 'skull_island/api_client'
35
+ require 'skull_island/simple_api_client'
36
+ require 'skull_island/resource_collection'
37
+ require 'skull_island/helpers/resource'
38
+ require 'skull_island/helpers/resource_class'
39
+ require 'skull_island/validations/resource'
40
+ require 'skull_island/resource'
41
+ require 'skull_island/resources/certificate'
42
+ require 'skull_island/resources/consumer'
43
+ require 'skull_island/resources/plugin'
44
+ require 'skull_island/resources/service'
45
+ require 'skull_island/resources/route'
46
+ require 'skull_island/resources/upstream'
47
+ require 'skull_island/resources/upstream_target'
2
48
 
3
49
  module SkullIsland
4
50
  class Error < StandardError; end
data/skull_island.gemspec CHANGED
@@ -1,36 +1,39 @@
1
+ # frozen_string_literal: true
1
2
 
2
- lib = File.expand_path("../lib", __FILE__)
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "skull_island/version"
5
+ require 'skull_island/version'
5
6
 
6
7
  Gem::Specification.new do |spec|
7
- spec.name = "skull_island"
8
+ spec.name = 'skull_island'
8
9
  spec.version = SkullIsland::VERSION
9
- spec.authors = ["Jonathan Gnagy"]
10
- spec.email = ["jonathan.gnagy@gmail.com"]
10
+ spec.authors = ['Jonathan Gnagy']
11
+ spec.email = ['jonathan.gnagy@gmail.com']
11
12
 
12
13
  spec.summary = 'Ruby SDK for Kong'
13
14
  spec.description = 'A Ruby SDK for Kong 1.0.x'
14
15
  spec.homepage = 'https://github.com/jgnagy/skull_island'
15
- spec.license = "MIT"
16
+ spec.license = 'MIT'
16
17
 
17
18
  # Specify which files should be added to the gem when it is released.
18
19
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
20
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
20
21
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
22
  end
22
- spec.bindir = "exe"
23
+ spec.bindir = 'exe'
23
24
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
- spec.require_paths = ["lib"]
25
+ spec.require_paths = ['lib']
25
26
 
26
27
  spec.required_ruby_version = '~> 2.5'
27
28
 
28
29
  spec.add_runtime_dependency 'json', '~> 2.0'
29
- spec.add_runtime_dependency 'faraday', '~> 0.15'
30
+ spec.add_runtime_dependency 'linguistics', '~> 2.1'
31
+ spec.add_runtime_dependency 'rest-client', '~> 2.0'
32
+ spec.add_runtime_dependency 'will_paginate', '~> 3.1'
30
33
 
31
- spec.add_development_dependency "bundler", "~> 2.0"
32
- spec.add_development_dependency "rake", "~> 10.0"
33
- spec.add_development_dependency "rspec", "~> 3.0"
34
+ spec.add_development_dependency 'bundler', '~> 2.0'
35
+ spec.add_development_dependency 'rake', '~> 10.0'
36
+ spec.add_development_dependency 'rspec', '~> 3.0'
34
37
  spec.add_development_dependency 'rubocop', '~> 0.50'
35
38
  spec.add_development_dependency 'simplecov', '~> 0.15'
36
39
  spec.add_development_dependency 'travis', '~> 1.8'