skull_island 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 19ab037beb02518e751019c8d59bd81cdf9165b373ea5b31287ed6dc829a4f21
4
- data.tar.gz: 1a352da5080eaae1ce030f872fcda1e53dbbbca2dfb39af5536db2cb1772fe8e
3
+ metadata.gz: 4d6ae44ca3ddea5f866e493af05df16b8355716442acd5a475e1177049260177
4
+ data.tar.gz: 41ee93c3a6491b28b7e516e3b13166506080f52fa6b3f439bc4510e77b8cae39
5
5
  SHA512:
6
- metadata.gz: f5f1d27b7b1114ccad775df93c7db93d42279fcdb085cf76323efb2f1c2d5aa09dc6a83132e4ab7dbc9eefd349b420ff10f9f7c3abc53fe39839aa18e787c9cb
7
- data.tar.gz: ee44a34f72104f02785949a609919230d511e39dbb332ee96ed4c13ec95d800e240d098613570b49b2d0814ac6b81f86f4cc201dfdb8bed93de1cdc320e451c9
6
+ metadata.gz: 5996e8c7bf5773c8d43f98d2d85f8590d47caa61d19d9a198a4252f6e34f834dd41ff9989ea3726eedbb94ae323b3b79d0c6ad472ab8d6525eb902911191c0f5
7
+ data.tar.gz: 48f5e8d92c92c6ab399426c3ba39687a20c091dfe01f5c0126cb13d935f6efdf8fd906f0ae38fbb140c740d17781e80e866572c8d4cdab2a0ecc6cc417fb7af6
data/.rubocop.yml CHANGED
@@ -12,10 +12,16 @@ Metrics/ClassLength:
12
12
 
13
13
  Metrics/ModuleLength:
14
14
  Max: 165
15
+ Exclude:
16
+ - 'lib/skull_island/helpers/resource.rb'
15
17
 
16
18
  Metrics/CyclomaticComplexity:
17
19
  Max: 7
18
20
 
21
+ Metrics/PerceivedComplexity:
22
+ Exclude:
23
+ - 'lib/skull_island/cli.rb'
24
+
19
25
  Metrics/AbcSize:
20
26
  Max: 25
21
27
 
@@ -32,7 +38,7 @@ Layout/IndentHeredoc:
32
38
 
33
39
  Security/Eval:
34
40
  Exclude:
35
- - Gemfile
41
+ - 'lib/skull_island/cli.rb'
36
42
 
37
43
  Style/NumericLiterals:
38
44
  Exclude:
data/Gemfile.lock CHANGED
@@ -1,10 +1,12 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- skull_island (0.1.1)
5
- json (~> 2.0)
4
+ skull_island (0.2.0)
5
+ erubi (~> 1.8)
6
+ json (~> 2.1)
6
7
  linguistics (~> 2.1)
7
8
  rest-client (~> 2.0)
9
+ thor (~> 0.20)
8
10
  will_paginate (~> 3.1)
9
11
 
10
12
  GEM
@@ -17,6 +19,7 @@ GEM
17
19
  docile (1.3.1)
18
20
  domain_name (0.5.20180417)
19
21
  unf (>= 0.0.5, < 1.0.0)
22
+ erubi (1.8.0)
20
23
  ethon (0.12.0)
21
24
  ffi (>= 1.3.0)
22
25
  faraday (0.15.4)
@@ -91,6 +94,7 @@ GEM
91
94
  json (>= 1.8, < 3)
92
95
  simplecov-html (~> 0.10.0)
93
96
  simplecov-html (0.10.2)
97
+ thor (0.20.3)
94
98
  travis (1.8.9)
95
99
  backports
96
100
  faraday (~> 0.9)
data/README.md CHANGED
@@ -26,7 +26,92 @@ Gem::Specification.new do |spec|
26
26
  end
