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,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module CycloneLariat
6
+ class Queue
7
+ SNS_SUFFIX = :queue
8
+
9
+ attr_reader :instance, :kind, :region, :dest, :account_id, :publisher, :type, :fifo, :tags
10
+
11
+ def initialize(instance:, kind:, region:, dest:, account_id:, publisher:, type:, fifo:, tags: nil, name: nil)
12
+ @instance = instance
13
+ @kind = kind
14
+ @region = region
15
+ @dest = dest
16
+ @account_id = account_id
17
+ @publisher = publisher
18
+ @type = type
19
+ @fifo = fifo
20
+ @tags = tags || default_tags(instance, kind, publisher, type, dest, fifo)
21
+ @name = name
22
+ end
23
+
24
+ def arn
25
+ ['arn', 'aws', 'sqs', region, account_id, name].join ':'
26
+ end
27
+
28
+ ##
29
+ # Url example:
30
+ # https://sqs.eu-west-1.amazonaws.com/247606935658/stage-event-queue
31
+ def url
32
+ "https://sqs.#{region}.amazonaws.com/#{account_id}/#{name}"
33
+ end
34
+
35
+ def custom?
36
+ !standard?
37
+ end
38
+
39
+ def standard?
40
+ instance && kind && publisher && type && true
41
+ end
42
+
43
+ def name
44
+ @name ||= begin
45
+ name = [instance, kind, SNS_SUFFIX, publisher, type, dest].compact.join '-'
46
+ name += '.fifo' if fifo
47
+ name
48
+ end
49
+ end
50
+
51
+ alias to_s name
52
+
53
+
54
+ def topic?
55
+ false
56
+ end
57
+
58
+ def queue?
59
+ true
60
+ end
61
+
62
+ def protocol
63
+ 'sqs'
64
+ end
65
+
66
+ class << self
67
+ ##
68
+ # Name example: test-event-queue-cyclone_lariat-note_added.fifo
69
+ # instance: teste
70
+ # kind: event
71
+ # publisher: cyclone_lariat
72
+ # type: note_added
73
+ # dest: nil
74
+ # fifo: true
75
+ def from_name(name, region:, account_id:)
76
+ is_fifo_array = name.split('.')
77
+ full_name = is_fifo_array[0]
78
+ fifo_suffix = is_fifo_array[-1]
79
+ suffix_exists = fifo_suffix != full_name
80
+
81
+ if suffix_exists && fifo_suffix != 'fifo'
82
+ raise ArgumentError, "Queue name #{name} consists unexpected suffix #{fifo_suffix}"
83
+ end
84
+
85
+ fifo = suffix_exists
86
+ queue_array = full_name.split('-')
87
+
88
+ raise ArgumentError, "Topic name should consists `#{SNS_SUFFIX}`" unless queue_array[2] != SNS_SUFFIX
89
+
90
+ new(
91
+ instance: queue_array[0], kind: queue_array[1], region: region, dest: queue_array[5],
92
+ account_id: account_id, publisher: queue_array[3], type: queue_array[4], fifo: fifo, name: name
93
+ )
94
+ end
95
+
96
+ ##
97
+ # URL example: https://sqs.eu-west-1.amazonaws.com/247606935658/test-event-queue-cyclone_lariat-note_added.fifo
98
+ # url_array[0] => https
99
+ # host_array[0] => sqs
100
+ # host_array[1] => eu-west-1
101
+ # url_array[3] => 247606935658 # account_id
102
+ # url_array[4] => test-event-queue-cyclone_lariat-note_added.fifo # name
103
+ def from_url(url)
104
+ raise ArgumentError, 'Url is not http format' unless url =~ URI::DEFAULT_PARSER.make_regexp
105
+
106
+ url_array = url.split('/')
107
+ raise ArgumentError, 'Url should start from https' unless url_array[0] == 'https:'
108
+
109
+ host_array = url_array[2].split('.')
110
+ raise ArgumentError, 'It is not queue url' unless host_array[0] == 'sqs'
111
+
112
+ from_name(url_array[4], region: host_array[1], account_id: url_array[3])
113
+ end
114
+
115
+ ##
116
+ # Arn example: "arn:aws:sqs:eu-west-1:247606935658:custom_queue"
117
+ # arn_array[0] => 'arn'
118
+ # arn_array[1] => 'aws'
119
+ # arn_array[2] => 'sqs'
120
+ # arn_array[3] => 'eu-west-1' # region
121
+ # arn_array[4] => '247606935658' # account_id
122
+ # arn_array[5] => 'alexey_test2' # name
123
+ def from_arn(arn)
124
+ arn_array = arn.split(':')
125
+
126
+ raise ArgumentError, "Arn `#{arn}` should consists `arn`" unless arn_array[0] == 'arn'
127
+ raise ArgumentError, "Arn `#{arn}` should consists `aws`" unless arn_array[1] == 'aws'
128
+ raise ArgumentError, "Arn `#{arn}` should consists `sqs`" unless arn_array[2] == 'sqs'
129
+
130
+ from_name(arn_array[5], region: arn_array[3], account_id: arn_array[4])
131
+ end
132
+ end
133
+
134
+ private
135
+
136
+ def default_tags(instance, kind, publisher, type, dest, fifo)
137
+ {
138
+ instance: String(instance),
139
+ kind: String(kind),
140
+ publisher: String(publisher),
141
+ type: String(type),
142
+ dest: dest ? String(dest) : 'undefined',
143
+ fifo: fifo ? 'true' : 'false'
144
+ }
145
+ end
146
+ end
147
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'aws-sdk-sns'
4
4
  require_relative 'abstract/client'
