kafka_command 0.0.1

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 (159) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +179 -0
  3. data/.env +1 -0
  4. data/.env.test +1 -0
  5. data/.gitignore +41 -0
  6. data/.rspec +1 -0
  7. data/.rubocop.yml +12 -0
  8. data/.ruby-version +1 -0
  9. data/Gemfile +17 -0
  10. data/Gemfile.lock +194 -0
  11. data/LICENSE +21 -0
  12. data/README.md +138 -0
  13. data/Rakefile +34 -0
  14. data/app/assets/config/manifest.js +3 -0
  15. data/app/assets/images/.keep +0 -0
  16. data/app/assets/images/kafka_command/cluster_view.png +0 -0
  17. data/app/assets/images/kafka_command/kafka.png +0 -0
  18. data/app/assets/images/kafka_command/topic_view.png +0 -0
  19. data/app/assets/javascripts/kafka_command/application.js +14 -0
  20. data/app/assets/stylesheets/kafka_command/application.css +27 -0
  21. data/app/assets/stylesheets/kafka_command/clusters.css +8 -0
  22. data/app/assets/stylesheets/kafka_command/topics.css +3 -0
  23. data/app/channels/application_cable/channel.rb +6 -0
  24. data/app/channels/application_cable/connection.rb +6 -0
  25. data/app/controllers/kafka_command/application_controller.rb +96 -0
  26. data/app/controllers/kafka_command/brokers_controller.rb +26 -0
  27. data/app/controllers/kafka_command/clusters_controller.rb +46 -0
  28. data/app/controllers/kafka_command/consumer_groups_controller.rb +44 -0
  29. data/app/controllers/kafka_command/topics_controller.rb +187 -0
  30. data/app/helpers/kafka_command/application_helper.rb +29 -0
  31. data/app/helpers/kafka_command/consumer_group_helper.rb +13 -0
  32. data/app/jobs/application_job.rb +6 -0
  33. data/app/mailers/application_mailer.rb +8 -0
  34. data/app/models/kafka_command/broker.rb +47 -0
  35. data/app/models/kafka_command/client.rb +102 -0
  36. data/app/models/kafka_command/cluster.rb +172 -0
  37. data/app/models/kafka_command/consumer_group.rb +142 -0
  38. data/app/models/kafka_command/consumer_group_partition.rb +23 -0
  39. data/app/models/kafka_command/group_member.rb +18 -0
  40. data/app/models/kafka_command/partition.rb +36 -0
  41. data/app/models/kafka_command/topic.rb +153 -0
  42. data/app/views/kafka_command/brokers/index.html.erb +38 -0
  43. data/app/views/kafka_command/clusters/_tabs.html.erb +9 -0
  44. data/app/views/kafka_command/clusters/index.html.erb +54 -0
  45. data/app/views/kafka_command/clusters/new.html.erb +115 -0
  46. data/app/views/kafka_command/configuration_error.html.erb +1 -0
  47. data/app/views/kafka_command/consumer_groups/index.html.erb +32 -0
  48. data/app/views/kafka_command/consumer_groups/show.html.erb +115 -0
  49. data/app/views/kafka_command/shared/_alert.html.erb +13 -0
  50. data/app/views/kafka_command/shared/_search_bar.html.erb +31 -0
  51. data/app/views/kafka_command/shared/_title.html.erb +6 -0
  52. data/app/views/kafka_command/topics/_form_fields.html.erb +49 -0
  53. data/app/views/kafka_command/topics/edit.html.erb +17 -0
  54. data/app/views/kafka_command/topics/index.html.erb +46 -0
  55. data/app/views/kafka_command/topics/new.html.erb +36 -0
  56. data/app/views/kafka_command/topics/show.html.erb +126 -0
  57. data/app/views/layouts/kafka_command/application.html.erb +50 -0
  58. data/bin/rails +16 -0
  59. data/config/initializers/kafka.rb +13 -0
  60. data/config/initializers/kafka_command.rb +11 -0
  61. data/config/routes.rb +11 -0
  62. data/docker-compose.yml +18 -0
  63. data/kafka_command.gemspec +27 -0
  64. data/lib/assets/.keep +0 -0
  65. data/lib/core_extensions/kafka/broker/attr_readers.rb +11 -0
  66. data/lib/core_extensions/kafka/broker_pool/attr_readers.rb +11 -0
  67. data/lib/core_extensions/kafka/client/attr_readers.rb +11 -0
  68. data/lib/core_extensions/kafka/cluster/attr_readers.rb +11 -0
  69. data/lib/core_extensions/kafka/protocol/metadata_response/partition_metadata/attr_readers.rb +15 -0
  70. data/lib/kafka_command/configuration.rb +150 -0
  71. data/lib/kafka_command/engine.rb +11 -0
  72. data/lib/kafka_command/errors.rb +6 -0
  73. data/lib/kafka_command/version.rb +5 -0
  74. data/lib/kafka_command.rb +13 -0
  75. data/lib/tasks/.keep +0 -0
  76. data/spec/dummy/Rakefile +6 -0
  77. data/spec/dummy/app/assets/config/manifest.js +4 -0
  78. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  79. data/spec/dummy/app/assets/javascripts/cable.js +13 -0
  80. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  81. data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
  82. data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
  83. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  84. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  85. data/spec/dummy/app/jobs/application_job.rb +2 -0
  86. data/spec/dummy/app/mailers/application_mailer.rb +4 -0
  87. data/spec/dummy/app/models/application_record.rb +3 -0
  88. data/spec/dummy/app/views/layouts/application.html.erb +15 -0
  89. data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
  90. data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
  91. data/spec/dummy/bin/bundle +3 -0
  92. data/spec/dummy/bin/rails +4 -0
  93. data/spec/dummy/bin/rake +4 -0
  94. data/spec/dummy/bin/setup +36 -0
  95. data/spec/dummy/bin/update +31 -0
  96. data/spec/dummy/bin/yarn +11 -0
  97. data/spec/dummy/config/application.rb +19 -0
  98. data/spec/dummy/config/boot.rb +5 -0
  99. data/spec/dummy/config/cable.yml +10 -0
  100. data/spec/dummy/config/database.yml +25 -0
  101. data/spec/dummy/config/environment.rb +5 -0
  102. data/spec/dummy/config/environments/development.rb +61 -0
  103. data/spec/dummy/config/environments/production.rb +94 -0
  104. data/spec/dummy/config/environments/test.rb +46 -0
  105. data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
  106. data/spec/dummy/config/initializers/assets.rb +14 -0
  107. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  108. data/spec/dummy/config/initializers/content_security_policy.rb +25 -0
  109. data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
  110. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  111. data/spec/dummy/config/initializers/inflections.rb +16 -0
  112. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  113. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  114. data/spec/dummy/config/kafka_command.yml +18 -0
  115. data/spec/dummy/config/locales/en.yml +33 -0
  116. data/spec/dummy/config/puma.rb +34 -0
  117. data/spec/dummy/config/routes.rb +3 -0
  118. data/spec/dummy/config/spring.rb +6 -0
  119. data/spec/dummy/config/ssl/test_ca_cert +1 -0
  120. data/spec/dummy/config/ssl/test_client_cert +1 -0
  121. data/spec/dummy/config/ssl/test_client_cert_key +1 -0
  122. data/spec/dummy/config/storage.yml +34 -0
  123. data/spec/dummy/config.ru +5 -0
  124. data/spec/dummy/db/schema.rb +42 -0
  125. data/spec/dummy/db/test.sqlite3 +0 -0
  126. data/spec/dummy/log/development.log +0 -0
  127. data/spec/dummy/log/hey.log +0 -0
  128. data/spec/dummy/log/test.log +2227 -0
  129. data/spec/dummy/package.json +5 -0
  130. data/spec/dummy/public/404.html +67 -0
  131. data/spec/dummy/public/422.html +67 -0
  132. data/spec/dummy/public/500.html +66 -0
  133. data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
  134. data/spec/dummy/public/apple-touch-icon.png +0 -0
  135. data/spec/dummy/public/favicon.ico +0 -0
  136. data/spec/examples.txt +165 -0
  137. data/spec/fast_helper.rb +20 -0
  138. data/spec/fixtures/files/kafka_command_sasl.yml +10 -0
  139. data/spec/fixtures/files/kafka_command_ssl.yml +10 -0
  140. data/spec/fixtures/files/kafka_command_ssl_file_paths.yml +11 -0
  141. data/spec/fixtures/files/kafka_command_staging.yml +8 -0
  142. data/spec/lib/kafka_command/configuration_spec.rb +311 -0
  143. data/spec/models/kafka_command/broker_spec.rb +83 -0
  144. data/spec/models/kafka_command/client_spec.rb +306 -0
  145. data/spec/models/kafka_command/cluster_spec.rb +163 -0
  146. data/spec/models/kafka_command/consumer_group_partition_spec.rb +43 -0
  147. data/spec/models/kafka_command/consumer_group_spec.rb +236 -0
  148. data/spec/models/kafka_command/partition_spec.rb +95 -0
  149. data/spec/models/kafka_command/topic_spec.rb +311 -0
  150. data/spec/rails_helper.rb +63 -0
  151. data/spec/requests/json/brokers_spec.rb +50 -0
  152. data/spec/requests/json/clusters_spec.rb +58 -0
  153. data/spec/requests/json/consumer_groups_spec.rb +139 -0
  154. data/spec/requests/json/topics_spec.rb +274 -0
  155. data/spec/spec_helper.rb +109 -0
  156. data/spec/support/factory_bot.rb +5 -0
  157. data/spec/support/json_helper.rb +13 -0
  158. data/spec/support/kafka_helper.rb +93 -0
  159. metadata +326 -0
