kong_schema 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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