5
+ require_relative 'topic'
6
+ require_relative 'queue'
5
7
 
6
8
  module CycloneLariat
7
9
  class SnsClient < Abstract::Client
@@ -9,30 +11,139 @@ module CycloneLariat
9
11
 
10
12
  dependency(:aws_client_class) { Aws::SNS::Client }
11
13
 
12
- SNS_SUFFIX = :fanout
14
+ def custom_topic(name)
15
+ Topic.from_name(name, account_id: account_id, region: region)
16
+ end
17
+
18
+ def topic(type, fifo:, publisher: nil, kind: :event)
19
+ publisher ||= self.publisher
20
+
21
+ Topic.new(
22
+ instance: instance,
23
+ publisher: publisher,
24
+ region: region,
25
+ account_id: account_id,
26
+ kind: kind,
27
+ type: type, fifo: fifo
28
+ )
29
+ end
30
+
31
+ def publish(msg, fifo:, topic: nil)
32
+ topic = topic ? custom_topic(topic) : topic(msg.type, kind: msg.kind, fifo: fifo)
33
+ aws_client.publish(topic_arn: topic.arn, message: msg.to_json)
34
+ end
35
+
36
+ def exists?(topic)
37
+ raise ArgumentError, 'Should be Topic' unless topic.is_a? Topic
38
+
39
+ aws_client.get_topic_attributes({ topic_arn: topic.arn }) && true
40
+ rescue Aws::SNS::Errors::NotFound
41
+ false
42
+ end
43
+
44
+ def publish_event(type, fifo:, data: {}, version: self.version, uuid: SecureRandom.uuid, request_id: nil, topic: nil)
45
+ publish event(type, data: data, version: version, uuid: uuid, request_id: request_id), topic: topic, fifo: fifo
46
+ end
47
+
48
+ def publish_command(type, fifo:, data: {}, version: self.version, uuid: SecureRandom.uuid, request_id: nil, topic: nil)
49
+ publish command(type, data: data, version: version, uuid: uuid, request_id: request_id), topic: topic, fifo: fifo
50
+ end
51
+
52
+ def create(topic)
53
+ raise ArgumentError, 'Should be Topic' unless topic.is_a? Topic
54
+ raise Errors::TopicAlreadyExists.new(expected_topic: topic.name) if exists?(topic)
55
+
56
+ aws_client.create_topic(name: topic.name, attributes: topic.attributes, tags: topic.tags)
57
+ topic
58
+ end
59
+
60
+ def delete(topic)
61
+ raise ArgumentError, 'Should be Topic' unless topic.is_a? Topic
62
+ raise Errors::TopicDoesNotExists.new(expected_topic: topic.name) unless exists?(topic)
13
63
 
