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,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'