cyclone_lariat 0.3.10 → 1.0.0.rc1

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 (68) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/gem-push.yml +4 -4
  3. data/.gitignore +6 -0
  4. data/.rubocop.yml +30 -1
  5. data/CHANGELOG.md +11 -1
  6. data/Gemfile.lock +137 -30
  7. data/Guardfile +42 -0
  8. data/README.md +715 -143
  9. data/Rakefile +2 -5
  10. data/bin/cyclone_lariat +206 -0
  11. data/cyclone_lariat.gemspec +13 -2
  12. data/lib/cyclone_lariat/clients/abstract.rb +40 -0
  13. data/lib/cyclone_lariat/clients/sns.rb +163 -0
  14. data/lib/cyclone_lariat/clients/sqs.rb +114 -0
  15. data/lib/cyclone_lariat/core.rb +21 -0
  16. data/lib/cyclone_lariat/errors.rb +38 -0
  17. data/lib/cyclone_lariat/fake.rb +19 -0
  18. data/lib/cyclone_lariat/generators/command.rb +53 -0
  19. data/lib/cyclone_lariat/generators/event.rb +52 -0
  20. data/lib/cyclone_lariat/generators/queue.rb +30 -0
  21. data/lib/cyclone_lariat/generators/topic.rb +29 -0
  22. data/lib/cyclone_lariat/messages/v1/abstract.rb +139 -0
  23. data/lib/cyclone_lariat/messages/v1/command.rb +20 -0
  24. data/lib/cyclone_lariat/messages/v1/event.rb +20 -0
  25. data/lib/cyclone_lariat/messages/v1/validator.rb +31 -0
  26. data/lib/cyclone_lariat/messages/v2/abstract.rb +149 -0
  27. data/lib/cyclone_lariat/messages/v2/command.rb +20 -0
  28. data/lib/cyclone_lariat/messages/v2/event.rb +20 -0
  29. data/lib/cyclone_lariat/messages/v2/validator.rb +39 -0
  30. data/lib/cyclone_lariat/middleware.rb +9 -5
  31. data/lib/cyclone_lariat/migration.rb +151 -0
  32. data/lib/cyclone_lariat/options.rb +52 -0
  33. data/lib/cyclone_lariat/presenters/graph.rb +54 -0
  34. data/lib/cyclone_lariat/presenters/queues.rb +41 -0
  35. data/lib/cyclone_lariat/presenters/subscriptions.rb +34 -0
  36. data/lib/cyclone_lariat/presenters/topics.rb +40 -0
  37. data/lib/cyclone_lariat/publisher.rb +25 -0
  38. data/lib/cyclone_lariat/repo/active_record/messages.rb +92 -0
  39. data/lib/cyclone_lariat/repo/active_record/versions.rb +28 -0
  40. data/lib/cyclone_lariat/repo/messages.rb +43 -0
  41. data/lib/cyclone_lariat/repo/messages_mapper.rb +49 -0
  42. data/lib/cyclone_lariat/repo/sequel/messages.rb +73 -0
  43. data/lib/cyclone_lariat/repo/sequel/versions.rb +28 -0
  44. data/lib/cyclone_lariat/repo/versions.rb +42 -0
  45. data/lib/cyclone_lariat/resources/queue.rb +167 -0
  46. data/lib/cyclone_lariat/resources/topic.rb +132 -0
  47. data/lib/cyclone_lariat/services/migrate.rb +51 -0
  48. data/lib/cyclone_lariat/services/rollback.rb +51 -0
  49. data/lib/cyclone_lariat/version.rb +1 -1
  50. data/lib/cyclone_lariat.rb +4 -10
  51. data/lib/tasks/console.rake +13 -0
  52. data/lib/tasks/cyclone_lariat.rake +42 -0
  53. data/lib/tasks/db.rake +0 -15
  54. metadata +161 -20
  55. data/config/db.example.rb +0 -9
  56. data/db/migrate/01_add_uuid_extensions.rb +0 -15
  57. data/db/migrate/02_add_events.rb +0 -19
  58. data/docs/_imgs/diagram.png +0 -0
  59. data/docs/_imgs/lariat.jpg +0 -0
  60. data/lib/cyclone_lariat/abstract/client.rb +0 -106
  61. data/lib/cyclone_lariat/abstract/message.rb +0 -83
  62. data/lib/cyclone_lariat/command.rb +0 -13
  63. data/lib/cyclone_lariat/configure.rb +0 -15
  64. data/lib/cyclone_lariat/event.rb +0 -13
  65. data/lib/cyclone_lariat/messages_mapper.rb +0 -46
  66. data/lib/cyclone_lariat/messages_repo.rb +0 -60
  67. data/lib/cyclone_lariat/sns_client.rb +0 -38
  68. data/lib/cyclone_lariat/sqs_client.rb +0 -39
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CycloneLariat
4
+ module Repo
5
+ module Sequel
6
+ class Versions
7
+ attr_reader :dataset
8
+
9
+ def initialize(dataset)
10
+ @dataset = dataset
11
+ end
12
+
13
+ def add(version)
14
+ dataset.insert(version: version)
15
+ true
16
+ end
17
+
18
+ def remove(version)
19
+ dataset.filter(version: version).delete.positive?
20
+ end
21
+
22
+ def all
23
+ dataset.all
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'luna_park/extensions/injector'
5
+ require 'cyclone_lariat/core'
6
+ require 'cyclone_lariat/repo/sequel/versions'
7
+ require 'cyclone_lariat/repo/active_record/versions'
8
+
9
+ module CycloneLariat
10
+ module Repo
11
+ class Versions
12
+ include LunaPark::Extensions::Injector
13
+
14
+ attr_reader :config
15
+
16
+ dependency(:sequel_versions_class) { Repo::Sequel::Versions }
17
+ dependency(:active_record_versions_class) { Repo::ActiveRecord::Versions }
18
+
19
+ extend Forwardable
20
+
21
+ def_delegators :driver, :add, :remove, :all
22
+
23
+ def initialize(**options)
24
+ @config = CycloneLariat::Options.wrap(options).merge!(CycloneLariat.config)
25
+ end
26
+
27
+ def driver
28
+ @driver ||= select(driver: config.driver)
29
+ end
30
+
31
+ private
32
+
33
+ def select(driver:)
34
+ case driver
35
+ when :sequel then sequel_versions_class.new(config.versions_dataset)
36
+ when :active_record then active_record_versions_class.new(config.versions_dataset)
37
+ else raise ArgumentError, "Undefined driver `#{driver}`"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module CycloneLariat
6
+ module Resources
7
+ class Queue
8
+ SNS_SUFFIX = :queue
9
+
10
+ attr_reader :instance, :kind, :region, :dest, :account_id, :publisher, :type, :fifo, :content_based_deduplication
11
+
12
+ def initialize(instance:, kind:, region:, dest:, account_id:, publisher:, type:, fifo:, content_based_deduplication: nil, tags: nil, name: nil)
13
+ @instance = instance
14
+ @kind = kind
15
+ @region = region
16
+ @dest = dest
17
+ @account_id = account_id
18
+ @publisher = publisher
19
+ @type = type
20
+ @fifo = fifo
21
+ @tags = tags
22
+ @name = name
23
+ @content_based_deduplication = content_based_deduplication
24
+ end
25
+
26
+ def arn
27
+ ['arn', 'aws', 'sqs', region, account_id, name].join ':'
28
+ end
29
+
30
+ ##
31
+ # Url example:
32
+ # https://sqs.eu-west-1.amazonaws.com/247606935658/stage-event-queue
33
+ def url
34
+ "https://sqs.#{region}.amazonaws.com/#{account_id}/#{name}"
35
+ end
36
+
37
+ def custom?
38
+ !standard?
39
+ end
40
+
41
+ def standard?
42
+ instance && kind && publisher && type && true
43
+ end
44
+
45
+ def name
46
+ @name ||= begin
47
+ name = [instance, kind, SNS_SUFFIX, publisher, type, dest].compact.join '-'
48
+ name += '.fifo' if fifo
49
+ name
50
+ end
51
+ end
52
+
53
+ alias to_s name
54
+
55
+ def topic?
56
+ false
57
+ end
58
+
59
+ def queue?
60
+ true
61
+ end
62
+
63
+ def protocol
64
+ 'sqs'
65
+ end
66
+
67
+ def attributes
68
+ attrs = {}
69
+ attrs['FifoQueue'] = 'true' if fifo
70
+ attrs['ContentBasedDeduplication'] = 'true' if content_based_deduplication
71
+ attrs
72
+ end
73
+
74
+ class << self
75
+ ##
76
+ # Name example: test-event-queue-cyclone_lariat-note_added.fifo
77
+ # instance: teste
78
+ # kind: event
79
+ # publisher: cyclone_lariat
80
+ # type: note_added
81
+ # dest: nil
82
+ # fifo: true
83
+ def from_name(name, region:, account_id:)
84
+ is_fifo_array = name.split('.')
85
+ full_name = is_fifo_array[0]
86
+ fifo_suffix = is_fifo_array[-1]
87
+ suffix_exists = fifo_suffix != full_name
88
+
89
+ if suffix_exists && fifo_suffix != 'fifo'
90
+ raise ArgumentError, "Queue name #{name} consists unexpected suffix #{fifo_suffix}"
91
+ end
92
+
93
+ fifo = suffix_exists
94
+ queue_array = full_name.split('-')
95
+
96
+ raise ArgumentError, "Topic name should consists `#{SNS_SUFFIX}`" unless queue_array[2] != SNS_SUFFIX
97
+
98
+ queue_array.clear if queue_array.size < 5
99
+
100
+ new(
101
+ instance: queue_array[0], kind: queue_array[1], region: region, dest: queue_array[5],
102
+ account_id: account_id, publisher: queue_array[3], type: queue_array[4], fifo: fifo, name: name
103
+ )
104
+ end
105
+
106
+ ##
107
+ # URL example: https://sqs.eu-west-1.amazonaws.com/247606935658/test-event-queue-cyclone_lariat-note_added.fifo
108
+ # url_array[0] => https
109
+ # host_array[0] => sqs
110
+ # host_array[1] => eu-west-1
111
+ # url_array[3] => 247606935658 # account_id
112
+ # url_array[4] => test-event-queue-cyclone_lariat-note_added.fifo # name
113
+ def from_url(url)
114
+ raise ArgumentError, 'Url is not http format' unless url =~ URI::DEFAULT_PARSER.make_regexp
115
+
116
+ url_array = url.split('/')
117
+ raise ArgumentError, 'Url should start from https' unless url_array[0] == 'https:'
118
+
119
+ host_array = url_array[2].split('.')
120
+ raise ArgumentError, 'It is not queue url' unless host_array[0] == 'sqs'
121
+
122
+ from_name(url_array[4], region: host_array[1], account_id: url_array[3])
123
+ end
124
+
125
+ ##
126
+ # Arn example: "arn:aws:sqs:eu-west-1:247606935658:custom_queue"
127
+ # arn_array[0] => 'arn'
128
+ # arn_array[1] => 'aws'
129
+ # arn_array[2] => 'sqs'
130
+ # arn_array[3] => 'eu-west-1' # region
131
+ # arn_array[4] => '247606935658' # account_id
132
+ # arn_array[5] => 'alexey_test2' # name
133
+ def from_arn(arn)
134
+ arn_array = arn.split(':')
135
+
136
+ raise ArgumentError, "Arn `#{arn}` should consists `arn`" unless arn_array[0] == 'arn'
137
+ raise ArgumentError, "Arn `#{arn}` should consists `aws`" unless arn_array[1] == 'aws'
138
+ raise ArgumentError, "Arn `#{arn}` should consists `sqs`" unless arn_array[2] == 'sqs'
139
+
140
+ from_name(arn_array[5], region: arn_array[3], account_id: arn_array[4])
141
+ end
142
+ end
143
+
144
+ def tags
145
+ @tags ||= begin
146
+ if standard?
147
+ {
148
+ 'standard' => 'true',
149
+ 'instance' => String(instance),
150
+ 'kind' => String(kind),
151
+ 'publisher' => String(publisher),
152
+ 'type' => String(type),
153
+ 'dest' => dest ? String(dest) : 'undefined',
154
+ 'fifo' => fifo ? 'true' : 'false'
155
+ }
156
+ else
157
+ {
158
+ 'standard' => 'false',
159
+ 'name' => String(name),
160
+ 'fifo' => fifo ? 'true' : 'false'
161
+ }
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CycloneLariat
4
+ module Resources
5
+ class Topic
6
+ SNS_SUFFIX = :fanout
7
+
8
+ attr_reader :instance, :kind, :region, :account_id, :publisher, :type, :fifo, :content_based_deduplication
9
+
10
+ def initialize(instance:, kind:, region:, account_id:, publisher:, type:, fifo:, content_based_deduplication: nil, tags: nil, name: nil)
11
+ @instance = instance
12
+ @kind = kind
13
+ @region = region
14
+ @account_id = account_id
15
+ @publisher = publisher
16
+ @type = type
17
+ @fifo = fifo
18
+ @tags = tags
19
+ @name = name
20
+ @content_based_deduplication = content_based_deduplication
21
+ end
22
+
23
+ def arn
24
+ ['arn', 'aws', 'sns', region, account_id, to_s].join ':'
25
+ end
26
+
27
+ def custom?
28
+ !standard?
29
+ end
30
+
31
+ def standard?
32
+ return false unless instance && kind && publisher && type
33
+
34
+ true
35
+ end
36
+
37
+ def name
38
+ @name ||= begin
39
+ name = [instance, kind, SNS_SUFFIX, publisher, type].compact.join '-'
40
+ name += '.fifo' if fifo
41
+ name
42
+ end
43
+ end
44
+
45
+ def attributes
46
+ attrs = {}
47
+ attrs['FifoTopic'] = 'true' if fifo
48
+ attrs['ContentBasedDeduplication'] = 'true' if content_based_deduplication
49
+ attrs
50
+ end
51
+
52
+ def topic?
53
+ true
54
+ end
55
+
56
+ def queue?
57
+ false
58
+ end
59
+
60
+ def protocol
61
+ 'sns'
62
+ end
63
+
64
+ alias to_s name
65
+
66
+ def ==(other)
67
+ arn == other.arn
68
+ end
69
+
70
+ class << self
71
+ def from_name(name, region:, account_id:)
72
+ is_fifo_array = name.split('.')
73
+ full_name = is_fifo_array[0]
74
+ fifo_suffix = is_fifo_array[-1]
75
+ suffix_exists = fifo_suffix != full_name
76
+
77
+ if suffix_exists && fifo_suffix != 'fifo'
78
+ raise ArgumentError, "Topic name #{name} consists unexpected suffix #{fifo_suffix}"
79
+ end
80
+
81
+ fifo = suffix_exists
82
+ topic_array = full_name.split('-')
83
+
84
+ raise ArgumentError, "Topic name should consists `#{SNS_SUFFIX}`" unless topic_array[2] != SNS_SUFFIX
85
+
86
+ topic_array.clear if topic_array.size < 5
87
+
88
+ new(
89
+ instance: topic_array[0],
90
+ kind: topic_array[1],
91
+ publisher: topic_array[3],
92
+ type: topic_array[4],
93
+ region: region,
94
+ account_id: account_id,
95
+ fifo: fifo,
96
+ name: name
97
+ )
98
+ end
99
+
100
+ def from_arn(arn)
101
+ arn_array = arn.split(':')
102
+ raise ArgumentError, 'Arn should consists `arn`' unless arn_array[0] == 'arn'
103
+ raise ArgumentError, 'Arn should consists `aws`' unless arn_array[1] == 'aws'
104
+ raise ArgumentError, 'Arn should consists `sns`' unless arn_array[2] == 'sns'
105
+
106
+ from_name(arn_array[5], region: arn_array[3], account_id: arn_array[4])
107
+ end
108
+ end
109
+
110
+ def tags
111
+ @tags ||= begin
112
+ if standard?
113
+ [
114
+ { key: 'standard', value: 'true' },
115
+ { key: 'instance', value: String(instance) },
116
+ { key: 'kind', value: String(kind) },
117
+ { key: 'publisher', value: String(publisher) },
118
+ { key: 'type', value: String(type) },
119
+ { key: 'fifo', value: fifo ? 'true' : 'false' }
120
+ ]
121
+ else
122
+ [
123
+ { key: 'standard', value: 'false' },
124
+ { key: 'name', value: String(name) },
125
+ { key: 'fifo', value: fifo ? 'true' : 'false' }
126
+ ]
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CycloneLariat
4
+ module Services
5
+ class Migrate
6
+ attr_reader :repo, :dir
7
+
8
+ def initialize(repo:, dir:)
9
+ @repo = repo
10
+ @dir = dir
11
+ end
12
+
13
+ def call
14
+ alert('No one migration exists') if !Dir.exist?(dir) || Dir.empty?(dir)
15
+ output = []
16
+
17
+ migration_paths.each do |path|
18
+ filename = File.basename(path, '.rb')
19
+ version, title = filename.split('_', 2)
20
+
21
+ next if existed_migrations.include? version.to_i
22
+
23
+ class_name = title.split('_').collect(&:capitalize).join
24
+ output << "Up - #{version} #{class_name} #{path}"
25
+ require_relative Pathname.new(Dir.pwd) + Pathname.new(path)
26
+ Object.const_get(class_name).new.up
27
+ repo.add(version)
28
+ end
29
+
30
+ output
31
+ end
32
+
33
+ private
34
+
35
+ # Sorted by timestamp
36
+ def migration_paths
37
+ # lariat/migrate/1668161620_many_to_one.rb
38
+ Dir.glob("#{dir}/*.rb").sort_by do |file_path|
39
+ # 1668161620_many_to_one.rb
40
+ file_name = file_path.split('/')[-1]
41
+ # 1668161620
42
+ file_name.split('_').first.to_i
43
+ end
44
+ end
45
+
46
+ def existed_migrations
47
+ @existed_migrations ||= repo.all.map { |row| row[:version] }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CycloneLariat
4
+ module Services
5
+ class Rollback
6
+ attr_reader :repo, :dir
7
+
8
+ def initialize(repo:, dir:)
9
+ @repo = repo
10
+ @dir = dir
11
+ end
12
+
13
+ def call(version = nil)
14
+ version ||= existed_migrations[-1]
15
+ output = []
16
+
17
+ paths_of_downgrades(version).each do |path|
18
+ filename = File.basename(path, '.rb')
19
+ version, title = filename.split('_', 2)
20
+ class_name = title.split('_').collect(&:capitalize).join
21
+ output << "Down - #{version} #{class_name} #{path}"
22
+ require_relative Pathname.new(Dir.pwd) + Pathname.new(path)
23
+ Object.const_get(class_name).new.down
24
+ repo.remove(version)
25
+ end
26
+
27
+ output
28
+ end
29
+
30
+ def existed_migrations
31
+ @existed_migrations ||= repo.all.map { |row| row[:version] }.sort
32
+ end
33
+
34
+ def paths_of_downgrades(version)
35
+ migrations_to_downgrade = existed_migrations.select { |migration| migration >= version }
36
+
37
+ paths = []
38
+ migrations_to_downgrade.each do |migration|
39
+ path = Pathname.new(Dir.pwd) + Pathname.new(dir)
40
+ founded = Dir.glob("#{path}/#{migration}_*.rb")
41
+ raise "Could not found migration: `#{migration}` in #{path}" if founded.empty?
42
+ raise "Found lot of migration: `#{migration}` in #{path}" if founded.size > 1
43
+
44
+ paths += founded
45
+ end
46
+
47
+ paths
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CycloneLariat
4
- VERSION = '0.3.10'
4
+ VERSION = '1.0.0.rc1'
5
5
  end