14
- def publish(msg, topic: nil)
15
- topic ||= get_topic(msg.kind, msg.type)
16
- arn = get_arn(topic)
17
- aws_client.publish(topic_arn: arn, message: msg.to_json)
64
+ aws_client.delete_topic topic_arn: topic.arn
65
+ topic
18
66
  end
19
67
 
20
- def publish_event(type, data: {}, version: self.version, uuid: SecureRandom.uuid, topic: nil)
21
- publish event(type, data: data, version: version, uuid: uuid), topic: topic
68
+ def subscribe(topic:, endpoint:)
69
+ subscription_arn = find_subscription_arn(topic: topic, endpoint: endpoint)
70
+ raise Errors::SubscriptionAlreadyExists.new(topic: topic, endpoint: endpoint) if subscription_arn
71
+
72
+ aws_client.subscribe(
73
+ {
74
+ topic_arn: topic.arn,
75
+ protocol: endpoint.protocol,
76
+ endpoint: endpoint.arn
77
+ }
78
+ )
22
79
  end
23
80
 
24
- def publish_command(type, data: {}, version: self.version, uuid: SecureRandom.uuid, topic: nil)
25
- publish command(type, data: data, version: version, uuid: uuid), topic: topic
81
+ def unsubscribe(topic:, endpoint:)
82
+ subscription_arn = find_subscription_arn(topic: topic, endpoint: endpoint)
83
+ raise Errors::SubscriptionDoesNotExists.new(topic: topic, endpoint: endpoint) unless subscription_arn
84
+
85
+ aws_client.unsubscribe(subscription_arn: subscription_arn)
26
86
  end
27
87
 
28
- private
88
+ def list_all
89
+ topics = []
90
+ resp = aws_client.list_topics
91
+
92
+ loop do
93
+ resp[:topics].map do |t|
94
+ topics << Topic.from_arn(t[:topic_arn])
95
+ end
96
+
97
+ break if resp[:next_token].nil?
98
+
99
+ resp = aws_client.list_topics(next_token: resp[:next_token])
100
+ end
101
+ topics
102
+ end
103
+
104
+ def list_subscriptions
105
+ subscriptions = []
106
+ resp = aws_client.list_subscriptions
107
+
108
+ loop do
109
+ resp[:subscriptions].each do |s|
110
+ endpoint = s.endpoint.split(':')[2] == 'sqs' ? Queue.from_arn(s.endpoint) : Topic.from_arn(s.endpoint)
111
+ subscriptions << { topic: Topic.from_arn(s.topic_arn), endpoint: endpoint, arn: s.subscription_arn }
112
+ end
113
+
114
+ break if resp[:next_token].nil?
29
115
 
30
- def get_arn(topic)
31
- ['arn', 'aws', 'sns', region, client_id, topic].join ':'
116
+ resp = aws_client.list_subscriptions(next_token: resp[:next_token])
117
+ end
118
+ subscriptions
32
119
  end
33
120
 