@@ -0,0 +1,27 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
6
+ * vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
16
+
17
+ .is-borderless {
18
+ border-width: 0px !important;
19
+ }
20
+
21
+ .kafka-logo {
22
+ margin-left: 0 !important;
23
+ }
24
+
25
+ .page-title {
26
+ margin-top: 1rem;
27
+ }
@@ -0,0 +1,8 @@
1
+ .cluster-card {
2
+ margin-bottom: 1.5rem;
3
+ }
4
+
5
+ .delete-cluster {
6
+ /* justify-content: flex-end; */
7
+ padding: 0.5rem;
8
+ }
@@ -0,0 +1,3 @@
1
+ .config-table {
2
+ margin-bottom: 2.5rem !important;
3
+ }
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApplicationCable
4
+ class Channel < ActionCable::Channel::Base
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApplicationCable
4
+ class Connection < ActionCable::Connection::Base
5
+ end
6
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KafkaCommand
4
+ class ApplicationController < ActionController::Base
5
+ protect_from_forgery unless: -> { request.format.json? }
6
+
7
+ rescue_from Kafka::ConnectionError, with: :kafka_connection_error
8
+ rescue_from Kafka::ClusterAuthorizationFailed, with: :kafka_authorization_error
9
+ rescue_from UnsupportedApiError, with: :unsupported_api_error
10
+
11
+ before_action do
12
+ unless KafkaCommand.config.valid?
13
+ flash[:error] = KafkaCommand.config.errors.join("\n")
14
+ render 'kafka_command/configuration_error'
15
+ end
16
+ end
17
+
18
+ protected
19
+
20
+ def unsupported_api_error(exception)
21
+ render_error(exception.message, status: 422, flash: { error: exception.message })
22
+ end
23
+
24
+ def record_not_found
25
+ render_error('Not Found', status: :not_found)
26
+ end
27
+
28
+ def kafka_connection_error
29
+ error_msg = 'Could not connect to Kafka with the specified brokers'
30
+ render_error(
31
+ error_msg,
32
+ status: 500,
33
+ flash: { error: error_msg }
34
+ )
35
+ end
36
+
37
+ def kafka_authorization_error
38
+ error_msg = 'You are not authorized to perform that action'
39
+ render_error(
40
+ error_msg,
41
+ status: 401,
42
+ flash: { error: error_msg }
43
+ )
44
+ end
45
+
46
+ def serialize_json(data, **kwargs)
47
+ if data.is_a?(ActiveRecord::Relation) || data.is_a?(Array)
48
+ return {
49
+ data: data.map { |d| d.as_json(**kwargs) }
50
+ }
51
+ end
52
+
53
+ data.as_json(**kwargs)
54
+ end
55
+
56
+ def render_success(data, status: :ok, redirection_path: nil, flash: {}, **kwargs)
57
+ respond_to do |format|
58
+ format.html do
59
+ redirect_to redirection_path, flash: flash if redirection_path
60
+ end
61
+
62
+ format.json do
63
+ if status == :no_content || status.to_s.to_i == 204
64
+ head :no_content
65
+ else
66
+ render_json(data, status: status, **kwargs)
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ def render_error(data, status: :unprocessible_entity, flash: {})
73
+ respond_to do |format|
74
+ format.html do
75
+ redirect_back(fallback_location: root_path, flash: flash) && (return) if flash.present?
76
+
77
+ case status
78
+ when :not_found, 404
79
+ render json: '404 Not Found', status: status, layout: false
80
+ else
81
+ render json: '500 Internal Server Error', status: status, layout: false
82
+ end
83
+ end
84
+ format.json { render_json_errors(data, status: status) }
85
+ end
86
+ end
87
+
88
+ def render_json(data, status:, **kwargs)
89
+ render json: serialize_json(data, **kwargs), status: status
90
+ end
91
+
92
+ def render_json_errors(errors, status: :unprocessible_entity)
93
+ render json: errors, status: status
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency 'kafka_command/application_controller'
4
+
5
+ module KafkaCommand
6
+ class BrokersController < ApplicationController
7
+ # GET /clusters/:cluster_id/brokers
8
+ def index
9
+ @cluster = Cluster.find(params[:cluster_id])
10
+ @brokers = @cluster.brokers
11
+ render_success(@brokers)
12
+ end
13
+
14
+ # GET /clusters/:cluster_id/brokers/:id
15
+ def show
16
+ cluster = Cluster.find(params[:cluster_id])
17
+ @broker = cluster.brokers.find { |b| b.node_id == params[:id].to_i }
18
+
19
+ if @broker.present?
20
+ render_success(@broker)
21
+ else
22
+ record_not_found
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency 'kafka_command/application_controller'
4
+
5
+ module KafkaCommand
6
+ class ClustersController < ApplicationController
7
+ # GET /clusters
8
+ def index
9
+ @clusters = Cluster.all
10
+
11
+ flash[:search] = params[:name]
12
+
13
+ if params[:name].present?
14
+ @clusters = @clusters.select do |c|
15
+ regex = /#{params[:name]}/i
16
+ c.name.match?(regex)
17
+ end
18
+ end
19
+
20
+ redirection_path = new_cluster_path if Cluster.none?
21
+ render_success(@clusters, redirection_path: redirection_path, flash: flash.to_hash)
22
+ end
23
+
24
+ # GET /clusters/:id
25
+ def show
26
+ @cluster = Cluster.find(params[:id])
27
+
28
+ if @cluster
29
+ render_success(@cluster)
30
+ else
31
+ record_not_found
32
+ end
33
+ end
34
+
35
+ private
36
+ # leave for config validation
37
+
38
+ def cluster_params
39
+ params.permit(*cluster_params_keys)
40
+ end
41
+
42
+ def cluster_params_keys
43
+ [:name, :description, :version]
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency 'kafka_command/application_controller'
4
+
5
+ module KafkaCommand
6
+ class ConsumerGroupsController < ApplicationController
7
+ # GET /clusters/:cluster_id/consumer_groups
8
+ def index
9
+ @cluster = Cluster.find(params[:cluster_id])
10
+ @groups = @cluster.groups
11
+
12
+ flash[:search] = params[:group_id]
13
+
14
+ if params[:group_id].present?
15
+ @groups = @groups.select do |g|
16
+ regex = /#{params[:group_id]}/i
17
+ g.group_id.match?(regex)
18
+ end
19
+ end
20
+
21
+ render_success(@groups)
22
+ end
23
+
24
+ # GET /alusters/:cluster_id/consumer_groups/:id
25
+ def show
26
+ @cluster = Cluster.find(params[:cluster_id])
27
+ @group = @cluster.groups.find { |g| g.group_id == params[:id] }
28
+
29
+ if @group.nil?
30
+ render_error('Consumer group not found', status: 404)
31
+ return
32
+ end
33
+
34
+ @current_topic =
35
+ if params[:topic]
36
+ @group.consumed_topics.find { |t| t.name == params[:topic] }
37
+ else
38
+ @group.consumed_topics.first
39
+ end
40
+
41
+ render_success(@group)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency 'kafka_command/application_controller'
4
+
5
+ module KafkaCommand
6
+ class TopicsController < ApplicationController
7
+ rescue_from Kafka::InvalidPartitions, with: :invalid_partitions
8
+ rescue_from Kafka::InvalidReplicationFactor, with: :invalid_replication_factor
9
+ rescue_from Kafka::InvalidTopic, with: :invalid_topic_name
10
+ rescue_from Kafka::TopicAlreadyExists, with: :topic_already_exists
11
+ rescue_from Kafka::UnknownError, with: :unknown_error
12
+ rescue_from Kafka::InvalidRequest, with: :unknown_error
13
+ rescue_from Kafka::InvalidConfig, with: :unknown_error
14
+ rescue_from Kafka::TopicAuthorizationFailed, with: :kafka_authorization_error
15
+ rescue_from KafkaCommand::Topic::DeletionError, with: :topic_deletion_error
16
+
17
+ # GET /clusters/:cluster_id/topics
18
+ def index
19
+ @cluster = Cluster.find(params[:cluster_id])
20
+ @topics = @cluster.topics
21
+
22
+ flash[:search] = params[:name]
23
+
24
+ if params[:name].present?
25
+ @topics = @topics.select do |t|
26
+ regex = /#{params[:name]}/i
27
+ t.name.match?(regex)
28
+ end
29
+ end
30
+
31
+ render_success(@topics)
32
+ end
33
+
34
+ # GET /clusters/:cluster_id/topics/:id
35
+ def show
36
+ @cluster = Cluster.find(params[:cluster_id])
37
+ @topic = @cluster.topics.find { |t| t.name == params[:id] }
38
+
39
+ if @topic.nil?
40
+ render_error('Topic not found', status: 404)
41
+ else
42
+ render_success(@topic, include_config: true)
43
+ end
44
+ end
45
+
46
+ # GET /clusters/:cluster_id/topics/new
47
+ def new
48
+ @cluster = Cluster.find(params[:cluster_id])
49
+ end
50
+
51
+ # GET /clusters/:cluster_id/topics/edit
52
+ def edit
53
+ @cluster = Cluster.find(params[:cluster_id])
54
+ @topic = @cluster.topics.find { |t| t.name == params[:id] }
55
+ @redirect_path = params[:redirect_path]
56
+ end
57
+
58
+ # POST /clusters/:cluster_id/topics
59
+ def create
60
+ @cluster = Cluster.find(params[:cluster_id])
61
+ @topic = @cluster.create_topic(
62
+ params[:name],
63
+ num_partitions: params[:num_partitions].to_i,
64
+ replication_factor: params[:replication_factor].to_i,
65
+ config: build_config
66
+ )
67
+
68
+ render_success(
69
+ @topic,
70
+ status: :created,
71
+ redirection_path: cluster_topics_path,
72
+ flash: { success: 'Topic created' },
73
+ include_config: true
74
+ )
75
+ end
76
+
77
+ # PATCH/PUT /clusters/:cluster_id/topics/:id
78
+ def update
79
+ cluster = Cluster.find(params[:cluster_id])
80
+ @topic = cluster.topics.find { |t| t.name == params[:id] }
81
+
82
+ render_error('Topic not found', status: 404) && (return) if @topic.nil?
83
+ if params[:num_partitions]
84
+ @topic.set_partitions!(params[:num_partitions].to_i) unless params[:num_partitions].to_i == @topic.partitions.count
85
+ end
86
+
87
+ if build_config.present?
88
+ @topic.set_configs!(
89
+ max_message_bytes: params[:max_message_bytes],
90
+ retention_ms: params[:retention_ms],
91
+ retention_bytes: params[:retention_bytes]
92
+ )
93
+ end
94
+
95
+ render_success(
96
+ @topic,
97
+ status: :ok,
98
+ redirection_path: params[:redirect_path] || cluster_topics_path,
99
+ flash: { success: 'Topic updated' }
100
+ )
101
+ end
102
+
103
+ # DELETE /clusters/:cluster_id/topics/:id
104
+ def destroy
105
+ @cluster = Cluster.find(params[:cluster_id])
106
+ @topic = @cluster.topics.find { |t| t.name == params[:id] }
107
+
108
+ if @topic.nil?
109
+ render_error('Topic not found', status: 404)
110
+ else
111
+ @topic.destroy
112
+ render_success(
113
+ @topic,
114
+ status: :no_content,
115
+ redirection_path: cluster_topics_path,
116
+ flash: { success: "Topic \"#{@topic.name}\" is marked for deletion. <strong>Note: This will have no impact if delete.topic.enable is not set to true.</strong>".html_safe }
117
+ )
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ def build_config
124
+ {}.tap do |config|
125
+ config['max.message.bytes'] = params[:max_message_bytes] if params[:max_message_bytes]
126
+ config['retention.ms'] = params[:retention_ms] if params[:retention_ms]
127
+ config['retention.bytes'] = params[:retention_bytes] if params[:retention_bytes]
128
+ end
129
+ end
130
+
131
+ def invalid_partitions
132
+ error_msg = 'Num partitions must be > 0 or > current number of partitions'
133
+
134
+ render_error(
135
+ error_msg,
136
+ status: 422,
137
+ flash: { error: error_msg }
138
+ )
139
+ end
140
+
141
+ def invalid_replication_factor
142
+ error_msg = 'Replication factor must be > 0 and < total number of brokers'
143
+
144
+ render_error(
145
+ error_msg,
146
+ status: 422,
147
+ flash: { error: error_msg }
148
+ )
149
+ end
150
+
151
+ def invalid_topic_name
152
+ error_msg = 'Topic must have a name'
153
+ render_error(
154
+ error_msg,
155
+ status: 422,
156
+ flash: { error: error_msg }
157
+ )
158
+ end
159
+
160
+ def topic_already_exists
161
+ error_msg = 'Topic already exists'
162
+ render_error(
163
+ error_msg,
164
+ status: 422,
165
+ flash: { error: error_msg }
166
+ )
167
+ end
168
+
169
+ def unknown_error
170
+ error_msg = 'An unknown error occurred with the request to Kafka. Check any request parameters.'
171
+
172
+ render_error(
173
+ error_msg,
174
+ status: 422,
175
+ flash: { error: error_msg }
176
+ )
177
+ end
178
+
179
+ def topic_deletion_error(exception)
180
+ render_error(
181
+ exception.message,
182
+ status: 422,
183
+ flash: { error: exception.message }
184
+ )
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KafkaCommand
4
+ module ApplicationHelper
5
+ def format_flash_errors
6
+ return '' if flash[:error].blank?
7
+ return flash[:error] if flash[:error].is_a?(String)
8
+
9
+ '<ul>'.tap do |str|
10
+ flash[:error].each do |k, v|
11
+ str << "<li><span class='has-text-weight-bold'>#{k.humanize}:</span> #{v.join('. ')}</li>"
12
+ end
13
+ end.html_safe
14
+ end
15
+
16
+ def topic_path(topic)
17
+ "#{cluster_path(@cluster)}/topics/#{CGI.escape(topic.name)}"
18
+ end
19
+
20
+ def consumer_groups_path(group)
21
+ "#{cluster_path(@cluster)}/consumer_groups/#{CGI.escape(group.group_id)}"
22
+ end
23
+
24
+ def trim_name(name)
25
+ return name if name.length < 25
26
+ name[0..22] + '...'
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KafkaCommand
4
+ module ConsumerGroupHelper
5
+ def status_color(group)
6
+ if group.empty?
7
+ 'has-text-info'
8
+ elsif group.stable?
9
+ 'has-text-success'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KafkaCommand
4
+ class ApplicationJob < ActiveJob::Base
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KafkaCommand
4
+ class ApplicationMailer < ActionMailer::Base
5
+ default from: 'from@example.com'
6
+ layout 'mailer'
7
+ end
8
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module KafkaCommand
6
+ class Broker
7
+ extend Forwardable
8
+ attr_reader :broker
9
+ def_delegators :@broker, :port, :host, :node_id, :fetch_metadata, :fetch_offsets
10
+ alias_method :kafka_broker_id, :node_id
11
+ alias_method :hostname, :host
12
+
13
+
14
+ def initialize(broker)
15
+ @broker = broker
16
+ end
17
+
18
+ def host_with_port
19
+ "#{host}:#{port}"
20
+ end
21
+
22
+ def as_json(*)
23
+ {
24
+ id: node_id,
25
+ host: host_with_port
26
+ }
27
+ end
28
+
29
+ # needs to be the group coordinator to work
30
+ def offsets_for(group, topic)
31
+ offsets = @broker.fetch_offsets(
32
+ group_id: group.group_id,
33
+ topics: { topic.name => topic.partitions.map(&:partition_id) }
34
+ ).topics[topic.name]
35
+
36
+ offsets.keys.each { |partition_id| offsets[partition_id] = offsets[partition_id].offset }
37
+ offsets
38
+ end
39
+
40
+ def connected?
41
+ @broker.api_versions # simple request to check connections
42
+ true
43
+ rescue Kafka::ConnectionError
44
+ false
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module KafkaCommand
6
+ class Client
7
+ extend Forwardable
8
+
9
+ CLUSTER_METHOD_DELGATIONS = %i(
10
+ broker_pool
11
+ delete_topic
12
+ alter_topic
13
+ describe_topic
14
+ create_partitions_for
15
+ resolve_offset
16
+ resolve_offsets
17
+ describe_group
18
+ supports_api?
19
+ ).freeze
20
+
21
+ attr_reader :cluster, :client
22
+ def_delegators :@client, :create_topic
23
+ def_delegators :@cluster, *CLUSTER_METHOD_DELGATIONS
24
+
25
+ def initialize(brokers:, **kwargs)
26
+ @client = Kafka.new(brokers, **kwargs)
27
+ @cluster = @client.cluster
28
+ end
29
+
30
+ def refresh!
31
+ refresh_brokers!
32
+ refresh_topics!
33
+ refresh_groups!
34
+ end
35
+
36
+ def topics
37
+ @topics ||= initialize_topics
38
+ end
39
+
40
+ def brokers
41
+ @brokers ||= initialize_brokers
42
+ end
43
+
44
+ def groups
45
+ @groups ||= initialize_groups
46
+ end
47
+
48
+ def refresh_groups!
49
+ @groups = initialize_groups
50
+ end
51
+
52
+ def refresh_topics!
53
+ @topics = initialize_topics
54
+ end
55
+
56
+ def refresh_brokers!
57
+ @brokers = initialize_brokers
58
+ end
59
+
60
+ def fetch_metadata(topics: nil)
61
+ brokers.sample.fetch_metadata(topics: topics)
62
+ end
63
+
64
+ def get_group_coordinator(group_id:)
65
+ broker = @cluster.get_group_coordinator(group_id: group_id)
66
+ Broker.new(broker)
67
+ end
68
+
69
+ def find_topic(topic_name)
70
+ topics.find { |t| t.name == topic_name }
71
+ end
72
+
73
+ def connect_to_broker(host:, port:, broker_id:)
74
+ Broker.new(broker_pool.connect(host, port, node_id: broker_id))
75
+ end
76
+
77
+ private
78
+
79
+ def initialize_brokers
80
+ cluster_info = @cluster.refresh_metadata!
81
+
82
+ cluster_info.brokers.map do |broker|
83
+ connect_to_broker(
84
+ host: broker.host,
85
+ port: broker.port,
86
+ broker_id: broker.node_id
87
+ )
88
+ end
89
+ end
90
+
91
+ def initialize_topics
92
+ # returns information about each topic
93
+ # i.e isr, leader, partitions
94
+ fetch_metadata.topics.map { |tm| Topic.new(tm, self) }
95
+ end
96
+
97
+ def initialize_groups
98
+ group_ids = @cluster.list_groups
99
+ group_ids.map { |id| ConsumerGroup.new(id, self) }
100
+ end
101
+ end
102
+ end