@@ -1,12 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'cyclone_lariat/configure'
4
- require_relative 'cyclone_lariat/sns_client'
5
- require_relative 'cyclone_lariat/errors'
6
- require_relative 'cyclone_lariat/event'
7
- require_relative 'cyclone_lariat/messages_mapper'
8
- require_relative 'cyclone_lariat/messages_repo'
9
- require_relative 'cyclone_lariat/middleware'
10
- require_relative 'cyclone_lariat/version'
11
-
12
- module CycloneLariat; end
3
+ require 'cyclone_lariat/core'
4
+ require 'cyclone_lariat/middleware'
5
+ require 'cyclone_lariat/migration'
6
+ require 'cyclone_lariat/publisher'
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc 'IRB console with required CycloneLariat'
4
+ task :console do
5
+ # require 'cyclone_lariat'
6
+ require 'cyclone_lariat'
7
+ require 'irb'
8
+ require_relative '../../config/initializers/cyclone_lariat'
9
+ # require_relative(init_file) if File.exists?(init_file)
10
+
11
+ ARGV.clear
12
+ IRB.start
13
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cyclone_lariat'
4
+
5
+ namespace :cyclone_lariat do
6
+ desc 'Migrate topics for SQS/SNS'
7
+ task migrate: :config do
8
+ CycloneLariat::Migration.migrate
9
+ end
10
+
11
+ desc 'Rollback topics for SQS/SNS'
12
+ task :rollback, [:version] => :config do |_, args|
13
+ target_version = args[:version] ? args[:version].to_i : nil
14
+ CycloneLariat::Migration.rollback(target_version)
15
+ end
16
+
17
+ namespace :list do
18
+ desc 'List all topics'
19
+ task topics: :config do
20
+ CycloneLariat::Migration.list_topics
21
+ end
22
+
23
+ desc 'List all queues'
24
+ task queues: :config do
25
+ CycloneLariat::Migration.list_queues
26
+ end
27
+
28
+ desc 'List all subscriptions'
29
+ task subscriptions: :config do
30
+ CycloneLariat::Migration.list_subscriptions
31
+ end
32
+ end
33
+
34
+ desc 'Build graphviz graph for whole system'
35
+ task graph: :config do
36
+ CycloneLariat::Migration.build_graph
37
+ end
38
+
39
+ task :config do
40
+ require_relative '../../config/initializers/cyclone_lariat'
41
+ end
42
+ end
data/lib/tasks/db.rake CHANGED
@@ -22,20 +22,6 @@ namespace :db do
22
22
  puts "Database `#{DB_CONF[:database]}` successfully dropped" if system(cmd)
23
23
  end
24
24
 
25
- desc 'Apply migrations'
26
- task :migrate, [:version] => :config do |_, args|
27
- require 'logger'
28
- require 'sequel/core'
29
-
30
- Sequel.extension :migration
31
- version = args[:version] ? args[:version].to_i : nil
32
- migrations_path = "#{__dir__}/../../db/migrate/"
33
-
34
- Sequel.connect(**DB_CONF, logger: Logger.new($stdout)) do |db|
35
- Sequel::Migrator.run(db, migrations_path, target: version)
36
- end
37
- end
38
-
39
25
  desc 'Database console'
40
26
  task console: :config do
41
27
  cmd = "PGPASSWORD=#{DB_CONF[:password]} psql" \
@@ -50,6 +36,5 @@ namespace :db do
50
36
  task :reset do
51
37
  Rake::Task['db:drop'].invoke
52
38
  Rake::Task['db:create'].invoke
53
- Rake::Task['db:migrate'].invoke
54
39
  end
55
40
  end