34
- def get_topic(kind, type)
35
- [instance, kind, SNS_SUFFIX, publisher, type].join '-'
121
+ def topic_subscriptions(topic)
122
+ raise ArgumentError, 'Should be Topic' unless topic.is_a? Topic
123
+
124
+ subscriptions = []
125
+ resp = aws_client.list_subscriptions_by_topic(topic_arn: topic.arn)
126
+
127
+ loop do
128
+ next_token = resp[:next_token]
129
+ subscriptions += resp[:subscriptions]
130
+
131
+ break if next_token.nil?
132
+
133
+ resp = aws_client.list_subscriptions_by_topic(topic_arn: topic.arn, next_token: next_token)
134
+ end
135
+ subscriptions
136
+ end
137
+
138
+ def find_subscription_arn(topic:, endpoint:)
139
+ raise ArgumentError, 'Should be Topic' unless topic.is_a? Topic
140
+ raise ArgumentError, 'Endpoint should be Topic or Queue' unless [Topic, Queue].include? endpoint.class
141
+
142
+ found_subscription = topic_subscriptions(topic).select do |subscription|
143
+ subscription.endpoint == endpoint.arn
144
+ end.first
145
+
146
+ found_subscription ? found_subscription.subscription_arn : nil
36
147
  end
37
148
  end
38
149
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'aws-sdk-sqs'
4
4
  require_relative 'abstract/client'
5
+ require_relative 'queue'
5
6
 
6
7
  module CycloneLariat
7
8
  class SqsClient < Abstract::Client
@@ -9,31 +10,84 @@ module CycloneLariat
9
10
 
10
11
  dependency(:aws_client_class) { Aws::SQS::Client }
11
12
 
12
- SQS_SUFFIX = :queue
13
-
14
- def publish(msg, dest: nil, topic: nil)
15
- raise ArgumentError, 'You should define dest or topic' if dest.nil? && topic.nil?
13
+ def custom_queue(name)
14
+ Queue.from_name(name, account_id: account_id, region: region)
15
+ end
16
16
 
17
- topic ||= [instance, msg.kind, SQS_SUFFIX, publisher, msg.type, dest].join('-')
17
+ def queue(type = :all, fifo:, dest: nil, publisher: nil, kind: :event)
18
+ publisher ||= self.publisher
18
19
 
