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