27
27
  ```
28
28
 
29
- ## Usage
29
+ ## CLI Usage
30
+
31
+ Skull Island comes with an executable called `skull_island` that leverages the SDK under the hood. Learn about what is can do via `help`:
32
+
33
+ ```
34
+ $ skull_island help
35
+ Commands:
36
+ skull_island export [OPTIONS] OUTPUT_FILE # Export the current configuration to OUTPUT_FILE
37
+ skull_island help [COMMAND] # Describe available commands or one specific command
38
+ skull_island import [OPTIONS] INPUT_FILE # Import a configuration from INPUT_FILE
39
+
40
+ Options:
41
+ [--verbose], [--no-verbose]
42
+ ```
43
+
44
+ To use the commands that interact with the Kong API, set environment variables for the required parameters:
45
+
46
+ ```
47
+ KONG_ADMIN_URL='https://api-admin.mydomain.com' \
48
+ KONG_ADMIN_USERNAME='my-basicauth-user' \
49
+ KONG_ADMIN_PASSWORD='my-basicauth-password' \
50
+ skull_island ...
51
+ ```
52
+
53
+ Note that you can skip `KONG_ADMIN_USERNAME` and `KONG_ADMIN_PASSWORD` if you aren't using a basic-auth reverse-proxy in front of the Admin API.
54
+
55
+ Also note that if you're having SSL issues (such as with a private CA), you can have Ruby make use of a custom CA public key using `SSL_CERT_FILE`:
56
+
57
+ ```
58
+ SSL_CERT_FILE=/path/to/cabundle.pem \
59
+ KONG_ADMIN_URL='https://api-admin.mydomain.com' \
60
+ KONG_ADMIN_USERNAME='my-basicauth-user' \
61
+ KONG_ADMIN_PASSWORD='my-basicauth-password' \
62
+ skull_island ...
63
+ ```
64
+
65
+ ### Exporting
66
+
67
+ The CLI allows you to export an existing configuration to a YAML + ERB document (a YAML document with embedded Ruby). This format is helpful because it doesn't require you to know the IDs of resources, making your configuration portable.
68
+
69
+ The `export` command will default to outputting to STDOUT if you don't provide an output file location. Otherwise, simply specify the filename you'd like to export to:
70
+
71
+ ```
72
+ KONG_ADMIN_URL='https://api-admin.mydomain.com' \
73
+ skull_island export /path/to/export.yml
74
+ ```
75
+
76
+ You can also get a little more information by turning on `--verbose`:
77
+
78
+ ```
79
+ KONG_ADMIN_URL='https://api-admin.mydomain.com' \
80
+ skull_island export --verbose /path/to/export.yml
81
+ ```
82
+
83
+ ### Importing
84
+
85
+ Skull Island also supports importing configurations (both partial and full) from a YAML + ERB document:
86
+
87
+ ```
88
+ KONG_ADMIN_URL='https://api-admin.mydomain.com' \
89
+ skull_island import /path/to/export.yml
90
+ ```
91
+
92
+ It'll also read from STDIN if you don't specify a file path (or if you specify `-` as the path):
93
+
94
+ ```
95
+ cat /path/to/export.yml | KONG_ADMIN_URL='https://api-admin.mydomain.com' skull_island import
96
+ # OR
97
+ KONG_ADMIN_URL='https://api-admin.mydomain.com' skull_island import < /path/to/export.yml
98
+ ```
99
+
100
+ You can also get a little more information by turning on `--verbose`:
101
+
102
+ ```
103
+ KONG_ADMIN_URL='https://api-admin.mydomain.com' \
104
+ skull_island import --verbose /path/to/export.yml
105
+ ```
106
+
107
+ Importing also supports a "dry run" functionality that shows you what it would do (but makes no changes) using `--test`:
108
+
109
+ ```
110
+ KONG_ADMIN_URL='https://api-admin.mydomain.com' \
111
+ skull_island import --verbose --test /path/to/export.yml
112
+ ```
113
+
114
+ ## SDK Usage
30
115
 
31
116
  The API Client requires configuration before it can be used. For now, this is a matter of calling `APIClient.configure()`, passing a Hash, with Symbols for keys:
32
117
 
@@ -59,7 +144,7 @@ APIClient.server_status
59
144
  # => {"database"=>{"reachable"=>true...
60
145
  ```
61
146
 
62
- This SDK also makes automatic (and mostly unobtrusive) caching behind the scenes. As long as this tool is the only tool making changes to the Admin API (at least while it is being used), this should be fine. Eventually, there will be an option to disable this cache (at the cost of poor performance). For now, it is possible to query this cache and even flushed it manually when required:
147
+ This SDK also makes use of automatic (and mostly unobtrusive) caching behind the scenes. As long as this tool is the only tool making changes to the Admin API (at least while it is being used), this should be fine. Eventually, there will be an option to disable this cache (at the cost of poor performance). For now, it is possible to query this cache and even flushed it manually when required:
63
148
 