19
- aws_client.send_message(
20
- queue_url: url(topic),
21
- message_body: msg.to_json
20
+ Queue.new(
21
+ instance: instance, publisher: publisher, region: region,
22
+ account_id: account_id, kind: kind, type: type, fifo: fifo, dest: dest
22
23
  )
23
24
  end
24
25
 
25
- def publish_event(type, dest: nil, data: {}, version: self.version, uuid: SecureRandom.uuid, topic: nil)
26
- publish event(type, data: data, version: version, uuid: uuid), dest: dest, topic: topic
26
+ def get_url(queue)
27
+ raise ArgumentError, 'Should be queue' unless queue.is_a? Queue
28
+
29
+ aws_client.get_queue_url(queue_name: queue.to_s).queue_url
30
+ end
31
+
32
+ def exists?(queue)
33
+ raise ArgumentError, 'Should be queue' unless queue.is_a? Queue
34
+
35
+ get_url(queue) && true
36
+ rescue Aws::SQS::Errors::NonExistentQueue
37
+ false
27
38
  end
28
39
 
29
- def publish_command(type, dest: nil, data: {}, version: self.version, uuid: SecureRandom.uuid, topic: nil)
30
- publish command(type, data: data, version: version, uuid: uuid), dest: dest, topic: topic
40
+ def publish(msg, fifo:, dest: nil, queue: nil)
41
+ queue = queue ? custom_queue(queue) : queue(msg.type, kind: msg.kind, fifo: fifo, dest: dest)
42
+ aws_client.send_message(queue_url: get_url(queue), message_body: msg.to_json)
31
43
  end
32
44
 
33
- private
45
+ def publish_event(type, fifo:, dest: nil, data: {}, version: self.version, uuid: SecureRandom.uuid, request_id: nil, queue: nil)
46
+ publish event(type, data: data, version: version, uuid: uuid, request_id: request_id),
47
+ fifo: fifo, dest: dest, queue: queue
48
+ end
49
+
50
+ def publish_command(type, fifo:, dest: nil, data: {}, version: self.version, uuid: SecureRandom.uuid, request_id: nil, queue: nil)
51
+ publish command(type, data: data, version: version, uuid: uuid, request_id: request_id),
52
+ fifo: fifo, dest: dest, queue: queue
53
+ end
54
+
55
+ def create(queue)
56
+ raise ArgumentError, 'Should be queue' unless queue.is_a? Queue
57
+ raise Errors::QueueAlreadyExists.new(expected_queue: queue.name) if exists?(queue)
58
+
59
+ attrs = {}
60
+ attrs['FifoQueue'] = 'true' if queue.fifo
61
+
62
+ aws_client.create_queue(queue_name: queue.name, attributes: attrs, tags: queue.tags)
63
+ queue
64
+ end
65
+
66
+ def delete(queue)
67
+ raise ArgumentError, 'Should be queue' unless queue.is_a? Queue
68
+ raise Errors::QueueDoesNotExists.new(expected_queue: queue.name) unless exists?(queue)
69
+
70
+ aws_client.delete_queue queue_url: queue.url
71
+ queue
72
+ end
73
+
74
+ def list_all
75
+ queues = []
76
+ resp = aws_client.list_queues
77
+
78
+ loop do
79
+ next_token = resp[:next_token]
80
+
81
+ resp[:queue_urls].map do |url|
82
+ queues << Queue.from_url(url)
83
+ end
84
+
85
+ break if next_token.nil?
86
+
87
+ resp = aws_client.list_queues(next_token: next_token)
88
+ end
34
89
 
35
- def url(topic_name)
36
- aws_client.get_queue_url(queue_name: topic_name).queue_url
90
+ queues
37
91
  end
38
92
  end
39
93
  end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CycloneLariat
4
+ class Topic
5
+ SNS_SUFFIX = :fanout
6
+
7
+ attr_reader :instance, :kind, :region, :account_id, :publisher, :type, :fifo, :tags
8
+
9
+ def initialize(instance:, kind:, region:, account_id:, publisher:, type:, fifo:, tags: nil, name: nil)
10
+ @instance = instance
11
+ @kind = kind
12
+ @region = region
13
+ @account_id = account_id
14
+ @publisher = publisher
15
+ @type = type
16
+ @fifo = fifo
17
+ @tags = tags || default_tags(instance, kind, publisher, type, fifo)
18
+ @name = name
19
+ end
20
+
21
+ def arn
22
+ ['arn', 'aws', 'sns', region, account_id, to_s].join ':'
23
+ end
24
+
25
+ def custom?
26
+ !standard?
27
+ end
28
+
29
+ def standard?
30
+ instance && kind && publisher && type && true
31
+ end
32
+
33
+ def name
34
+ @name ||= begin
35
+ name = [instance, kind, SNS_SUFFIX, publisher, type].join '-'
36
+ name += '.fifo' if fifo
37
+ name
38
+ end
39
+ end
40
+
41
+ def attributes
42
+ fifo ? { 'FifoTopic' => 'true' } : {}
43
+ end
44
+
45
+ def topic?
46
+ true
47
+ end
48
+
49
+ def queue?
50
+ false
51
+ end
52
+
53
+ def protocol
54
+ 'sns'
55
+ end
56
+
57
+ alias to_s name
58
+
59
+ def ==(other)
60
+ arn == other.arn
61
+ end
62
+
63
+ class << self
64
+ def from_name(name, region:, account_id:)
65
+ is_fifo_array = name.split('.')
66
+ full_name = is_fifo_array[0]
67
+ fifo_suffix = is_fifo_array[-1]
68
+ suffix_exists = fifo_suffix != full_name
69
+
70
+ if suffix_exists && fifo_suffix != 'fifo'
71
+ raise ArgumentError, "Topic name #{name} consists unexpected suffix #{fifo_suffix}"
72
+ end
73
+
74
+ fifo = suffix_exists
75
+ topic_array = full_name.split('-')
76
+
77
+ raise ArgumentError, "Topic name should consists `#{SNS_SUFFIX}`" unless topic_array[2] != SNS_SUFFIX
78
+
79
+ new(
80
+ instance: topic_array[0],
81
+ kind: topic_array[1],
82
+ publisher: topic_array[3],
83
+ type: topic_array[4],
84
+ region: region,
85
+ account_id: account_id,
86
+ fifo: fifo,
87
+ name: name
88
+ )
89
+ end
90
+
91
+ def from_arn(arn)
92
+ arn_array = arn.split(':')
93
+ raise ArgumentError, 'Arn should consists `arn`' unless arn_array[0] == 'arn'
94
+ raise ArgumentError, 'Arn should consists `aws`' unless arn_array[1] == 'aws'
95
+ raise ArgumentError, 'Arn should consists `sns`' unless arn_array[2] == 'sns'
96
+
97
+ from_name(arn_array[5], region: arn_array[3], account_id: arn_array[4])
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ def default_tags(instance, kind, publisher, type, fifo)
104
+ [
105
+ { key: 'instance', value: String(instance) },
106
+ { key: 'kind', value: String(kind) },
107
+ { key: 'publisher', value: String(publisher) },
108
+ { key: 'type', value: String(type) },
109
+ { key: 'fifo', value: fifo ? 'true' : 'false' }
110
+ ]
111
+ end
112
+ end
113
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CycloneLariat
4
- VERSION = '0.3.10'
4
+ VERSION = '0.4.0'
5
5
  end
@@ -6,6 +6,7 @@ require_relative 'cyclone_lariat/errors'
6
6
  require_relative 'cyclone_lariat/event'
7
7
  require_relative 'cyclone_lariat/messages_mapper'
8
8
  require_relative 'cyclone_lariat/messages_repo'
9
+ require_relative 'cyclone_lariat/migration'
9
10
  require_relative 'cyclone_lariat/middleware'
10
11
  require_relative 'cyclone_lariat/version'
11
12
 
@@ -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_relative '../../lib/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,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cyclone_lariat'
4
+
5
+ namespace :cyclone_lariat do
6
+ desc 'Migrate topics for SQSSNS'
7
+ task migrate: :config do
8
+ require_relative '../../config/initializers/cyclone_lariat'
9
+ CycloneLariat::Migration.migrate
10
+ end
11
+
12
+ desc 'Rollback topics for SQSSNS'
13
+ task :rollback, [:version] => :config do |_, args|
14
+ require_relative '../../config/initializers/cyclone_lariat'
15
+ target_version = args[:version] ? args[:version].to_i : nil
16
+ CycloneLariat::Migration.rollback(target_version)
17
+ end
18
+
19
+ namespace :list do
20
+ desc 'List all topics'
21
+ task :topics do
22
+ require_relative '../../config/initializers/cyclone_lariat'
23
+ CycloneLariat::Migration.list_topics
24
+ end
25
+
26
+ desc 'List all queues'
27
+ task :queues do
28
+ require_relative '../../config/initializers/cyclone_lariat'
29
+ CycloneLariat::Migration.list_queues
30
+ end
31
+
32
+ desc 'List all subscriptions'
33
+ task :subscriptions do
34
+ require_relative '../../config/initializers/cyclone_lariat'
35
+ CycloneLariat::Migration.list_subscriptions
36
+ end
37
+ end
38
+
39
+ desc 'Build graphviz graph for whole system'
40
+ task :graph do
41
+ require_relative '../../config/initializers/cyclone_lariat'
42
+ CycloneLariat::Migration.build_graph
43
+ end
44
+ end
data/lib/tasks/db.rake CHANGED
@@ -22,7 +22,7 @@ namespace :db do
22
22
  puts "Database `#{DB_CONF[:database]}` successfully dropped" if system(cmd)
23
23
  end
24
24
 
25
- desc 'Apply migrations'
25
+ desc 'Apply migrate'
26
26
  task :migrate, [:version] => :config do |_, args|
27
27
  require 'logger'
28
28
  require 'sequel/core'