cyclone_lariat 0.3.10 → 0.4.0

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