cyclone_lariat 0.3.10 → 0.4.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,174 @@
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
+
28
+ def call(*)
29
+ create_config
30
+ create_rake_task
31
+ end
32
+
33
+ def create_config
34
+ FileUtils.mkdir_p RAKE_TASKS_DIR unless Dir.exist? RAKE_TASKS_DIR
35
+ config_path = "#{RAKE_TASKS_DIR}/cyclone_lariat.rake"
36
+ config_file = File.open(config_path, 'w')
37
+ config_file.puts rake_task_context
38
+ puts "Created rake task: #{config_path}"
39
+ end
40
+
41
+ def create_rake_task
42
+ FileUtils.mkdir_p INITIALIZERS_DIR unless Dir.exist? INITIALIZERS_DIR
43
+ config_path = "#{INITIALIZERS_DIR}/cyclone_lariat.rb"
44
+ config_file = File.open(config_path, 'w')
45
+ config_file.puts config_contents
46
+ puts "Created config: #{config_path}"
47
+ end
48
+
49
+ def config_contents
50
+ <<~CONFIG
51
+ # frozen_string_literal: true
52
+
53
+ CycloneLariat.tap do |cl|
54
+ cl.default_version = 1 # api version
55
+ cl.aws_key = ENV['AWS_KEY'] # aws key
56
+ cl.aws_account_id = ENV['AWS_ACCOUNT_ID'] # aws account id
57
+ cl.aws_secret_key = ENV['AWS_SECRET_KEY'] # aws secret
58
+ cl.aws_default_region = ENV['AWS_REGION'] # aws default region
59
+ cl.publisher = ENV['APP_NAME'] # name of your publishers, usually name of your application
60
+ cl.default_instance = ENV['INSTANCE'] # stage, production, test
61
+ cl.events_dataset = DB[:events] # sequel dataset for store income messages, for receiver
62
+ cl.versions_dataset = DB[:lariat_versions] # sequel dataset for migrations, for publisher
63
+ end
64
+ CONFIG
65
+ end
66
+
67
+ def rake_task_context
68
+ <<~TASKS
69
+ # frozen_string_literal: true
70
+
71
+ require 'cyclone_lariat'
72
+
73
+ namespace :cyclone_lariat do
74
+ desc 'Migrate topics for SQS/SNS'
75
+ task migrate: :config do
76
+ require_relative '../../config/initializers/cyclone_lariat'
77
+ CycloneLariat::Migration.migrate
78
+ end
79
+
80
+ desc 'Rollback topics for SQS/SNS'
81
+ task :rollback, [:version] => :config do |_, args|
82
+ require_relative '../../config/initializers/cyclone_lariat'
83
+ target_version = args[:version] ? args[:version].to_i : nil
84
+ CycloneLariat::Migration.rollback(target_version)
85
+ end
86
+
87
+ namespace :list do
88
+ desc 'List all topics'
89
+ task :topics do
90
+ require_relative '../../config/initializers/cyclone_lariat'
91
+ CycloneLariat::Migration.list_topics
92
+ end
93
+
94
+ desc 'List all queues'
95
+ task :queues do
96
+ require_relative '../../config/initializers/cyclone_lariat'
97
+ CycloneLariat::Migration.list_queues
98
+ end
99
+
100
+ desc 'List all subscriptions'
101
+ task :subscriptions do
102
+ require_relative '../../config/initializers/cyclone_lariat'
103
+ CycloneLariat::Migration.list_subscriptions
104
+ end
105
+ end
106
+
107
+ desc 'Build graphviz graph for whole system'
108
+ task :graph do
109
+ require_relative '../../config/initializers/cyclone_lariat'
110
+ CycloneLariat::Migration.build_graph
111
+ end
112
+ end
113
+ TASKS
114
+ end
115
+ end
116
+
117
+ module Generate
118
+ class Migration < Dry::CLI::Command
119
+ desc 'Generate migration'
120
+
121
+ argument :title, type: :string, required: true, desc: 'Title of migration use only a-z and _'
122
+
123
+ def call(title:, **)
124
+ abort('Use only a-z and _ in your title') unless title_correct? title
125
+
126
+ FileUtils.mkdir_p CycloneLariat::Migration::DIR unless Dir.exist? CycloneLariat::Migration::DIR
127
+
128
+ file_name = generate_filename title
129
+ class_name = generate_class_name title
130
+ file = File.open(file_name, 'w')
131
+ file.puts file_contents(class_name)
132
+ puts "Migration successful created:\n\t#{file_name}"
133
+ end
134
+
135
+ private
136
+
137
+ def title_correct?(title)
138
+ /^(?!.*__.*)[a-z]?[a-z_]+[a-z]+$/.match? title
139
+ end
140
+
141
+ def generate_filename(title)
142
+ "#{CycloneLariat::Migration::DIR}/#{Time.now.to_i}_#{title}.rb"
143
+ end
144
+
145
+ def generate_class_name(title)
146
+ title.split('_').collect(&:capitalize).join
147
+ end
148
+
149
+ def file_contents(klass_name)
150
+ <<~MIGRATION
151
+ # frozen_string_literal: true
152
+
153
+ class #{klass_name} < CycloneLariat::Migration
154
+ def up
155
+ end
156
+
157
+ def down
158
+ end
159
+ end
160
+ MIGRATION
161
+ end
162
+ end
163
+ end
164
+
165
+ register 'version', Version, aliases: %w[v -v --version]
166
+ register 'install', Install
167
+ register 'generate', aliases: %w[g] do |prefix|
168
+ prefix.register 'migration', Generate::Migration
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ Dry::CLI.new(CycloneLariat::CLI::Commands).call
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../db'
4
+
5
+ require 'sequel'
6
+
7
+ DB = Sequel.connect(DB_CONF)
@@ -30,11 +30,15 @@ Gem::Specification.new do |spec|
30
30
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
31
31
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
32
32
  end
