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.
- checksums.yaml +5 -5
- data/.github/workflows/gem-push.yml +4 -4
- data/.gitignore +6 -0
- data/.rubocop.yml +30 -1
- data/CHANGELOG.md +11 -1
- data/Gemfile.lock +137 -30
- data/Guardfile +42 -0
- data/README.md +715 -143
- data/Rakefile +2 -5
- data/bin/cyclone_lariat +206 -0
- data/cyclone_lariat.gemspec +13 -2
- data/lib/cyclone_lariat/clients/abstract.rb +40 -0
- data/lib/cyclone_lariat/clients/sns.rb +163 -0
- data/lib/cyclone_lariat/clients/sqs.rb +114 -0
- data/lib/cyclone_lariat/core.rb +21 -0
- data/lib/cyclone_lariat/errors.rb +38 -0
- data/lib/cyclone_lariat/fake.rb +19 -0
- data/lib/cyclone_lariat/generators/command.rb +53 -0
- data/lib/cyclone_lariat/generators/event.rb +52 -0
- data/lib/cyclone_lariat/generators/queue.rb +30 -0
- data/lib/cyclone_lariat/generators/topic.rb +29 -0
- data/lib/cyclone_lariat/messages/v1/abstract.rb +139 -0
- data/lib/cyclone_lariat/messages/v1/command.rb +20 -0
- data/lib/cyclone_lariat/messages/v1/event.rb +20 -0
- data/lib/cyclone_lariat/messages/v1/validator.rb +31 -0
- data/lib/cyclone_lariat/messages/v2/abstract.rb +149 -0
- data/lib/cyclone_lariat/messages/v2/command.rb +20 -0
- data/lib/cyclone_lariat/messages/v2/event.rb +20 -0
- data/lib/cyclone_lariat/messages/v2/validator.rb +39 -0
- data/lib/cyclone_lariat/middleware.rb +9 -5
- data/lib/cyclone_lariat/migration.rb +151 -0
- data/lib/cyclone_lariat/options.rb +52 -0
- data/lib/cyclone_lariat/presenters/graph.rb +54 -0
- data/lib/cyclone_lariat/presenters/queues.rb +41 -0
- data/lib/cyclone_lariat/presenters/subscriptions.rb +34 -0
- data/lib/cyclone_lariat/presenters/topics.rb +40 -0
- data/lib/cyclone_lariat/publisher.rb +25 -0
- data/lib/cyclone_lariat/repo/active_record/messages.rb +92 -0
- data/lib/cyclone_lariat/repo/active_record/versions.rb +28 -0
- data/lib/cyclone_lariat/repo/messages.rb +43 -0
- data/lib/cyclone_lariat/repo/messages_mapper.rb +49 -0
- data/lib/cyclone_lariat/repo/sequel/messages.rb +73 -0
- data/lib/cyclone_lariat/repo/sequel/versions.rb +28 -0
- data/lib/cyclone_lariat/repo/versions.rb +42 -0
- data/lib/cyclone_lariat/resources/queue.rb +167 -0
- data/lib/cyclone_lariat/resources/topic.rb +132 -0
- data/lib/cyclone_lariat/services/migrate.rb +51 -0
- data/lib/cyclone_lariat/services/rollback.rb +51 -0
- data/lib/cyclone_lariat/version.rb +1 -1
- data/lib/cyclone_lariat.rb +4 -10
- data/lib/tasks/console.rake +13 -0
- data/lib/tasks/cyclone_lariat.rake +42 -0
- data/lib/tasks/db.rake +0 -15
- metadata +161 -20
- data/config/db.example.rb +0 -9
- data/db/migrate/01_add_uuid_extensions.rb +0 -15
- data/db/migrate/02_add_events.rb +0 -19
- data/docs/_imgs/diagram.png +0 -0
- data/docs/_imgs/lariat.jpg +0 -0
- data/lib/cyclone_lariat/abstract/client.rb +0 -106
- data/lib/cyclone_lariat/abstract/message.rb +0 -83
- data/lib/cyclone_lariat/command.rb +0 -13
- data/lib/cyclone_lariat/configure.rb +0 -15
- data/lib/cyclone_lariat/event.rb +0 -13
- data/lib/cyclone_lariat/messages_mapper.rb +0 -46
- data/lib/cyclone_lariat/messages_repo.rb +0 -60
- data/lib/cyclone_lariat/sns_client.rb +0 -38
- data/lib/cyclone_lariat/sqs_client.rb +0 -39
data/Rakefile
CHANGED
@@ -7,9 +7,6 @@ require 'rspec/core/rake_task'
|
|
7
7
|
RSpec::Core::RakeTask.new(:spec)
|
8
8
|
|
9
9
|
# tasks from lib directory
|
10
|
-
|
11
|
-
print "#{entity} : "
|
12
|
-
puts load entity
|
13
|
-
end
|
10
|
+
Rake.add_rakelib 'lib/tasks'
|
14
11
|
|
15
|
-
task default: %i[spec]
|
12
|
+
task default: %i[spec]
|
data/bin/cyclone_lariat
ADDED
@@ -0,0 +1,206 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative '../lib/cyclone_lariat'
|
5
|
+
require 'bundler/setup'
|
6
|
+
require 'dry/cli'
|
7
|
+
require 'fileutils'
|
8
|
+
|
9
|
+
module CycloneLariat
|
10
|
+
module CLI
|
11
|
+
module Commands
|
12
|
+
extend Dry::CLI::Registry
|
13
|
+
|
14
|
+
INITIALIZERS_DIR = './config/initializers'
|
15
|
+
RAKE_TASKS_DIR = './lib/tasks'
|
16
|
+
|
17
|
+
class Version < Dry::CLI::Command
|
18
|
+
desc 'Print version'
|
19
|
+
|
20
|
+
def call(*)
|
21
|
+
puts CycloneLariat::VERSION
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Install < Dry::CLI::Command
|
26
|
+
desc 'Install cyclone lariat to current directory'
|
27
|
+
option :adapter,
|
28
|
+
default: 'sequel',
|
29
|
+
values: %w[sequel active_record],
|
30
|
+
desc: 'adapter for store events and versions'
|
31
|
+
|
32
|
+
def call(adapter: 'sequel', **)
|
33
|
+
create_config(adapter)
|
34
|
+
create_rake_task
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_config(adapter)
|
38
|
+
FileUtils.mkdir_p INITIALIZERS_DIR unless Dir.exist? INITIALIZERS_DIR
|
39
|
+
config_path = "#{INITIALIZERS_DIR}/cyclone_lariat.rb"
|
40
|
+
config_file = File.open(config_path, 'w')
|
41
|
+
config_file.puts config_contents(adapter)
|
42
|
+
puts "Created config: #{config_path}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def create_rake_task
|
46
|
+
FileUtils.mkdir_p RAKE_TASKS_DIR unless Dir.exist? RAKE_TASKS_DIR
|
47
|
+
config_path = "#{RAKE_TASKS_DIR}/cyclone_lariat.rake"
|
48
|
+
config_file = File.open(config_path, 'w')
|
49
|
+
config_file.puts rake_task_context
|
50
|
+
puts "Created rake task: #{config_path}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def config_contents(adapter)
|
54
|
+
return config_active_record_contents if adapter == 'active_record'
|
55
|
+
return config_sequel_contents if adapter == 'sequel'
|
56
|
+
|
57
|
+
raise ArgumentError, "Unknown adapter #{adapter}"
|
58
|
+
end
|
59
|
+
|
60
|
+
def config_sequel_contents
|
61
|
+
<<~CONFIG
|
62
|
+
# frozen_string_literal: true
|
63
|
+
|
64
|
+
CycloneLariat.configure do |c|
|
65
|
+
c.version = 1 # messages version
|
66
|
+
c.aws_key = ENV['AWS_KEY'] # aws key
|
67
|
+
c.aws_account_id = ENV['AWS_ACCOUNT_ID'] # aws account id
|
68
|
+
c.aws_secret_key = ENV['AWS_SECRET_KEY'] # aws secret
|
69
|
+
c.aws_region = ENV['AWS_REGION'] # aws default region
|
70
|
+
c.publisher = ENV['APP_NAME'] # name of your publishers, usually name of your application
|
71
|
+
c.instance = ENV['INSTANCE'] # stage, production, test
|
72
|
+
c.driver = :sequel # :sequel or :active_record
|
73
|
+
c.messages_dataset = DB[:messages] # Sequel dataset / ActiveRecord model for store income messages (on receiver)
|
74
|
+
c.versions_dataset = DB[:lariat_versions] # Sequel dataset / ActiveRecord model for publisher migrations
|
75
|
+
c.fake_publish = ENV['INSTANCE'] == 'test' # when true, prevents messages from being published
|
76
|
+
end
|
77
|
+
CONFIG
|
78
|
+
end
|
79
|
+
|
80
|
+
def config_active_record_contents
|
81
|
+
<<~CONFIG
|
82
|
+
# frozen_string_literal: true
|
83
|
+
|
84
|
+
CycloneLariat.configure do |c|
|
85
|
+
c.version = 1 # messages version
|
86
|
+
c.aws_key = ENV['AWS_KEY'] # aws key
|
87
|
+
c.aws_account_id = ENV['AWS_ACCOUNT_ID'] # aws account id
|
88
|
+
c.aws_secret_key = ENV['AWS_SECRET_KEY'] # aws secret
|
89
|
+
c.aws_region = ENV['AWS_REGION'] # aws default region
|
90
|
+
c.publisher = ENV['APP_NAME'] # name of your publishers, usually name of your application
|
91
|
+
c.instance = ENV['INSTANCE'] # stage, production, test
|
92
|
+
c.driver = :active_record # :sequel or :active_record
|
93
|
+
c.messages_dataset = CycloneLariatMessage # Sequel dataset / ActiveRecord model for store income messages (on receiver)
|
94
|
+
c.versions_dataset = CycloneLariatVersion # Sequel dataset / ActiveRecord model for publisher migrations
|
95
|
+
c.fake_publish = ENV['INSTANCE'] == 'test' # when true, prevents messages from being published
|
96
|
+
end
|
97
|
+
CONFIG
|
98
|
+
end
|
99
|
+
|
100
|
+
def rake_task_context
|
101
|
+
<<~TASKS
|
102
|
+
# frozen_string_literal: true
|
103
|
+
|
104
|
+
require 'cyclone_lariat'
|
105
|
+
|
106
|
+
namespace :cyclone_lariat do
|
107
|
+
desc 'Migrate topics for SQS/SNS'
|
108
|
+
task migrate: :cyclone_lariat_config do
|
109
|
+
CycloneLariat::Migration.migrate
|
110
|
+
end
|
111
|
+
|
112
|
+
desc 'Rollback topics for SQS/SNS'
|
113
|
+
task :rollback, [:version] => :cyclone_lariat_config do |_, args|
|
114
|
+
target_version = args[:version] ? args[:version].to_i : nil
|
115
|
+
CycloneLariat::Migration.rollback(target_version)
|
116
|
+
end
|
117
|
+
|
118
|
+
namespace :list do
|
119
|
+
desc 'List all topics'
|
120
|
+
task topics: :cyclone_lariat_config do
|
121
|
+
CycloneLariat::Migration.list_topics
|
122
|
+
end
|
123
|
+
|
124
|
+
desc 'List all queues'
|
125
|
+
task queues: :cyclone_lariat_config do
|
126
|
+
CycloneLariat::Migration.list_queues
|
127
|
+
end
|
128
|
+
|
129
|
+
desc 'List all subscriptions'
|
130
|
+
task subscriptions: :cyclone_lariat_config do
|
131
|
+
CycloneLariat::Migration.list_subscriptions
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
desc 'Build graphviz graph for whole system'
|
136
|
+
task graph: :cyclone_lariat_config do
|
137
|
+
CycloneLariat::Migration.build_graph
|
138
|
+
end
|
139
|
+
|
140
|
+
task :cyclone_lariat_config do
|
141
|
+
require_relative '../../config/initializers/cyclone_lariat'
|
142
|
+
end
|
143
|
+
end
|
144
|
+
TASKS
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
module Generate
|
149
|
+
class Migration < Dry::CLI::Command
|
150
|
+
desc 'Generate migration'
|
151
|
+
|
152
|
+
argument :title, type: :string, required: true, desc: 'Title of migration use only a-z and _'
|
153
|
+
|
154
|
+
def call(title:, **)
|
155
|
+
abort('Use only a-z and _ in your title') unless title_correct? title
|
156
|
+
|
157
|
+
FileUtils.mkdir_p CycloneLariat::Migration::DIR unless Dir.exist? CycloneLariat::Migration::DIR
|
158
|
+
|
159
|
+
file_name = generate_filename(title)
|
160
|
+
class_name = generate_class_name(title)
|
161
|
+
|
162
|
+
file = File.open(file_name, 'w')
|
163
|
+
file.puts(file_contents(class_name))
|
164
|
+
puts "Migration successful created:\n\t#{file_name}"
|
165
|
+
end
|
166
|
+
|
167
|
+
private
|
168
|
+
|
169
|
+
def title_correct?(title)
|
170
|
+
/^(?!.*__.*)[a-z]?[a-z_]+[a-z]+$/.match? title
|
171
|
+
end
|
172
|
+
|
173
|
+
def generate_filename(title)
|
174
|
+
"#{CycloneLariat::Migration::DIR}/#{Time.now.to_i}_#{title}.rb"
|
175
|
+
end
|
176
|
+
|
177
|
+
def generate_class_name(title)
|
178
|
+
title.split('_').collect(&:capitalize).join
|
179
|
+
end
|
180
|
+
|
181
|
+
def file_contents(klass_name)
|
182
|
+
<<~MIGRATION
|
183
|
+
# frozen_string_literal: true
|
184
|
+
|
185
|
+
class #{klass_name} < CycloneLariat::Migration
|
186
|
+
def up
|
187
|
+
end
|
188
|
+
|
189
|
+
def down
|
190
|
+
end
|
191
|
+
end
|
192
|
+
MIGRATION
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
register 'version', Version, aliases: %w[v -v --version]
|
198
|
+
register 'install', Install
|
199
|
+
register 'generate', aliases: %w[g] do |prefix|
|
200
|
+
prefix.register 'migration', Generate::Migration
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
Dry::CLI.new(CycloneLariat::CLI::Commands).call
|
data/cyclone_lariat.gemspec
CHANGED
@@ -7,7 +7,7 @@ require 'cyclone_lariat/version'
|
|
7
7
|
Gem::Specification.new do |spec|
|
8
8
|
spec.name = 'cyclone_lariat'
|
9
9
|
spec.version = CycloneLariat::VERSION
|
10
|
-
spec.authors = ['Alexander Kudrin', 'Philip Sorokin']
|
10
|
+
spec.authors = ['Alexander Kudrin', 'Philip Sorokin', 'Kirill Drozdov', 'Vitaly Perminov']
|
11
11
|
spec.email = ['kudrin.alexander@gmail.com']
|
12
12
|
|
13
13
|
spec.summary = 'Shoryuken middleware for LunaPark based application.'
|
@@ -28,17 +28,28 @@ Gem::Specification.new do |spec|
|
|
28
28
|
# Specify which files should be added to the gem when it is released.
|
29
29
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
30
30
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
31
|
-
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
31
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|docs|examples|config)/}) }
|
32
32
|
end
|
33
|
+
|
33
34
|
spec.require_paths = ['lib']
|
35
|
+
spec.bindir = 'bin'
|
36
|
+
spec.executables = ['cyclone_lariat']
|
34
37
|
|
35
38
|
spec.add_dependency 'aws-sdk-sns'
|
36
39
|
spec.add_dependency 'aws-sdk-sqs'
|
40
|
+
spec.add_dependency 'dry-cli', '~> 0.6'
|
41
|
+
spec.add_dependency 'dry-validation', '~> 1.5'
|
37
42
|
spec.add_dependency 'luna_park', '~> 0.11'
|
43
|
+
spec.add_dependency 'terminal-table', '~> 3.0'
|
38
44
|
|
39
45
|
spec.add_development_dependency 'bundler', '~> 1.17'
|
40
46
|
spec.add_development_dependency 'byebug', '~> 11.1'
|
47
|
+
spec.add_development_dependency 'database_cleaner-active_record'
|
41
48
|
spec.add_development_dependency 'database_cleaner-sequel', '~> 2.0'
|
49
|
+
spec.add_development_dependency 'guard'
|
50
|
+
spec.add_development_dependency 'guard-bundler'
|
51
|
+
spec.add_development_dependency 'guard-rspec'
|
52
|
+
spec.add_development_dependency 'guard-rubocop'
|
42
53
|
spec.add_development_dependency 'pg', '~> 1.2'
|
43
54
|
spec.add_development_dependency 'pry', '~> 0.13'
|
44
55
|
spec.add_development_dependency 'pry-byebug', '~> 3.9'
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'luna_park/extensions/injector'
|
4
|
+
require 'cyclone_lariat/generators/event'
|
5
|
+
require 'cyclone_lariat/generators/command'
|
6
|
+
require 'cyclone_lariat/errors'
|
7
|
+
require 'cyclone_lariat/core'
|
8
|
+
|
9
|
+
module CycloneLariat
|
10
|
+
module Clients
|
11
|
+
class Abstract
|
12
|
+
include LunaPark::Extensions::Injector
|
13
|
+
include Generators::Event
|
14
|
+
include Generators::Command
|
15
|
+
|
16
|
+
dependency(:aws_client_class) { raise ArgumentError, 'Client class should be defined' }
|
17
|
+
dependency(:aws_credentials_class) { Aws::Credentials }
|
18
|
+
|
19
|
+
def initialize(**options)
|
20
|
+
@config = CycloneLariat::Options.wrap(options).merge!(CycloneLariat.config)
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :config
|
24
|
+
|
25
|
+
def publish
|
26
|
+
raise LunaPark::Errors::AbstractMethod, 'Publish method should be defined'
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def aws_client
|
32
|
+
@aws_client ||= aws_client_class.new(credentials: aws_credentials, region: config.aws_region)
|
33
|
+
end
|
34
|
+
|
35
|
+
def aws_credentials
|
36
|
+
@aws_credentials ||= aws_credentials_class.new(config.aws_key, config.aws_secret_key)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'aws-sdk-sns'
|
4
|
+
require 'cyclone_lariat/fake'
|
5
|
+
require 'cyclone_lariat/clients/abstract'
|
6
|
+
require 'cyclone_lariat/resources/topic'
|
7
|
+
require 'cyclone_lariat/resources/queue'
|
8
|
+
|
9
|
+
module CycloneLariat
|
10
|
+
module Clients
|
11
|
+
class Sns < Abstract
|
12
|
+
include LunaPark::Extensions::Injector
|
13
|
+
include Generators::Topic
|
14
|
+
|
15
|
+
dependency(:aws_client_class) { Aws::SNS::Client }
|
16
|
+
|
17
|
+
def publish(msg, fifo:, topic: nil, skip_validation: false)
|
18
|
+
return Fake.sns_publish_response(msg) if config.fake_publish
|
19
|
+
|
20
|
+
topic = topic ? custom_topic(topic) : topic(msg.type, kind: msg.kind, fifo: fifo)
|
21
|
+
|
22
|
+
raise Errors::GroupIdUndefined.new(resource: topic) if fifo && msg.group_id.nil?
|
23
|
+
raise Errors::GroupDefined.new(resource: topic) if !fifo && msg.group_id
|
24
|
+
raise Errors::DeduplicationIdDefined.new(resource: topic) if !fifo && msg.deduplication_id
|
25
|
+
|
26
|
+
msg.validation.check! unless skip_validation
|
27
|
+
|
28
|
+
params = {
|
29
|
+
topic_arn: topic.arn,
|
30
|
+
message: msg.to_json,
|
31
|
+
message_group_id: msg.group_id,
|
32
|
+
message_deduplication_id: msg.deduplication_id
|
33
|
+
}.compact
|
34
|
+
|
35
|
+
aws_client.publish(**params)
|
36
|
+
end
|
37
|
+
|
38
|
+
def exists?(topic)
|
39
|
+
raise ArgumentError, 'Should be Topic' unless topic.is_a? Resources::Topic
|
40
|
+
|
41
|
+
aws_client.get_topic_attributes({ topic_arn: topic.arn }) && true
|
42
|
+
rescue Aws::SNS::Errors::NotFound
|
43
|
+
false
|
44
|
+
end
|
45
|
+
|
46
|
+
def publish_event(type, fifo:, topic: nil, **options)
|
47
|
+
options[:version] ||= config.version
|
48
|
+
options[:data] ||= {}
|
49
|
+
options[:uuid] ||= SecureRandom.uuid
|
50
|
+
|
51
|
+
publish event(type, **options), fifo: fifo, topic: topic
|
52
|
+
end
|
53
|
+
|
54
|
+
def publish_command(type, fifo:, topic: nil, **options)
|
55
|
+
options[:version] ||= config.version
|
56
|
+
options[:data] ||= {}
|
57
|
+
options[:uuid] ||= SecureRandom.uuid
|
58
|
+
|
59
|
+
publish command(type, **options), fifo: fifo, topic: topic
|
60
|
+
end
|
61
|
+
|
62
|
+
def create(topic)
|
63
|
+
raise ArgumentError, 'Should be Resources::Topic' unless topic.is_a? Resources::Topic
|
64
|
+
raise Errors::TopicAlreadyExists.new(expected_topic: topic.name) if exists?(topic)
|
65
|
+
|
66
|
+
aws_client.create_topic(name: topic.name, attributes: topic.attributes, tags: topic.tags)
|
67
|
+
topic
|
68
|
+
end
|
69
|
+
|
70
|
+
def delete(topic)
|
71
|
+
raise ArgumentError, 'Should be Resources::Topic' unless topic.is_a? Resources::Topic
|
72
|
+
raise Errors::TopicDoesNotExists.new(expected_topic: topic.name) unless exists?(topic)
|
73
|
+
|
74
|
+
aws_client.delete_topic topic_arn: topic.arn
|
75
|
+
topic
|
76
|
+
end
|
77
|
+
|
78
|
+
def subscribe(topic:, endpoint:)
|
79
|
+
subscription_arn = find_subscription_arn(topic: topic, endpoint: endpoint)
|
80
|
+
raise Errors::SubscriptionAlreadyExists.new(topic: topic, endpoint: endpoint) if subscription_arn
|
81
|
+
|
82
|
+
aws_client.subscribe(
|
83
|
+
{
|
84
|
+
topic_arn: topic.arn,
|
85
|
+
protocol: endpoint.protocol,
|
86
|
+
endpoint: endpoint.arn
|
87
|
+
}
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
def unsubscribe(topic:, endpoint:)
|
92
|
+
subscription_arn = find_subscription_arn(topic: topic, endpoint: endpoint)
|
93
|
+
raise Errors::SubscriptionDoesNotExists.new(topic: topic, endpoint: endpoint) unless subscription_arn
|
94
|
+
|
95
|
+
aws_client.unsubscribe(subscription_arn: subscription_arn)
|
96
|
+
end
|
97
|
+
|
98
|
+
def list_all
|
99
|
+
topics = []
|
100
|
+
resp = aws_client.list_topics
|
101
|
+
|
102
|
+
loop do
|
103
|
+
resp[:topics].map do |t|
|
104
|
+
topics << Resources::Topic.from_arn(t[:topic_arn])
|
105
|
+
end
|
106
|
+
|
107
|
+
break if resp[:next_token].nil?
|
108
|
+
|
109
|
+
resp = aws_client.list_topics(next_token: resp[:next_token])
|
110
|
+
end
|
111
|
+
topics
|
112
|
+
end
|
113
|
+
|
114
|
+
def list_subscriptions
|
115
|
+
subscriptions = []
|
116
|
+
resp = aws_client.list_subscriptions
|
117
|
+
|
118
|
+
loop do
|
119
|
+
resp[:subscriptions].each do |s|
|
120
|
+
endpoint = s.endpoint.split(':')[2] == 'sqs' ? Resources::Queue.from_arn(s.endpoint) : Resources::Topic.from_arn(s.endpoint)
|
121
|
+
subscriptions << { topic: Resources::Topic.from_arn(s.topic_arn), endpoint: endpoint, arn: s.subscription_arn }
|
122
|
+
end
|
123
|
+
|
124
|
+
break if resp[:next_token].nil?
|
125
|
+
|
126
|
+
resp = aws_client.list_subscriptions(next_token: resp[:next_token])
|
127
|
+
end
|
128
|
+
subscriptions
|
129
|
+
end
|
130
|
+
|
131
|
+
def topic_subscriptions(topic)
|
132
|
+
raise ArgumentError, 'Should be Topic' unless topic.is_a? Resources::Topic
|
133
|
+
|
134
|
+
subscriptions = []
|
135
|
+
|
136
|
+
resp = aws_client.list_subscriptions_by_topic(topic_arn: topic.arn)
|
137
|
+
|
138
|
+
loop do
|
139
|
+
next_token = resp[:next_token]
|
140
|
+
subscriptions += resp[:subscriptions]
|
141
|
+
|
142
|
+
break if next_token.nil?
|
143
|
+
|
144
|
+
resp = aws_client.list_subscriptions_by_topic(topic_arn: topic.arn, next_token: next_token)
|
145
|
+
end
|
146
|
+
subscriptions
|
147
|
+
end
|
148
|
+
|
149
|
+
def find_subscription_arn(topic:, endpoint:)
|
150
|
+
raise ArgumentError, 'Should be Topic' unless topic.is_a? Resources::Topic
|
151
|
+
unless [Resources::Topic, Resources::Queue].include? endpoint.class
|
152
|
+
raise ArgumentError, 'Endpoint should be Topic or Queue'
|
153
|
+
end
|
154
|
+
|
155
|
+
found_subscription = topic_subscriptions(topic).select do |subscription|
|
156
|
+
subscription.endpoint == endpoint.arn
|
157
|
+
end.first
|
158
|
+
|
159
|
+
found_subscription ? found_subscription.subscription_arn : nil
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'aws-sdk-sqs'
|
4
|
+
require 'cyclone_lariat/fake'
|
5
|
+
require 'cyclone_lariat/clients/abstract'
|
6
|
+
require 'cyclone_lariat/resources/queue'
|
7
|
+
require 'cyclone_lariat/generators/queue'
|
8
|
+
|
9
|
+
module CycloneLariat
|
10
|
+
module Clients
|
11
|
+
class Sqs < Abstract
|
12
|
+
include LunaPark::Extensions::Injector
|
13
|
+
include Generators::Queue
|
14
|
+
|
15
|
+
dependency(:aws_client_class) { Aws::SQS::Client }
|
16
|
+
|
17
|
+
def exists?(queue)
|
18
|
+
raise ArgumentError, 'Should be queue' unless queue.is_a? Resources::Queue
|
19
|
+
|
20
|
+
aws_client.get_queue_url(queue_name: queue.to_s) && true
|
21
|
+
rescue Aws::SQS::Errors::NonExistentQueue
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_policy(queue:, policy:)
|
26
|
+
current_policy_json = aws_client.get_queue_attributes({
|
27
|
+
queue_url: queue.url,
|
28
|
+
attribute_names: ['Policy']
|
29
|
+
}).attributes['Policy']
|
30
|
+
|
31
|
+
current_policy = JSON.parse(current_policy_json) if current_policy_json
|
32
|
+
|
33
|
+
return if current_policy && current_policy['Statement'].find { |s| s['Sid'] == policy['Sid'] }
|
34
|
+
|
35
|
+
new_policy = current_policy || { 'Statement' => [] }
|
36
|
+
new_policy['Statement'] << policy
|
37
|
+
|
38
|
+
aws_client.set_queue_attributes({ queue_url: queue.url, attributes: { 'Policy' => new_policy.to_json } })
|
39
|
+
end
|
40
|
+
|
41
|
+
def publish(msg, fifo:, dest: nil, queue: nil, skip_validation: false)
|
42
|
+
return Fake.sqs_send_message_result(msg) if config.fake_publish
|
43
|
+
|
44
|
+
queue = queue ? custom_queue(queue) : queue(msg.type, kind: msg.kind, fifo: fifo, dest: dest)
|
45
|
+
|
46
|
+
raise Errors::GroupIdUndefined.new(resource: queue) if fifo && msg.group_id.nil?
|
47
|
+
raise Errors::GroupDefined.new(resource: queue) if !fifo && msg.group_id
|
48
|
+
raise Errors::DeduplicationIdDefined.new(resource: queue) if !fifo && msg.deduplication_id
|
49
|
+
|
50
|
+
msg.validation.check! unless skip_validation
|
51
|
+
|
52
|
+
params = {
|
53
|
+
queue_url: queue.url,
|
54
|
+
message_body: msg.to_json,
|
55
|
+
message_group_id: msg.group_id,
|
56
|
+
message_deduplication_id: msg.deduplication_id
|
57
|
+
}.compact
|
58
|
+
|
59
|
+
aws_client.send_message(**params)
|
60
|
+
end
|
61
|
+
|
62
|
+
def publish_event(type, fifo:, dest: nil, queue: nil, **options)
|
63
|
+
options[:version] ||= self.config.version
|
64
|
+
options[:data] ||= {}
|
65
|
+
options[:uuid] ||= SecureRandom.uuid
|
66
|
+
|
67
|
+
publish event(type, data: data, **options), fifo: fifo, dest: dest, queue: queue
|
68
|
+
end
|
69
|
+
|
70
|
+
def publish_command(type, fifo:, dest: nil, queue: nil, **options)
|
71
|
+
options[:version] ||= self.config.version
|
72
|
+
options[:data] ||= {}
|
73
|
+
options[:uuid] ||= SecureRandom.uuid
|
74
|
+
|
75
|
+
publish event(type, data: data, **options), fifo: fifo, dest: dest, queue: queue
|
76
|
+
end
|
77
|
+
|
78
|
+
def create(queue)
|
79
|
+
raise ArgumentError, 'Should be queue' unless queue.is_a? Resources::Queue
|
80
|
+
raise Errors::QueueAlreadyExists.new(expected_queue: queue.name) if exists?(queue)
|
81
|
+
|
82
|
+
aws_client.create_queue(queue_name: queue.name, attributes: queue.attributes, tags: queue.tags)
|
83
|
+
queue
|
84
|
+
end
|
85
|
+
|
86
|
+
def delete(queue)
|
87
|
+
raise ArgumentError, 'Should be queue' unless queue.is_a? Resources::Queue
|
88
|
+
raise Errors::QueueDoesNotExists.new(expected_queue: queue.name) unless exists?(queue)
|
89
|
+
|
90
|
+
aws_client.delete_queue queue_url: queue.url
|
91
|
+
queue
|
92
|
+
end
|
93
|
+
|
94
|
+
def list_all
|
95
|
+
queues = []
|
96
|
+
resp = aws_client.list_queues
|
97
|
+
|
98
|
+
loop do
|
99
|
+
next_token = resp[:next_token]
|
100
|
+
|
101
|
+
resp[:queue_urls].map do |url|
|
102
|
+
queues << Resources::Queue.from_url(url)
|
103
|
+
end
|
104
|
+
|
105
|
+
break if next_token.nil?
|
106
|
+
|
107
|
+
resp = aws_client.list_queues(next_token: next_token)
|
108
|
+
end
|
109
|
+
|
110
|
+
queues
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cyclone_lariat/generators/queue'
|
4
|
+
require 'cyclone_lariat/generators/topic'
|
5
|
+
require 'cyclone_lariat/options'
|
6
|
+
|
7
|
+
module CycloneLariat
|
8
|
+
module CycloneLariatMethods
|
9
|
+
def config
|
10
|
+
@config ||= Options.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def configure
|
14
|
+
yield(config)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
extend Generators::Topic
|
19
|
+
extend Generators::Queue
|
20
|
+
extend CycloneLariatMethods
|
21
|
+
end
|
@@ -18,5 +18,43 @@ module CycloneLariat
|
|
18
18
|
other.details == details
|
19
19
|
end
|
20
20
|
end
|
21
|
+
|
22
|
+
class TopicAlreadyExists < LunaPark::Errors::System
|
23
|
+
message { |d| "Topic already exists: `#{d[:expected_topic]}`" }
|
24
|
+
end
|
25
|
+
|
26
|
+
class TopicDoesNotExists < LunaPark::Errors::System
|
27
|
+
message { |d| "Topic does not exists: `#{d[:expected_topic]}`" }
|
28
|
+
end
|
29
|
+
|
30
|
+
class QueueAlreadyExists < LunaPark::Errors::System
|
31
|
+
message { |d| "Queue already exists: `#{d[:expected_queue]}`" }
|
32
|
+
end
|
33
|
+
|
34
|
+
class QueueDoesNotExists < LunaPark::Errors::System
|
35
|
+
message { |d| "Queue does not exists: `#{d[:expected_queue]}`" }
|
36
|
+
end
|
37
|
+
class SubscriptionAlreadyExists < LunaPark::Errors::System
|
38
|
+
message { |d| "Subscription for topic `#{d[:topic].name}`, on endpoint `#{d[:endpoint].name}` already exists" }
|
39
|
+
end
|
40
|
+
class SubscriptionDoesNotExists < LunaPark::Errors::System
|
41
|
+
message { |d| "Subscription for topic `#{d[:topic].name}`, on endpoint `#{d[:endpoint].name}` does not exists" }
|
42
|
+
end
|
43
|
+
|
44
|
+
class InvalidMessage < LunaPark::Errors::Business
|
45
|
+
message 'Message is not valid'
|
46
|
+
end
|
47
|
+
|
48
|
+
class GroupIdUndefined < LunaPark::Errors::System
|
49
|
+
message { |d| "Group id must be defined for FIFO resources: `#{d[:resource].name}`" }
|
50
|
+
end
|
51
|
+
|
52
|
+
class GroupDefined < LunaPark::Errors::System
|
53
|
+
message { |d| "Group id must be nil for non-FIFO resources: `#{d[:resource].name}`" }
|
54
|
+
end
|
55
|
+
|
56
|
+
class DeduplicationIdDefined < LunaPark::Errors::System
|
57
|
+
message { |d| "Deduplication id must be nil for non-FIFO resources: `#{d[:resource].name}`" }
|
58
|
+
end
|
21
59
|
end
|
22
60
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CycloneLariat
|
4
|
+
class Fake
|
5
|
+
def self.sns_publish_response(message)
|
6
|
+
Aws::SNS::Types::PublishResponse.new.tap do |resp|
|
7
|
+
resp.message_id = SecureRandom.uuid
|
8
|
+
resp.sequence_number = rand(10).to_s if message.fifo?
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.sqs_send_message_result(message)
|
13
|
+
Aws::SQS::Types::SendMessageResult.new.tap do |res|
|
14
|
+
res.message_id = SecureRandom.uuid
|
15
|
+
res.sequence_number = rand(10).to_s if message.fifo?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|