kong_schema 1.1.0

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.
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kong_schema'
4
+ require 'tty-table'
5
+ require 'json'
6
+ require 'diffy'
7
+ require 'pastel'
8
+ require_relative './actions'
9
+
10
+ module KongSchema
11
+ # Helper class for printing a report of the changes to be committed to Kong
12
+ # created by {Schema.analyze}.
13
+ module Reporter
14
+ extend self
15
+
16
+ TableHeader = %w(Change Parameters).freeze
17
+
18
+ # @param [Array<KongSchema::Action>] changes
19
+ # What you get from calling {KongSchema::Schema.analyze}
20
+ #
21
+ # @return [String] The report to print to something like STDOUT.
22
+ def report(changes, object_format: :json)
23
+ pretty_print = if object_format == :json
24
+ JSONPrettyPrinter.method(:print)
25
+ else
26
+ YAMLPrettyPrinter.method(:print)
27
+ end
28
+
29
+ table = TTY::Table.new header: TableHeader do |t|
30
+ changes.each do |change|
31
+ t << print_change(change: change, pretty_print: pretty_print)
32
+ end
33
+ end
34
+
35
+ table.render(:ascii, multiline: true, padding: [0, 1, 0, 1])
36
+ end
37
+
38
+ private
39
+
40
+ # Print objects as human-readable JSON
41
+ class JSONPrettyPrinter
42
+ def self.print(object)
43
+ JSON.pretty_generate(YAML.load(YAML.dump(object))) + "\n"
44
+ end
45
+ end
46
+
47
+ # Print objects as YAML
48
+ class YAMLPrettyPrinter
49
+ def self.print(object)
50
+ YAML.dump(object)
51
+ end
52
+ end
53
+
54
+ def print_change(change:, pretty_print:)
55
+ resource_name = change.model.to_s.split('::').last
56
+
57
+ case change
58
+ when KongSchema::Actions::Create
59
+ [ "Create #{resource_name}", pretty_print.call(change.params) ]
60
+ when KongSchema::Actions::Update
61
+ record_attributes = rewrite_record_attributes(change.record)
62
+ current_attributes = change.params.keys.reduce({}) do |map, key|
63
+ map[key] = record_attributes[key]
64
+ map
65
+ end
66
+
67
+ changed_attributes = pretty_print.call(change.params)
68
+ current_attributes = pretty_print.call(current_attributes)
69
+ diff = Diffy::Diff.new(current_attributes, changed_attributes)
70
+
71
+ [ "Update #{resource_name}", diff.to_s(:color) ]
72
+ when KongSchema::Actions::Delete
73
+ [
74
+ "Delete #{resource_name}",
75
+ pretty_print.call(rewrite_record_attributes(change.record))
76
+ ]
77
+ end
78
+ end
79
+
80
+ # This warrants some explanation.
81
+ #
82
+ # For some Kong API objects like Target, the API will accept "indirect"
83
+ # values for certain parameters like "upstream_id" but in the responses for
84
+ # those APIs, the payload will contain a value different than what we POSTed
85
+ # with. In this example, it will accept an upstream_id of either an actual
86
+ # Upstream.id like "c9633f63-fdaf-4c1c-b9dd-b5a0fa28c780" or an
87
+ # Upstream.name like "some-upstream.kong-service".
88
+ #
89
+ # For our purposes, the user doesn't know about Upstream.id, so we don't
90
+ # care to show that value, so we will rewrite it with the value they're
91
+ # meant to input (e.g. target.upstream_id -> target.upstream.name)
92
+ def rewrite_record_attributes(record)
93
+ case record
94
+ when Kong::Target
95
+ record.attributes.merge('upstream_id' => record.upstream.name)
96
+ else
97
+ record.attributes
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kong'
4
+ require_relative '../adapter'
5
+
6
+ module KongSchema
7
+ module Resource
8
+ module Api
9
+ extend self
10
+
11
+ def identify(record)
12
+ case record
13
+ when Kong::Api
14
+ record.name
15
+ when Hash
16
+ record['name']
17
+ end
18
+ end
19
+
20
+ def all(*)
21
+ Kong::Api.all
22
+ end
23
+
24
+ def create(attributes)
25
+ Adapter.for(Kong::Api).create(serialize_outbound(attributes))
26
+ end
27
+
28
+ def changed?(record, attributes)
29
+ Adapter.for(Kong::Api).changed?(record, attributes)
30
+ end
31
+
32
+ def update(record, partial_attributes)
33
+ Adapter.for(Kong::Api).update(
34
+ record,
35
+ serialize_outbound(partial_attributes)
36
+ )
37
+ end
38
+
39
+ def delete(record)
40
+ Adapter.for(Kong::Api).delete(record)
41
+ end
42
+
43
+ def serialize_outbound(attributes)
44
+ attributes.keys.reduce({}) do |map, key|
45
+ case key
46
+ when 'hosts', 'uris', 'methods'
47
+ map[key] = Array(attributes[key]).join(',')
48
+ else
49
+ map[key] = attributes[key]
50
+ end
51
+
52
+ map
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kong'
4
+ require_relative '../adapter'
5
+
6
+ module KongSchema
7
+ module Resource
8
+ module Target
9
+ extend self
10
+
11
+ def identify(record)
12
+ case record
13
+ when Kong::Target
14
+ [record.upstream.name, record.target].to_json
15
+ when Hash
16
+ [record['upstream_id'], record['target']].to_json
17
+ end
18
+ end
19
+
20
+ def all(*)
21
+ Kong::Upstream.all.map(&:targets).flatten
22
+ end
23
+
24
+ def create(attributes)
25
+ with_upstream(attributes) do |upstream|
26
+ Adapter.for(Kong::Target).create(
27
+ attributes.merge('upstream_id' => upstream.id)
28
+ )
29
+ end
30
+ end
31
+
32
+ def changed?(record, directive)
33
+ (
34
+ record.target != directive['target'] ||
35
+ record.weight != directive.fetch('weight', 100) ||
36
+ record.upstream.name != directive['upstream_id']
37
+ )
38
+ end
39
+
40
+ def update(record, partial_attributes)
41
+ delete(record)
42
+ create(partial_attributes)
43
+ end
44
+
45
+ def delete(target)
46
+ Adapter.for(Kong::Target).delete(target)
47
+ end
48
+
49
+ private
50
+
51
+ def with_upstream(params)
52
+ upstream = Kong::Upstream.find_by_name(params.fetch('upstream_id', ''))
53
+
54
+ fail "Can not add a target without an upstream!" if upstream.nil?
55
+
56
+ yield upstream
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kong'
4
+ require_relative '../adapter'
5
+
6
+ module KongSchema
7
+ module Resource
8
+ # https://getkong.org/docs/0.11.x/admin-api/#upstream-objects
9
+ module Upstream
10
+ extend self
11
+
12
+ def identify(record)
13
+ case record
14
+ when Kong::Upstream
15
+ record.name
16
+ when Hash
17
+ record['name']
18
+ end
19
+ end
20
+
21
+ def all(*)
22
+ Kong::Upstream.all
23
+ end
24
+
25
+ def create(attributes)
26
+ Adapter.for(Kong::Upstream).create(attributes)
27
+ end
28
+
29
+ def changed?(record, attributes)
30
+ Adapter.for(Kong::Upstream).changed?(record, attributes)
31
+ end
32
+
33
+ def update(record, partial_attributes)
34
+ Adapter.for(Kong::Upstream).update(record, partial_attributes)
35
+ end
36
+
37
+ def delete(record)
38
+ Adapter.for(Kong::Upstream).delete(record)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,3 @@
1
+ require_relative './resource/api'
2
+ require_relative './resource/target'
3
+ require_relative './resource/upstream'
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'kong'
5
+ require 'English'
6
+
7
+ require_relative './client'
8
+ require_relative './resource'
9
+
10
+ module KongSchema
11
+ module Schema
12
+ extend self
13
+
14
+ # Scan for changes between Kong's database and the configuration. To
15
+ # commit the changes (if any) use {.commit} with the results.
16
+ #
17
+ # @param [Object] config
18
+ # The configuration directives as explain in README.
19
+ #
20
+ # @return [Array<Change>]
21
+ def scan(config)
22
+ Client.connect(config) do
23
+ [
24
+ scan_in(model: Resource::Api, directives: Array(config['apis'])),
25
+
26
+ # order matters in some of the resources; Upstream directives must be
27
+ # handled before Target ones
28
+ scan_in(model: Resource::Upstream, directives: Array(config['upstreams'])),
29
+
30
+ scan_in(model: Resource::Target, directives: Array(config['targets']))
31
+ ].flatten
32
+ end
33
+ end
34
+
35
+ # Commit changes to Kong's database through its REST API.
36
+ #
37
+ # @param [Object] config
38
+ # @param [Array<Change>] changes
39
+ #
40
+ # @return NilClass
41
+ def commit(config, directives)
42
+ Client.connect(config) do |client|
43
+ directives.each do |d|
44
+ begin
45
+ d.apply(client, config)
46
+ rescue StandardError
47
+ e = $ERROR_INFO
48
+ raise e, "#{e}\nSource:\n#{YAML.dump(d)}", e.backtrace
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def scan_in(model:, directives:)
57
+ state = {
58
+ model: model,
59
+ defined: model.all.each_with_object({}) do |record, map|
60
+ map[model.identify(record)] = record
61
+ end,
62
+ declared: directives.each_with_object({}) do |directive, map|
63
+ map[model.identify(directive)] = directive
64
+ end
65
+ }
66
+
67
+ [
68
+ build_create_changes(state),
69
+ build_update_changes(state),
70
+ build_delete_changes(state)
71
+ ].flatten
72
+ end
73
+
74
+ def build_create_changes(model:, defined:, declared:)
75
+ to_create = declared.keys - defined.keys
76
+ to_create.map do |id|
77
+ Actions::Create.new(model: model, params: declared[id])
78
+ end
79
+ end
80
+
81
+ def build_update_changes(model:, defined:, declared:)
82
+ to_update = declared.keys & defined.keys
83
+ changed = to_update.select do |id|
84
+ model.changed?(defined[id], declared[id])
85
+ end
86
+
87
+ changed.map do |id|
88
+ Actions::Update.new(
89
+ model: model,
90
+ record: defined[id],
91
+ params: declared[id]
92
+ )
93
+ end
94
+ end
95
+
96
+ def build_delete_changes(model:, defined:, declared:)
97
+ to_delete = defined.keys - declared.keys
98
+ to_delete.map do |id|
99
+ Actions::Delete.new(model: model, record: defined[id])
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KongSchema
4
+ VERSION = "1.1.0"
5
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../ext/kong/upstream'
4
+ require_relative './kong_schema/actions'
5
+ require_relative './kong_schema/adapter'
6
+ require_relative './kong_schema/cli'
7
+ require_relative './kong_schema/reporter'
8
+ require_relative './kong_schema/resource'
9
+ require_relative './kong_schema/schema'
10
+ require_relative './kong_schema/version'
data/spec/examples.txt ADDED
File without changes
@@ -0,0 +1,89 @@
1
+ describe KongSchema::Reporter do
2
+ subject { described_class }
3
+
4
+ let(:test_utils) { KongSchemaTestUtils.new }
5
+ let(:schema) { KongSchema::Schema }
6
+
7
+ after(:each) do
8
+ test_utils.reset_kong
9
+ end
10
+
11
+ let :config do
12
+ test_utils.generate_config({
13
+ upstreams: [{ name: 'bridge-learn.kong-service' }]
14
+ })
15
+ end
16
+
17
+ let :with_updated_config do
18
+ test_utils.generate_config({
19
+ upstreams: [{
20
+ name: 'bridge-learn.kong-service',
21
+ slots: 50,
22
+ orderlist: nil
23
+ }]
24
+ })
25
+ end
26
+
27
+ let :with_deleted_config do
28
+ test_utils.generate_config({
29
+ upstreams: []
30
+ })
31
+ end
32
+
33
+ it 'reports a resource to be created' do
34
+ report = subject.report(schema.scan(config))
35
+
36
+ expect(report).to include('Create Upstream')
37
+ end
38
+
39
+ it 'reports a resource to be updated [JSON]' do
40
+ schema.commit(config, schema.scan(config))
41
+
42
+ report = subject.report(schema.scan(with_updated_config))
43
+
44
+ expect(report).to include('Update Upstream')
45
+ expect(report).to match(/\-[ ]*"slots": 100/)
46
+ expect(report).to match(/\+[ ]*"slots": 50/)
47
+ end
48
+
49
+ it 'reports a resource to be updated [YAML]' do
50
+ schema.commit(config, schema.scan(config))
51
+
52
+ report = subject.report(schema.scan(with_updated_config), object_format: :yaml)
53
+
54
+ expect(report).to include('Update Upstream')
55
+ expect(report).to include('-slots: 100')
56
+ expect(report).to include('+slots: 50')
57
+ end
58
+
59
+ it 'reports a resource to be deleted' do
60
+ schema.commit(config, schema.scan(config))
61
+
62
+ report = subject.report(schema.scan(with_deleted_config))
63
+
64
+ expect(report).to include('Delete Upstream')
65
+ end
66
+
67
+ describe '.extract_record_attributes' do
68
+ context 'Kong::Target' do
69
+ it 'it rewrites "upstream_id" into the upstream name' do
70
+ with_target = test_utils.generate_config({
71
+ upstreams: [{ name: 'foo' }],
72
+ targets: [{ upstream_id: 'foo', target: '127.0.0.1' }]
73
+ })
74
+
75
+ with_updated_target = test_utils.generate_config({
76
+ upstreams: [{ name: 'foo' }],
77
+ targets: []
78
+ })
79
+
80
+ schema.commit(with_target, schema.scan(with_target))
81
+
82
+ next_changes = schema.scan(with_updated_target)
83
+ report = subject.report(next_changes, object_format: :yml)
84
+
85
+ expect(report).to include('upstream_id: foo')
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,125 @@
1
+ describe KongSchema::Resource::Api do
2
+ let(:schema) { KongSchema::Schema }
3
+ let(:test_utils) { KongSchemaTestUtils.new }
4
+
5
+ after(:each) do
6
+ test_utils.reset_kong
7
+ end
8
+
9
+ describe 'creating APIs' do
10
+ let :config do
11
+ test_utils.generate_config({
12
+ apis: [{
13
+ name: 'bridge-learn',
14
+ hosts: ['bridgeapp.com'],
15
+ upstream_url: 'http://bridge-learn.kong-service'
16
+ }]
17
+ })
18
+ end
19
+
20
+ it 'adds an API if it does not exist' do
21
+ directives = schema.scan(config)
22
+
23
+ expect(directives.map(&:class)).to include(KongSchema::Actions::Create)
24
+ end
25
+
26
+ it 'does add an API' do
27
+ directives = schema.scan(config)
28
+
29
+ expect {
30
+ schema.commit(config, directives)
31
+ }.to change {
32
+ KongSchema::Client.connect(config) { Kong::Api.all.count }
33
+ }.by(1)
34
+ end
35
+
36
+ it 'does not add an API if it exists' do
37
+ directives = schema.scan(config)
38
+
39
+ schema.commit(config, directives)
40
+
41
+ next_directives = schema.scan(config)
42
+
43
+ expect(next_directives.map(&:class)).not_to include(KongSchema::Actions::Create)
44
+ end
45
+ end
46
+
47
+ describe 'updating APIs' do
48
+ let :config do
49
+ test_utils.generate_config({
50
+ apis: [{
51
+ name: 'bridge-learn',
52
+ hosts: ['bridgeapp.com'],
53
+ upstream_url: 'http://bridge-learn.kong-service'
54
+ }]
55
+ })
56
+ end
57
+
58
+ let :with_updated_config do
59
+ test_utils.generate_config({
60
+ apis: [{
61
+ name: 'bridge-learn',
62
+ hosts: ['bar.com'],
63
+ upstream_url: 'http://bridge-learn.kong-service'
64
+ }]
65
+ })
66
+ end
67
+
68
+ before(:each) do
69
+ schema.commit(config, schema.scan(config))
70
+ end
71
+
72
+ it 'updates an API' do
73
+ directives = schema.scan(with_updated_config)
74
+ expect(directives.map(&:class)).to include(KongSchema::Actions::Update)
75
+ end
76
+
77
+ it 'does update an API' do
78
+ directives = schema.scan(with_updated_config)
79
+
80
+ expect {
81
+ schema.commit(with_updated_config, directives)
82
+ }.to change {
83
+ KongSchema::Client.connect(config) { Kong::Api.all[0].hosts[0] }
84
+ }.from('bridgeapp.com').to('bar.com')
85
+ end
86
+ end
87
+
88
+ describe 'deleting APIs' do
89
+ let :config do
90
+ test_utils.generate_config({
91
+ apis: [{
92
+ name: 'bridge-learn',
93
+ hosts: ['bar.com'],
94
+ upstream_url: 'http://bridge-learn.kong-service'
95
+ }]
96
+ })
97
+ end
98
+
99
+ let :with_deleted_config do
100
+ test_utils.generate_config({
101
+ apis: []
102
+ })
103
+ end
104
+
105
+ before(:each) do
106
+ schema.commit(config, schema.scan(config))
107
+ end
108
+
109
+ it 'deletes an API' do
110
+ directives = schema.scan(with_deleted_config)
111
+
112
+ expect(directives.map(&:class)).to include(KongSchema::Actions::Delete)
113
+ end
114
+
115
+ it 'does delete an API' do
116
+ directives = schema.scan(with_deleted_config)
117
+
118
+ expect {
119
+ schema.commit(with_deleted_config, directives)
120
+ }.to change {
121
+ KongSchema::Client.connect(config) { Kong::Api.all.count }
122
+ }.from(1).to(0)
123
+ end
124
+ end
125
+ end