33
+
33
34
  spec.require_paths = ['lib']
35
+ spec.executables = ['cyclone_lariat']
34
36
 
35
37
  spec.add_dependency 'aws-sdk-sns'
36
38
  spec.add_dependency 'aws-sdk-sqs'
39
+ spec.add_dependency 'dry-cli', '~> 0.6'
37
40
  spec.add_dependency 'luna_park', '~> 0.11'
41
+ spec.add_dependency 'terminal-table', '~> 3.0'
38
42
 
39
43
  spec.add_development_dependency 'bundler', '~> 1.17'
40
44
  spec.add_development_dependency 'byebug', '~> 11.1'
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ create_table :lariat_versions do
6
+ Integer :version, null: false, unique: true
7
+ end
8
+ end
9
+ end
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -14,36 +14,42 @@ module CycloneLariat
14
14
  dependency(:aws_client_class) { raise ArgumentError, 'Client class should be defined' }
15
15
  dependency(:aws_credentials_class) { Aws::Credentials }
16
16
 
17
- def initialize(key: nil, secret_key: nil, region: nil, version: nil, publisher: nil, instance: nil, client_id: nil)
17
+ def initialize(key: nil, secret_key: nil, region: nil, version: nil, publisher: nil, instance: nil, account_id: nil)
18
18
  @key = key
19
19
  @secret_key = secret_key
20
20
  @region = region
21
21
  @version = version
22
22
  @publisher = publisher
23
23
  @instance = instance
24
- @client_id = client_id
24
+ @account_id = account_id
25
25
  end
26
26
 
27
- def event(type, data: {}, version: self.version, uuid: SecureRandom.uuid)
28
- Event.wrap(
27
+ def event(type, data: {}, version: self.version, request_id: nil, uuid: SecureRandom.uuid)
28
+ params = {
29
29
  uuid: uuid,
30
30
  type: type,
31
- sent_at: Time.now.iso8601,
31
+ sent_at: Time.now.iso8601(3),
32
32
  version: version,
33
33
  publisher: publisher,
34
- data: data
35
- )
34
+ data: data,
35
+ request_id: request_id
36
+ }
37
+
38
+ Event.wrap(params.compact)
36
39
  end
