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.
- checksums.yaml +7 -0
- data/.circleci/config.yml +179 -0
- data/.env +1 -0
- data/.env.test +1 -0
- data/.gitignore +41 -0
- data/.rspec +1 -0
- data/.rubocop.yml +12 -0
- data/.ruby-version +1 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +194 -0
- data/LICENSE +21 -0
- data/README.md +138 -0
- data/Rakefile +34 -0
- data/app/assets/config/manifest.js +3 -0
- data/app/assets/images/.keep +0 -0
- data/app/assets/images/kafka_command/cluster_view.png +0 -0
- data/app/assets/images/kafka_command/kafka.png +0 -0
- data/app/assets/images/kafka_command/topic_view.png +0 -0
- data/app/assets/javascripts/kafka_command/application.js +14 -0
- data/app/assets/stylesheets/kafka_command/application.css +27 -0
- data/app/assets/stylesheets/kafka_command/clusters.css +8 -0
- data/app/assets/stylesheets/kafka_command/topics.css +3 -0
- data/app/channels/application_cable/channel.rb +6 -0
- data/app/channels/application_cable/connection.rb +6 -0
- data/app/controllers/kafka_command/application_controller.rb +96 -0
- data/app/controllers/kafka_command/brokers_controller.rb +26 -0
- data/app/controllers/kafka_command/clusters_controller.rb +46 -0
- data/app/controllers/kafka_command/consumer_groups_controller.rb +44 -0
- data/app/controllers/kafka_command/topics_controller.rb +187 -0
- data/app/helpers/kafka_command/application_helper.rb +29 -0
- data/app/helpers/kafka_command/consumer_group_helper.rb +13 -0
- data/app/jobs/application_job.rb +6 -0
- data/app/mailers/application_mailer.rb +8 -0
- data/app/models/kafka_command/broker.rb +47 -0
- data/app/models/kafka_command/client.rb +102 -0
- data/app/models/kafka_command/cluster.rb +172 -0
- data/app/models/kafka_command/consumer_group.rb +142 -0
- data/app/models/kafka_command/consumer_group_partition.rb +23 -0
- data/app/models/kafka_command/group_member.rb +18 -0
- data/app/models/kafka_command/partition.rb +36 -0
- data/app/models/kafka_command/topic.rb +153 -0
- data/app/views/kafka_command/brokers/index.html.erb +38 -0
- data/app/views/kafka_command/clusters/_tabs.html.erb +9 -0
- data/app/views/kafka_command/clusters/index.html.erb +54 -0
- data/app/views/kafka_command/clusters/new.html.erb +115 -0
- data/app/views/kafka_command/configuration_error.html.erb +1 -0
- data/app/views/kafka_command/consumer_groups/index.html.erb +32 -0
- data/app/views/kafka_command/consumer_groups/show.html.erb +115 -0
- data/app/views/kafka_command/shared/_alert.html.erb +13 -0
- data/app/views/kafka_command/shared/_search_bar.html.erb +31 -0
- data/app/views/kafka_command/shared/_title.html.erb +6 -0
- data/app/views/kafka_command/topics/_form_fields.html.erb +49 -0
- data/app/views/kafka_command/topics/edit.html.erb +17 -0
- data/app/views/kafka_command/topics/index.html.erb +46 -0
- data/app/views/kafka_command/topics/new.html.erb +36 -0
- data/app/views/kafka_command/topics/show.html.erb +126 -0
- data/app/views/layouts/kafka_command/application.html.erb +50 -0
- data/bin/rails +16 -0
- data/config/initializers/kafka.rb +13 -0
- data/config/initializers/kafka_command.rb +11 -0
- data/config/routes.rb +11 -0
- data/docker-compose.yml +18 -0
- data/kafka_command.gemspec +27 -0
- data/lib/assets/.keep +0 -0
- data/lib/core_extensions/kafka/broker/attr_readers.rb +11 -0
- data/lib/core_extensions/kafka/broker_pool/attr_readers.rb +11 -0
- data/lib/core_extensions/kafka/client/attr_readers.rb +11 -0
- data/lib/core_extensions/kafka/cluster/attr_readers.rb +11 -0
- data/lib/core_extensions/kafka/protocol/metadata_response/partition_metadata/attr_readers.rb +15 -0
- data/lib/kafka_command/configuration.rb +150 -0
- data/lib/kafka_command/engine.rb +11 -0
- data/lib/kafka_command/errors.rb +6 -0
- data/lib/kafka_command/version.rb +5 -0
- data/lib/kafka_command.rb +13 -0
- data/lib/tasks/.keep +0 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +4 -0
- data/spec/dummy/app/assets/javascripts/application.js +15 -0
- data/spec/dummy/app/assets/javascripts/cable.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
- data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
- data/spec/dummy/app/controllers/application_controller.rb +2 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/jobs/application_job.rb +2 -0
- data/spec/dummy/app/mailers/application_mailer.rb +4 -0
- data/spec/dummy/app/models/application_record.rb +3 -0
- data/spec/dummy/app/views/layouts/application.html.erb +15 -0
- data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +36 -0
- data/spec/dummy/bin/update +31 -0
- data/spec/dummy/bin/yarn +11 -0
- data/spec/dummy/config/application.rb +19 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/cable.yml +10 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +61 -0
- data/spec/dummy/config/environments/production.rb +94 -0
- data/spec/dummy/config/environments/test.rb +46 -0
- data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
- data/spec/dummy/config/initializers/assets.rb +14 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/content_security_policy.rb +25 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/kafka_command.yml +18 -0
- data/spec/dummy/config/locales/en.yml +33 -0
- data/spec/dummy/config/puma.rb +34 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/config/spring.rb +6 -0
- data/spec/dummy/config/ssl/test_ca_cert +1 -0
- data/spec/dummy/config/ssl/test_client_cert +1 -0
- data/spec/dummy/config/ssl/test_client_cert_key +1 -0
- data/spec/dummy/config/storage.yml +34 -0
- data/spec/dummy/config.ru +5 -0
- data/spec/dummy/db/schema.rb +42 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +0 -0
- data/spec/dummy/log/hey.log +0 -0
- data/spec/dummy/log/test.log +2227 -0
- data/spec/dummy/package.json +5 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/spec/dummy/public/apple-touch-icon.png +0 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/examples.txt +165 -0
- data/spec/fast_helper.rb +20 -0
- data/spec/fixtures/files/kafka_command_sasl.yml +10 -0
- data/spec/fixtures/files/kafka_command_ssl.yml +10 -0
- data/spec/fixtures/files/kafka_command_ssl_file_paths.yml +11 -0
- data/spec/fixtures/files/kafka_command_staging.yml +8 -0
- data/spec/lib/kafka_command/configuration_spec.rb +311 -0
- data/spec/models/kafka_command/broker_spec.rb +83 -0
- data/spec/models/kafka_command/client_spec.rb +306 -0
- data/spec/models/kafka_command/cluster_spec.rb +163 -0
- data/spec/models/kafka_command/consumer_group_partition_spec.rb +43 -0
- data/spec/models/kafka_command/consumer_group_spec.rb +236 -0
- data/spec/models/kafka_command/partition_spec.rb +95 -0
- data/spec/models/kafka_command/topic_spec.rb +311 -0
- data/spec/rails_helper.rb +63 -0
- data/spec/requests/json/brokers_spec.rb +50 -0
- data/spec/requests/json/clusters_spec.rb +58 -0
- data/spec/requests/json/consumer_groups_spec.rb +139 -0
- data/spec/requests/json/topics_spec.rb +274 -0
- data/spec/spec_helper.rb +109 -0
- data/spec/support/factory_bot.rb +5 -0
- data/spec/support/json_helper.rb +13 -0
- data/spec/support/kafka_helper.rb +93 -0
- 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,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,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
|