64
149
  ```ruby
65
150
  APIClient.lru_cache
@@ -194,7 +279,7 @@ resource.enabled = true # A Boolean
194
279
  resource.config = { 'minute' => 50, 'hour' => 1000 } # A Hash of config keys and values
195
280
 
196
281
  # Either reference related resources by ID
197
- resource.service = { 'id' => '5fd1z584-1adb-40a5-c042-63b19db49x21' }
282
+ resource.service = '5fd1z584-1adb-40a5-c042-63b19db49x21'
198
283
  resource.service
199
284
  # => #<SkullIsland::Resources::Services:0x00007f9f201f6f44...
200
285
 
data/exe/skull_island ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'skull_island/cli'
6
+
7
+ configuration = {
8
+ server: ENV['KONG_ADMIN_URL']
9
+ }
10
+ configuration[:username] = ENV['KONG_ADMIN_USERNAME'] if ENV['KONG_ADMIN_USERNAME']
11
+ configuration[:password] = ENV['KONG_ADMIN_PASSWORD'] if ENV['KONG_ADMIN_PASSWORD']
12
+
13
+ SkullIsland::APIClient.configure(configuration)
14
+
15
+ SkullIsland::CLI.start(ARGV)
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Internal requirements
4
+ require 'skull_island'
5
+
6
+ # External requirements
7
+ require 'yaml'
8
+ require 'thor'
9
+
10
+ module SkullIsland
11
+ # Base CLI for SkullIsland
12
+ class CLI < Thor
13
+ class_option :verbose, type: :boolean
14
+
15
+ desc 'export [OPTIONS] OUTPUT_FILE', 'Export the current configuration to OUTPUT_FILE'
16
+ def export(output_file = '-')
17
+ if output_file == '-'
18
+ STDERR.puts '[INFO] Outputting to STDOUT' if options['verbose']
19
+ else
20
+ full_filename = File.expand_path(output_file)
21
+ dirname = File.dirname(full_filename)
22
+ unless File.exist?(dirname) && File.ftype(dirname) == 'directory'
23
+ raise Exceptions::InvalidArguments, "#{full_filename} is invalid"
24
+ end
25
+ end
26
+
27
+ output = { 'version' => '0.14' }
28
+
29
+ [
30
+ Resources::Consumer,
31
+ Resources::Service,
32
+ Resources::Upstream,
33
+ Resources::Plugin
34
+ ].each { |clname| export_class(clname, output) }
35
+
36
+ if output_file == '-'
37
+ STDOUT.puts output.to_yaml
38
+ else
39
+ File.write(full_filename, output.to_yaml)
40
+ end
41
+ end
42
+
43
+ desc 'import [OPTIONS] INPUT_FILE', 'Import a configuration from INPUT_FILE'
44
+ option :test, type: :boolean, desc: "Don't do anything, just show what would happen"
45
+ def import(input_file = '-')
46
+ raw ||= if input_file == '-'
47
+ STDERR.puts '[INFO] Reading from STDOUT' if options['verbose']
48
+ STDIN.read
49
+ else
50
+ full_filename = File.expand_path(input_file)
51
+ unless File.exist?(full_filename) && File.ftype(full_filename) == 'file'
52
+ raise Exceptions::InvalidArguments, "#{full_filename} is invalid"
53
+ end
54
+
55
+ begin
56
+ File.read(full_filename)
57
+ rescue StandardError => e
58
+ raise "Unable to process #{relative_path}: #{e.message}"
59
+ end
60
+ end
61
+
62
+ # rubocop:disable Security/YAMLLoad
63
+ input = YAML.load(raw)
64
+ # rubocop:enable Security/YAMLLoad
65
+
66
+ [
67
+ Resources::Consumer,
68
+ Resources::Service,
69
+ Resources::Upstream,
70
+ Resources::Plugin
71
+ ].each { |clname| import_class(clname, input) }
72
+ end
73
+
74
+ private
75
+
76
+ def export_class(class_name, output_data)
77
+ STDERR.puts "[INFO] Processing #{class_name.route_key}" if options['verbose']
78
+ output_data[class_name.route_key] = class_name.all.collect(&:export)
79
+ end
80
+
81
+ def import_class(class_name, import_data)
82
+ STDERR.puts "[INFO] Processing #{class_name.route_key}" if options['verbose']
83
+ class_name.batch_import(
84
+ import_data[class_name.route_key],
85
+ verbose: options['verbose'],
86
+ test: options['test']
87
+ )
88
+ end
89
+ end
90
+ end
@@ -14,6 +14,45 @@ module SkullIsland
14
14
  )
15
15
  end
16
16
 
17
+ # rubocop:disable Style/GuardClause
18
+ def delayed_set(property, data, key)
19
+ # rubocop:disable Security/Eval
20
+ if data[key]
21
+ send(
22
+ "#{property}=".to_sym,
23
+ data[key].is_a?(String) ? eval(Erubi::Engine.new(data[key]).src) : data[key]
24
+ )
25
+ end
26
+ # rubocop:enable Security/Eval
27
+ end
28
+ # rubocop:enable Style/GuardClause
29
+
30
+ def digest
31
+ Digest::MD5.hexdigest(
32
+ digest_properties.sort.map { |prop| "#{prop}=#{send(prop.to_sym)}" }.compact.join(':')
33
+ )
34
+ end
35
+
36
+ def digest_properties
37
+ properties.keys.reject { |k| %i[created_at updated_at].include? k }
38
+ end
39
+
40
+ # Tests for an existing version of this resource based on its properties rather than its `id`
41
+ def find_by_digest
42
+ result = self.class.where(:digest, digest) # matching digest means the equivalent resource
43
+ if result.size == 1
44
+ entity_data = @api_client.cache(result.first.relative_uri.to_s) do |client|
45
+ client.get(result.first.relative_uri.to_s)
46
+ end
47
+ @entity = entity_data
48
+ @lazy = false
49
+ @tainted = false
50
+ true
51
+ else
52
+ false
53
+ end
54
+ end
55
+
17
56
  def fresh?
18
57
  !tainted?
19
58
  end
@@ -34,6 +73,39 @@ module SkullIsland
34
73
  self.class.immutable?
35
74
  end
36
75
 
76
+ # rubocop:disable Metrics/CyclomaticComplexity
77
+ # rubocop:disable Metrics/PerceivedComplexity
78
+ def import_update_or_skip(verbose: false, test: false, index:)
79
+ if find_by_digest
80
+ puts "[INFO] Skipping #{self.class} index #{index} (#{id})" if verbose
81
+ elsif test
82
+ puts "[INFO] Would have saved #{sef.class} index #{index}"
83
+ elsif modified_existing?
84
+ puts "[INFO] Modified #{self.class} index #{index} (#{id})" if verbose
85
+ elsif save
86
+ puts "[INFO] Saved #{self.class} index #{index} (#{id})" if verbose
87
+ else
88
+ puts "[ERR] Failed to save #{self.class} index #{index}"
89
+ end
90
+ end
91
+ # rubocop:enable Metrics/CyclomaticComplexity
92
+ # rubocop:enable Metrics/PerceivedComplexity
93
+
94
+ def lookup(type, value)
95
+ case type
96
+ when :consumer
97
+ Resources::Consumer.find(:username, value).id
98
+ when :route
99
+ Resources::Route.find(:name, value).id
100
+ when :service
101
+ Resources::Service.find(:name, value).id
102
+ when :upstream
103
+ Resources::Upstream.find(:name, value).id
104
+ else
105
+ raise Exceptions::InvalidArguments, "#{type} is not a valid lookup type"
106
+ end
107
+ end
108
+
37
109
  # ActiveRecord ActiveModel::Name compatibility method
38
110
  def model_name
39
111
  self.class
@@ -68,21 +140,15 @@ module SkullIsland
68
140
  @tainted ? true : false
69
141
  end
70
142
 
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
143
  # ActiveRecord ActiveModel::Conversion compatibility method
82
144
  def to_param
83
145
  new? ? nil : id.to_s
84
146
  end
85
147
 
148
+ def to_s
149
+ to_param.to_s
150
+ end
151
+
86
152
  def destroy
87
153
  raise Exceptions::ImmutableModification if immutable?
88
154
 
@@ -123,6 +189,7 @@ module SkullIsland
123
189
  @api_client.invalidate_cache_for(relative_uri.to_s)
124
190
  @entity = @api_client.put(relative_uri, saveable_data)
125
191
  end
192
+ @api_client.invalidate_cache_for(self.class.relative_uri.to_s) # clear any collection class
126
193
  @tainted = false
127
194
  true
128
195
  end
@@ -10,7 +10,8 @@ module SkullIsland
10
10
  # @return [Array<Symbol>] the list of names
11
11
  def determine_getter_names(original_name, opts)
12
12
  names = []
13
- names << (opts[:type] == :boolean ? "#{original_name}?" : original_name)
13
+ names << original_name
14
+ names << "#{original_name}?" if opts[:type] == :boolean
14
15
  if opts[:as]
15
16
  Array(opts[:as]).each do |new_name|
16
17
  names << (opts[:type] == :boolean ? "#{new_name}?" : new_name)
@@ -97,8 +97,12 @@ module SkullIsland
97
97
  root = 'data' # root for API JSON response data
98
98
  # TODO: do something with lazy requests...
99
99
 
100
+ collection_entity = api_client.cache(relative_uri) do |client|
101
+ client.get(relative_uri)[root]
102
+ end
103
+
100
104
  ResourceCollection.new(
101
- api_client.get(relative_uri)[root].collect do |record|
105
+ collection_entity.collect do |record|
102
106
  unless options[:lazy]
103
107
  api_client.invalidate_cache_for "#{relative_uri}/#{record['id']}"
104
108
  api_client.cache("#{relative_uri}/#{record['id']}") do
@@ -131,6 +135,12 @@ module SkullIsland
131
135
  )
132
136
  end
133
137
 
138
+ # Returns the first (and hopefully only) resource given some criteria
139
+ # This is a very crude helper and could be made much better
140
+ def self.find(attribute, value, options = {})
141
+ where(attribute, value, options).first
142
+ end
143
+
134
144
  def self.get(id, options = {})
135
145
  # TODO: Add validations for options
136
146
 
@@ -12,6 +12,45 @@ module SkullIsland
12
12
  property :snis, validate: true
13
13
  property :created_at, read_only: true, postprocess: true
14
14
 
15
+ def self.batch_import(data, verbose: false, test: false)
16
+ raise(Exceptions::InvalidArguments) unless data.is_a?(Array)
17
+
18
+ data.each_with_index do |resource_data, index|
19
+ resource = new
20
+ resource.cert = resource_data['cert']
21
+ resource.key = resource_data['key']
22
+ resource.snis = resource_data['snis'] if resource_data['snis']
23
+ resource.import_update_or_skip(index: index, verbose: verbose, test: test)
24
+ end
25
+ end
26
+
27
+ def export(options = {})
28
+ hash = { 'cert' => cert, 'key' => key, 'snis' => snis }
29
+ [*options[:exclude]].each do |exclude|
30
+ hash.delete(exclude.to_s)
31
+ end
32
+ [*options[:include]].each do |inc|
33
+ hash[inc.to_s] = send(:inc)
34
+ end
35
+ hash.reject { |_, value| value.nil? }
36
+ end
37
+
38
+ def modified_existing?
39
+ return false unless new?
40
+
41
+ # Find certs of the same cert and key
42
+ same_key = self.class.where(:key, key)
43
+
44
+ existing = same_key.size == 1 ? same_key.first : nil
45
+
46
+ if existing
47
+ @entity['id'] = existing.id
48
+ save
49
+ else
50
+ false
51
+ end
52
+ end
53
+
15
54
  private
16
55
 
17
56
  # Used to validate {#cert} on set
@@ -11,10 +11,48 @@ module SkullIsland
11
11
  property :custom_id
12
12
  property :created_at, read_only: true, postprocess: true
13
13
 
14
+ def self.batch_import(data, verbose: false, test: false)
15
+ raise(Exceptions::InvalidArguments) unless data.is_a?(Array)
16
+
17
+ data.each_with_index do |resource_data, index|
18
+ resource = new
19
+ resource.username = resource_data['username']
20
+ resource.custom_id = resource_data['custom_id']
21
+ resource.import_update_or_skip(index: index, verbose: verbose, test: test)
22
+ end
23
+ end
24
+
14
25
  # Provides a collection of related {Plugin} instances
15
26
  def plugins
16
27
  Plugin.where(:consumer, self, api_client: api_client)
17
28
  end
29
+
30
+ def export(options = {})
31
+ hash = { 'username' => username, 'custom_id' => custom_id }
32
+ [*options[:exclude]].each do |exclude|
33
+ hash.delete(exclude.to_s)
34
+ end
35
+ [*options[:include]].each do |inc|
36
+ hash[inc.to_s] = send(:inc)
37
+ end
38
+ hash.reject { |_, value| value.nil? }
39
+ end
40
+
41
+ def modified_existing?
42
+ return false unless new?
43
+
44
+ # Find consumers of the same username
45
+ same_username = self.class.where(:username, username)
46
+
47
+ existing = same_username.size == 1 ? same_username.first : nil
48
+
49
+ if existing
50
+ @entity['id'] = existing.id
51
+ save
52
+ else
53
+ false
54
+ end
55
+ end
18
56
  end
19
57
  end
20
58
  end
@@ -16,6 +16,21 @@ module SkullIsland
16
16
  property :service_id, validate: true, preprocess: true, postprocess: true, as: :service
17
17
  property :created_at, read_only: true, postprocess: true
18
18
 
19
+ def self.batch_import(data, verbose: false, test: false)
20
+ raise(Exceptions::InvalidArguments) unless data.is_a?(Array)
21
+
22
+ data.each_with_index do |resource_data, index|
23
+ resource = new
24
+ resource.name = resource_data['name']
25
+ resource.enabled = resource_data['enabled']
26
+ resource.config = resource_data['config'] if resource_data['config']
27
+ resource.delayed_set(:consumer, resource_data, 'consumer_id')
28
+ resource.delayed_set(:route, resource_data, 'route_id')
29
+ resource.delayed_set(:service, resource_data, 'service_id')
30
+ resource.import_update_or_skip(index: index, verbose: verbose, test: test)
31
+ end
32
+ end
33
+
19
34
  def self.enabled_names(api_client: APIClient.instance)
20
35
  api_client.get("#{relative_uri}/enabled")['enabled_plugins']
21
36
  end
@@ -24,6 +39,51 @@ module SkullIsland
24
39
  api_client.get("#{relative_uri}/schema/#{name}")
25
40
  end
26
41
 
42
+ def export(options = {})
43
+ hash = {
44
+ 'name' => name,
45
+ 'enabled' => enabled?,
46
+ 'config' => config
47
+ }
48
+ hash['consumer_id'] = "<%= lookup :consumer, '#{consumer.username}' %>" if consumer
49
+ hash['route_id'] = "<%= lookup :route, '#{route.name}' %>" if route
50
+ hash['service_id'] = "<%= lookup :service, '#{service.name}' %>" if service
51
+ [*options[:exclude]].each do |exclude|
52
+ hash.delete(exclude.to_s)
53
+ end
54
+ [*options[:include]].each do |inc|
55
+ hash[inc.to_s] = send(:inc)
56
+ end
57
+ hash.reject { |_, value| value.nil? }
58
+ end
59
+
60
+ # rubocop:disable Metrics/PerceivedComplexity
61
+ def modified_existing?
62
+ return false unless new?
63
+
64
+ # Find plugins of the same name
65
+ same_name = self.class.where(:name, name)
66
+ return false if same_name.size.zero?
67
+
68
+ same_name_and_consumer = same_name.where(:consumer, consumer)
69
+ same_name_and_route = same_name.where(:route, route)
70
+ same_name_and_service = same_name.where(:service, service)
71
+ existing = if same_name_and_consumer.size == 1
72
+ same_name_and_consumer.first
73
+ elsif same_name_and_route.size == 1
74
+ same_name_and_route.first
75
+ elsif same_name_and_service.size == 1
76
+ same_name_and_service.first
77
+ end
78
+ if existing
79
+ @entity['id'] = existing.id
80
+ save
81
+ else
82
+ false
83
+ end
84
+ end
85
+ # rubocop:enable Metrics/PerceivedComplexity
86
+
27
87
  private
28
88
 
29
89
  # TODO: 1.0.x requires refactoring as `consumer_id` becomes `consumer`
@@ -125,19 +185,19 @@ module SkullIsland
125
185
  # Used to validate {#consumer} on set
126
186
  def validate_consumer_id(value)
127
187
  # allow either a Consumer object or a Hash of a specific structure
128
- value.is_a?(Consumer) || (value.is_a?(Hash) && value['id'].is_a?(String))
188
+ value.is_a?(Consumer) || value.is_a?(String)
129
189
  end
130
190
 
131
191
  # Used to validate {#route} on set
132
192
  def validate_route_id(value)
133
193
  # allow either a Route object or a Hash of a specific structure
134
- value.is_a?(Route) || (value.is_a?(Hash) && value['id'].is_a?(String))
194
+ value.is_a?(Route) || value.is_a?(String)
135
195
  end
136
196
 
137
197
  # Used to validate {#service} on set
138
198
  def validate_service_id(value)
139
199
  # allow either a Service object or a Hash of a specific structure
140
- value.is_a?(Service) || (value.is_a?(Hash) && value['id'].is_a?(String))
200
+ value.is_a?(Service) || value.is_a?(String)
141
201
  end
142
202
  end
143
203
  end
@@ -23,11 +23,72 @@ module SkullIsland
23
23
  property :created_at, read_only: true, postprocess: true
24
24
  property :updated_at, read_only: true, postprocess: true
25
25
 
26
+ # rubocop:disable Metrics/CyclomaticComplexity
27
+ # rubocop:disable Metrics/PerceivedComplexity
28
+ # rubocop:disable Metrics/AbcSize
29
+ def self.batch_import(data, verbose: false, test: false)
30
+ raise(Exceptions::InvalidArguments) unless data.is_a?(Array)
31
+
32
+ data.each_with_index do |rdata, index|
33
+ resource = new
34
+ resource.name = rdata['name']
35
+ resource.methods = rdata['methods'] if rdata['methods']
36
+ resource.paths = rdata['paths'] if rdata['paths']
37
+ resource.protocols = rdata['protocols'] if rdata['protocols']
38
+ resource.hosts = rdata['hosts'] if rdata['hosts']
39
+ resource.regex_priority = rdata['regex_priority'] if rdata['regex_priority']
40
+ resource.strip_path = rdata['strip_path'] unless rdata['strip_path'].nil?
41
+ resource.preserve_host = rdata['preserve_host'] unless rdata['preserve_host'].nil?
42
+ resource.delayed_set(:service, rdata, 'service')
43
+ resource.import_update_or_skip(index: index, verbose: verbose, test: test)
44
+ end
45
+ end
46
+ # rubocop:enable Metrics/CyclomaticComplexity
47
+ # rubocop:enable Metrics/PerceivedComplexity
48
+ # rubocop:enable Metrics/AbcSize
49
+
26
50
  # Provides a collection of related {Plugin} instances
27
51
  def plugins
28
52
  Plugin.where(:route, self, api_client: api_client)
29
53
  end
30
54
 
55
+ def export(options = {})
56
+ hash = {
57
+ 'name' => name,
58
+ 'methods' => methods,
59
+ 'paths' => paths,
60
+ 'protocols' => protocols,
61
+ 'hosts' => hosts,
62
+ 'regex_priority' => regex_priority,
63
+ 'strip_path' => strip_path?,
64
+ 'preserve_host' => preserve_host?
65
+ }
66
+ hash['service'] = { 'id' => "<%= lookup :service, '#{service.name}' %>" } if service
67
+ [*options[:exclude]].each do |exclude|
68
+ hash.delete(exclude.to_s)
69
+ end
70
+ [*options[:include]].each do |inc|
71
+ hash[inc.to_s] = send(:inc)
72
+ end
73
+ hash.reject { |_, value| value.nil? }
74
+ end
75
+
76
+ def modified_existing?
77
+ return false unless new?
78
+
79
+ # Find routes of the same name and service
80
+ same_name_and_service = self.class.where(:name, name).and(:service, service)
81
+
82
+ existing = same_name_and_service.size == 1 ? same_name_and_service.first : nil
83
+
84
+ if existing
85
+ @entity['id'] = existing.id
86
+ save
87
+ else
88
+ false
89
+ end
90
+ end
91
+
31
92
  private
32
93
 
33
94
  def postprocess_service(value)
@@ -19,6 +19,32 @@ module SkullIsland
19
19
  property :created_at, read_only: true, postprocess: true
20
20
  property :updated_at, read_only: true, postprocess: true
21
21
 
22
+ # rubocop:disable Metrics/AbcSize
23
+ def self.batch_import(data, verbose: false, test: false)
24
+ raise(Exceptions::InvalidArguments) unless data.is_a?(Array)
25
+
26
+ data.each_with_index do |rdata, index|
27
+ resource = new
28
+ resource.name = rdata['name']
29
+ resource.retries = rdata['retries'] if rdata['retries']
30
+ resource.protocol = rdata['protocol']
31
+ resource.host = rdata['host']
32
+ resource.port = rdata['port']
33
+ resource.path = rdata['path'] if rdata['path']
34
+ resource.connect_timeout = rdata['connect_timeout'] if rdata['connect_timeout']
35
+ resource.write_timeout = rdata['write_timeout'] if rdata['write_timeout']
36
+ resource.read_timeout = rdata['read_timeout'] if rdata['read_timeout']
37
+ resource.import_update_or_skip(index: index, verbose: verbose, test: test)
38
+
39
+ Route.batch_import(
40
+ rdata['routes'].map { |r| r.merge('service' => { 'id' => resource.id }) },
41
+ verbose: verbose,
42
+ test: test
43
+ )
44
+ end
45
+ end
46
+ # rubocop:enable Metrics/AbcSize
47
+
22
48
  # Convenience method to add routes
23
49
  def add_route!(details)
24
50
  r = details.is_a?(Route) ? details : Route.from_hash(details, api_client: api_client)
@@ -37,6 +63,44 @@ module SkullIsland
37
63
  Plugin.where(:service, self, api_client: api_client)
38
64
  end
39
65
 
66
+ def export(options = {})
67
+ hash = {
68
+ 'name' => name,
69
+ 'retries' => retries,
70
+ 'protocol' => protocol,
71
+ 'host' => host,
72
+ 'port' => port,
73
+ 'path' => path,
74
+ 'connect_timeout' => connect_timeout,
75
+ 'write_timeout' => write_timeout,
76
+ 'read_timeout' => read_timeout
77
+ }
78
+ hash['routes'] = routes.collect { |route| route.export(exclude: 'service') }
79
+ [*options[:exclude]].each do |exclude|
80
+ hash.delete(exclude.to_s)
81
+ end
82
+ [*options[:include]].each do |inc|
83
+ hash[inc.to_s] = send(:inc)
84
+ end
85
+ hash.reject { |_, value| value.nil? }
86
+ end
87
+
88
+ def modified_existing?
89
+ return false unless new?
90
+
91
+ # Find routes of the same name
92
+ same_name = self.class.where(:name, name)
93
+
94
+ existing = same_name.size == 1 ? same_name.first : nil
95
+
96
+ if existing
97
+ @entity['id'] = existing.id
98
+ save
99
+ else
100
+ false
101
+ end
102
+ end
103
+
40
104
  def url=(uri_or_string)
41
105
  uri_data = URI(uri_or_string)
42
106
  self.protocol = uri_data.scheme
@@ -18,6 +18,41 @@ module SkullIsland
18
18
  property :healthchecks, validate: true
19
19
  property :created_at, read_only: true, postprocess: true
20
20
 
21
+ # rubocop:disable Metrics/CyclomaticComplexity
22
+ # rubocop:disable Metrics/PerceivedComplexity
23
+ # rubocop:disable Metrics/AbcSize
24
+ def self.batch_import(data, verbose: false, test: false)
25
+ raise(Exceptions::InvalidArguments) unless data.is_a?(Array)
26
+
27
+ data.each_with_index do |rdata, index|
28
+ resource = new
29
+ resource.name = rdata['name']
30
+ resource.slots = rdata['slots'] if rdata['slots']
31
+ resource.hash_on = rdata['hash_on']
32
+ resource.hash_fallback = rdata['hash_fallback']
33
+ resource.hash_on_header = rdata['hash_on_header']
34
+ if rdata['hash_fallback_header']
35
+ resource.hash_fallback_header = rdata['hash_fallback_header']
36
+ end
37
+ resource.hash_on_cookie = rdata['hash_on_cookie'] if rdata['hash_on_cookie']
38
+ if rdata['hash_on_cookie_path']
39
+ resource.hash_on_cookie_path = rdata['hash_on_cookie_path']
40
+ end
41
+ resource.healthchecks = rdata['healthchecks'] if rdata['healthchecks']
42
+ resource.import_update_or_skip(index: index, verbose: verbose, test: test)
43
+ puts '[INFO] Processing UpstreamTarget entries...' if verbose
44
+
45
+ UpstreamTarget.batch_import(
46
+ rdata['targets'].map { |t| t.merge('upstream_id' => resource.id) },
47
+ verbose: verbose,
48
+ test: test
49
+ )
50
+ end
51
+ end
52
+ # rubocop:enable Metrics/CyclomaticComplexity
53
+ # rubocop:enable Metrics/PerceivedComplexity
54
+ # rubocop:enable Metrics/AbcSize
55
+
21
56
  def health
22
57
  if new?
23
58
  # No health status for new Upstreams
@@ -68,6 +103,44 @@ module SkullIsland
68
103
  )
69
104
  end
70
105
 
106
+ def export(options = {})
107
+ hash = {
108
+ 'name' => name,
109
+ 'slots' => slots,
110
+ 'hash_on' => hash_on,
111
+ 'hash_fallback' => hash_fallback,
112
+ 'hash_on_header' => hash_on_header,
113
+ 'hash_fallback_header' => hash_fallback_header,
114
+ 'hash_on_cookie' => hash_on_cookie,
115
+ 'hash_on_cookie_path' => hash_on_cookie_path,
116
+ 'healthchecks' => healthchecks
117
+ }
118
+ hash['targets'] = targets.collect { |route| route.export(exclude: 'upstream_id') }
119
+ [*options[:exclude]].each do |exclude|
120
+ hash.delete(exclude.to_s)
121
+ end
122
+ [*options[:include]].each do |inc|
123
+ hash[inc.to_s] = send(:inc)
124
+ end
125
+ hash.reject { |_, value| value.nil? }
126
+ end
127
+
128
+ def modified_existing?
129
+ return false unless new?
130
+
131
+ # Find routes of the same name
132
+ same_name = self.class.where(:name, name)
133
+
134
+ existing = same_name.size == 1 ? same_name.first : nil
135
+
136
+ if existing
137
+ @entity['id'] = existing.id
138
+ save
139
+ else
140
+ false
141
+ end
142
+ end
143
+
71
144
  private
72
145
 
73
146
  # Used to validate {#hash_on} on set
@@ -15,6 +15,18 @@ module SkullIsland
15
15
  property :weight, validate: true
16
16
  property :created_at, read_only: true, postprocess: true
17
17
 
18
+ def self.batch_import(data, verbose: false, test: false)
19
+ raise(Exceptions::InvalidArguments) unless data.is_a?(Array)
20
+
21
+ data.each_with_index do |resource_data, index|
22
+ resource = new
23
+ resource.target = resource_data['target']
24
+ resource.delayed_set(:upstream, resource_data, 'upstream_id')
25
+ resource.weight = resource_data['weight'] if resource_data['weight']
26
+ resource.import_update_or_skip(index: index, verbose: verbose, test: test)
27
+ end
28
+ end
29
+
18
30
  def self.get(id, options = {})
19
31
  if options[:upstream]&.is_a?(Upstream)
20
32
  options[:upstream].target(id)
@@ -32,6 +44,36 @@ module SkullIsland
32
44
  upstream ? "#{upstream.relative_uri}/targets" : nil
33
45
  end
34
46
 
47
+ def export(options = {})
48
+ hash = { 'target' => target, 'weight' => weight }
49
+ hash['upstream_id'] = "<%= lookup :upstream, '#{upstream.name}' %>" if upstream
50
+ [*options[:exclude]].each do |exclude|
51
+ hash.delete(exclude.to_s)
52
+ end
53
+ [*options[:include]].each do |inc|
54
+ hash[inc.to_s] = send(:inc)
55
+ end
56
+ hash.reject { |_, value| value.nil? }
57
+ end
58
+
59
+ def modified_existing?
60
+ return false unless new?
61
+
62
+ # Find routes of the same name
63
+ same_target_and_upstream = self.class.where(:target, target).and(:upstream, upstream)
64
+
65
+ existing = same_target_and_upstream.size == 1 ? same_target_and_upstream.first : nil
66
+
67
+ if existing
68
+ @entity['id'] = existing.id
69
+ save
70
+ else
71
+ false
72
+ end
73
+ end
74
+
75
+ private
76
+
35
77
  def preprocess_target(input)
36
78
  if input.is_a?(URI)
37
79
  "#{input.host}:#{input.port || 8000}"
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'digest'
4
-
5
3
  module SkullIsland
6
4
  module RSpec
7
5
  # A Fake API Client for RSpec testing
@@ -3,7 +3,7 @@
3
3
  module SkullIsland
4
4
  VERSION = [
5
5
  0, # Major
6
- 1, # Minor
7
- 1 # Patch
6
+ 2, # Minor
7
+ 0 # Patch
8
8
  ].join('.')
9
9
  end
data/lib/skull_island.rb CHANGED
@@ -2,11 +2,14 @@
2
2
 
3
3
  # Standard Library Requirements
4
4
  require 'date'
5
+ require 'digest'
5
6
  require 'json'
6
7
  require 'singleton'
7
8
  require 'uri'
9
+ require 'yaml'
8
10
 
9
11
  # External Library Requirements
12
+ require 'erubi'
10
13
  require 'linguistics'
11
14
  Linguistics.use(:en)
12
15
  require 'rest-client'
data/skull_island.gemspec CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
  spec.email = ['jonathan.gnagy@gmail.com']
12
12
 
13
13
  spec.summary = 'Ruby SDK for Kong'
14
- spec.description = 'A Ruby SDK for Kong 1.0.x'
14
+ spec.description = 'A Ruby SDK for Kong 0.14.x'
15
15
  spec.homepage = 'https://github.com/jgnagy/skull_island'
16
16
  spec.license = 'MIT'
17
17
 
@@ -26,9 +26,11 @@ Gem::Specification.new do |spec|
26
26
 
27
27
  spec.required_ruby_version = '~> 2.5'
28
28
 
29
- spec.add_runtime_dependency 'json', '~> 2.0'
29
+ spec.add_runtime_dependency 'erubi', '~> 1.8'
30
+ spec.add_runtime_dependency 'json', '~> 2.1'
30
31
  spec.add_runtime_dependency 'linguistics', '~> 2.1'
31
32
  spec.add_runtime_dependency 'rest-client', '~> 2.0'
33
+ spec.add_runtime_dependency 'thor', '~> 0.20'
32
34
  spec.add_runtime_dependency 'will_paginate', '~> 3.1'
33
35
 
34
36
  spec.add_development_dependency 'bundler', '~> 2.0'
metadata CHANGED
@@ -1,29 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: skull_island
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Gnagy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-03-04 00:00:00.000000000 Z
11
+ date: 2019-03-06 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: erubi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.8'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.8'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: json
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
31
  - - "~>"
18
32
  - !ruby/object:Gem::Version
19
- version: '2.0'
33
+ version: '2.1'
20
34
  type: :runtime
21
35
  prerelease: false
22
36
  version_requirements: !ruby/object:Gem::Requirement
23
37
  requirements:
24
38
  - - "~>"
25
39
  - !ruby/object:Gem::Version
26
- version: '2.0'
40
+ version: '2.1'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: linguistics
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +66,20 @@ dependencies:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
68
  version: '2.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: thor
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.20'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.20'
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: will_paginate
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -164,10 +192,11 @@ dependencies:
164
192
  - - "~>"
165
193
  - !ruby/object:Gem::Version
166
194
  version: '0.9'
167
- description: A Ruby SDK for Kong 1.0.x
195
+ description: A Ruby SDK for Kong 0.14.x
168
196
  email:
169
197
  - jonathan.gnagy@gmail.com
170
- executables: []
198
+ executables:
199
+ - skull_island
171
200
  extensions: []
172
201
  extra_rdoc_files: []
173
202
  files:
@@ -182,11 +211,13 @@ files:
182
211
  - Rakefile
183
212
  - bin/console
184
213
  - bin/setup
214
+ - exe/skull_island
185
215
  - lib/core_extensions/string/transformations.rb
186
216
  - lib/skull_island.rb
187
217
  - lib/skull_island/api_client.rb
188
218
  - lib/skull_island/api_client_base.rb
189
219
  - lib/skull_island/api_exception.rb
220
+ - lib/skull_island/cli.rb
190
221
  - lib/skull_island/exceptions/api_client_not_configured.rb
191
222
  - lib/skull_island/exceptions/immutable_modification.rb
192
223
  - lib/skull_island/exceptions/invalid_arguments.rb
@@ -234,7 +265,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
234
265
  - !ruby/object:Gem::Version
235
266
  version: '0'
236
267
  requirements: []
237
- rubygems_version: 3.0.2
268
+ rubygems_version: 3.0.3
238
269
  signing_key:
239
270
  specification_version: 4
240
271
  summary: Ruby SDK for Kong