37
40
 
38
- def command(type, data: {}, version: self.version, uuid: SecureRandom.uuid)
39
- Command.wrap(
41
+ def command(type, data: {}, version: self.version, request_id: nil, uuid: SecureRandom.uuid)
42
+ params = {
40
43
  uuid: uuid,
41
44
  type: type,
42
- sent_at: Time.now.iso8601,
45
+ sent_at: Time.now.iso8601(3),
43
46
  version: version,
44
47
  publisher: publisher,
45
- data: data
46
- )
48
+ data: data,
49
+ request_id: request_id
50
+ }
51
+
52
+ Command.wrap(params.compact)
47
53
  end
48
54
 
49
55
  def publish
@@ -88,8 +94,8 @@ module CycloneLariat
88
94
  @region ||= CycloneLariat.aws_default_region
89
95
  end
90
96
 
91
- def client_id
92
- @client_id ||= CycloneLariat.aws_client_id
97
+ def account_id
98
+ @account_id ||= CycloneLariat.aws_account_id
93
99
  end
94
100
 
95
101
  private
@@ -6,10 +6,10 @@ require_relative '../errors'
6
6
  module CycloneLariat
7
7
  module Abstract
8
8
  class Message < LunaPark::Entities::Attributable
9
- attr :uuid, String, :new
10
- attr :publisher, String, :new
11
- attr :type, String, :new
12
- attrs :client_error, :version, :data,
9
+ attr :uuid, String, :new
10
+ attr :publisher, String, :new
11
+ attr :type, String, :new
12
+ attrs :client_error, :version, :data, :request_id,
13
13
  :sent_at, :processed_at, :received_at
14
14
 
15
15
  def kind
@@ -32,6 +32,10 @@ module CycloneLariat
32
32
  @processed_at = wrap_time(value)
33
33
  end
34
34
 
35
+ def request_at=(value)
36
+ @request_id = wrap_string(value)
37
+ end
38
+
35
39
  def processed?
36
40
  !@processed_at.nil?
37
41
  end
@@ -64,7 +68,10 @@ module CycloneLariat
64
68
 
65
69
  def to_json(*args)
66
70
  hash = serialize
67
- hash[:type] = [kind, hash[:type]].join '_'
71
+ hash[:type] = [kind, hash[:type]].join '_'
72
+ hash[:sent_at] = hash[:sent_at].iso8601(3) if hash[:sent_at]
73
+ hash[:received_at] = hash[:received_at].iso8601(3) if hash[:received_at]
74
+ hash[:processed_at] = hash[:processed_at].iso8601(3) if hash[:processed_at]
68
75
  hash.to_json(*args)
69
76
  end
70
77
 
@@ -78,6 +85,14 @@ module CycloneLariat
78
85
  else raise ArgumentError, "Unknown type `#{value.class}`"
79
86
  end
80
87
  end
88
+
89
+ def wrap_string(value)
90
+ case value
91
+ when String then String(value)
92
+ when NilClass then nil
93
+ else raise ArgumentError, "Unknown type `#{value.class}`"
94
+ end
95
+ end
81
96
  end
82
97
  end
83
98
  end
@@ -5,7 +5,7 @@ module CycloneLariat
5
5
  DEFAULT_VERSION = 1
6
6
 
7
7
  attr_accessor :aws_key, :aws_secret_key, :publisher, :aws_default_region, :default_instance,
8
- :aws_client_id
8
+ :aws_account_id, :events_dataset, :versions_dataset
9
9
  attr_writer :default_version
10
10
 
11
11
  def default_version
@@ -18,5 +18,27 @@ 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
21
43
  end
22
44
  end
@@ -7,7 +7,8 @@ require 'json'
7
7
  module CycloneLariat
8
8
  class Middleware
9
9
  def initialize(dataset: nil, errors_notifier: nil, message_notifier: nil, repo: MessagesRepo)
10
- @events_repo = repo.new(dataset) if dataset
10
+ events_dataset = dataset || CycloneLariat.events_dataset
11
+ @events_repo = repo.new(events_dataset) if events_dataset
11
12
  @message_notifier = message_notifier
12
13
  @errors_notifier = errors_notifier
13
14
  end
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'forwardable'
5
+ require_relative 'sns_client'
6
+ require_relative 'sqs_client'
7
+ require 'luna_park/errors'
8
+ require 'terminal-table'
9
+ require 'set'
10
+
11
+ module CycloneLariat
12
+ class Migration
13
+ extend Forwardable
14
+ include LunaPark::Extensions::Injector
15
+
16
+ dependency(:sns) { CycloneLariat::SnsClient.new }
17
+ dependency(:sqs) { CycloneLariat::SqsClient.new }
18
+
19
+ DIR = './lariat/migrate'
20
+
21
+ def up
22
+ raise LunaPark::Errors::Abstract, "Up method should be defined in #{self.class.name}"
23
+ end
24
+
25
+ def down
26
+ raise LunaPark::Errors::Abstract, "Down method should be defined in #{self.class.name}"
27
+ end
28
+
29
+ def_delegators :sqs, :queue, :custom_queue
30
+ def_delegators :sns, :topic, :custom_topic
31
+
32
+ def create(resource)
33
+ process(
34
+ resource: resource,
35
+ for_topic: ->(topic) { sns.create(topic) },
36
+ for_queue: ->(queue) { sqs.create(queue) }
37
+ )
38
+
39
+ puts " #{resource.class.name.split('::').last} was created `#{resource.name}`"
40
+ end
41
+
42
+ def delete(resource)
43
+ process(
44
+ resource: resource,
45
+ for_topic: ->(topic) { sns.delete(topic) },
46
+ for_queue: ->(queue) { sqs.delete(queue) }
47
+ )
48
+ puts " #{resource.class.name.split('::').last} was deleted `#{resource.name}`"
49
+ end
50
+
51
+ def exists?(resource)
52
+ process(
53
+ resource: resource,
54
+ for_topic: ->(topic) { sns.exists?(topic) },
55
+ for_queue: ->(queue) { sqs.exists?(queue) }
56
+ )
57
+ end
58
+
59
+ def subscribe(topic:, endpoint:)
60
+ sns.subscribe topic: topic, endpoint: endpoint
61
+ puts " Subscription was created `#{topic.name} -> #{endpoint.name}`"
62
+ end
63
+
64
+ def unsubscribe(topic:, endpoint:)
65
+ sns.unsubscribe topic: topic, endpoint: endpoint
66
+ puts " Subscription was deleted `#{topic.name} -> #{endpoint.name}`"
67
+ end
68
+
69
+ def topics
70
+ sns.list_all
71
+ end
72
+
73
+ def queues
74
+ sqs.list_all
75
+ end
76
+
77
+ def subscriptions
78
+ sns.list_subscriptions
79
+ end
80
+
81
+ private
82
+
83
+ def process(resource:, for_topic:, for_queue:)
84
+ case resource
85
+ when Topic then for_topic.call(resource)
86
+ when Queue then for_queue.call(resource)
87
+ else
88
+ raise ArgumentError, "Unknown resource class #{resource.class}"
89
+ end
90
+ end
91
+
92
+ class << self
93
+ def migrate(dataset: CycloneLariat.versions_dataset, dir: DIR)
94
+ alert('No one migration exists') if !Dir.exist?(dir) || Dir.empty?(dir)
95
+
96
+ Dir.glob("#{dir}/*.rb") do |path|
97
+ filename = File.basename(path, '.rb')
98
+ version, title = filename.split('_', 2)
99
+
100
+ existed_migrations = dataset.all.map { |row| row[:version] }
101
+ unless existed_migrations.include? version.to_i
102
+ class_name = title.split('_').collect(&:capitalize).join
103
+ puts "Up - #{version} #{class_name} #{path}"
104
+ require_relative Pathname.new(Dir.pwd) + Pathname.new(path)
105
+ Object.const_get(class_name).new.up
106
+ dataset.insert(version: version)
107
+ end
108
+ end
109
+ end
110
+
111
+ def rollback(version = nil, dataset: CycloneLariat.versions_dataset, dir: DIR)
112
+ existed_migrations = dataset.all.map { |row| row[:version] }.sort
113
+ version ||= existed_migrations[-1]
114
+ migrations_to_downgrade = existed_migrations.select { |migration| migration >= version }
115
+
116
+ paths = []
117
+ migrations_to_downgrade.each do |migration|
118
+ path = Pathname.new(Dir.pwd) + Pathname.new(dir)
119
+ founded = Dir.glob("#{path}/#{migration}_*.rb")
120
+ raise "Could not found migration: `#{migration}` in #{path}" if founded.empty?
121
+ raise "Found lot of migration: `#{migration}` in #{path}" if founded.size > 1
122
+
123
+ paths += founded
124
+ end
125
+
126
+ paths.each do |path|
127
+ filename = File.basename(path, '.rb')
128
+ version, title = filename.split('_', 2)
129
+ class_name = title.split('_').collect(&:capitalize).join
130
+ puts "Down - #{version} #{class_name} #{path}"
131
+ require_relative Pathname.new(Dir.pwd) + Pathname.new(path)
132
+ Object.const_get(class_name).new.down
133
+ dataset.filter(version: version).delete
134
+ end
135
+ end
136
+
137
+ def list_topics
138
+ rows = []
139
+ new.topics.each do |topic|
140
+ rows << [
141
+ topic.custom? ? 'custom' : 'standard',
142
+ topic.region,
143
+ topic.account_id,
144
+ topic.name,
145
+ topic.instance,
146
+ topic.kind,
147
+ topic.publisher,
148
+ topic.type,
149
+ topic.fifo
150
+ ]
151
+ end
152
+
153
+ puts Terminal::Table.new rows: rows, headings: %w[valid region account_id name instance kind publisher type fifo]
154
+ end
155
+
156
+ def list_queues
157
+ rows = []
158
+ new.queues.each do |queue|
159
+ rows << [
160
+ queue.custom? ? 'custom' : 'standard',
161
+ queue.region,
162
+ queue.account_id,
163
+ queue.name,
164
+ queue.instance,
165
+ queue.kind,
166
+ queue.publisher,
167
+ queue.type,
168
+ queue.dest,
169
+ queue.fifo
170
+ ]
171
+ end
172
+
173
+ puts Terminal::Table.new rows: rows, headings: %w[valid region account_id name instance kind publisher type destination fifo]
174
+ end
175
+
176
+ def list_subscriptions
177
+ rows = []
178
+ new.subscriptions.each do |subscription|
179
+ rows << [
180
+ subscription[:topic].name,
181
+ subscription[:endpoint].name,
182
+ subscription[:arn]
183
+ ]
184
+ end
185
+
186
+ puts Terminal::Table.new rows: rows, headings: %w[topic endpoint subscription_arn]
187
+ end
188
+
189
+ def build_graph
190
+ subscriptions = new.subscriptions
191
+ resources_set = Set.new
192
+
193
+ subscriptions.each do |subscription|
194
+ resources_set << subscription[:topic]
195
+ resources_set << subscription[:endpoint]
196
+ end
197
+
198
+ puts 'digraph G {'
199
+ puts ' rankdir=LR;'
200
+
201
+ resources_set.each do |resource|
202
+ color = resource.custom? ? ', fillcolor=grey' : ', fillcolor=white'
203
+ style = resource.topic? ? "[shape=component style=filled#{color}]" : "[shape=record, style=\"rounded,filled\"#{color}]"
204
+ puts " \"#{resource.name}\" #{style};"
205
+ end
206
+
207
+ subscriptions.each do |subscription|
208
+ puts " \"#{subscription[:topic].name}\" -> \"#{subscription[:endpoint].name}\";"
209
+ end
210
+ puts '}'
211
+ end
212
+ end
213
+ end
214
+ end