foreman_webhooks 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/LICENSE +619 -0
- data/README.md +30 -0
- data/Rakefile +49 -0
- data/app/controllers/api/v2/webhook_templates_controller.rb +110 -0
- data/app/controllers/api/v2/webhooks_controller.rb +61 -0
- data/app/controllers/concerns/foreman_webhooks/controller/parameters/webhook.rb +34 -0
- data/app/controllers/concerns/foreman_webhooks/controller/parameters/webhook_template.rb +34 -0
- data/app/controllers/webhook_templates_controller.rb +5 -0
- data/app/controllers/webhooks_controller.rb +39 -0
- data/app/jobs/foreman_webhooks/deliver_webhook_job.rb +27 -0
- data/app/lib/foreman_webhooks/renderer/scope/webhook_template.rb +36 -0
- data/app/models/webhook.rb +141 -0
- data/app/models/webhook_template.rb +59 -0
- data/app/services/foreman_webhooks/webhook_service.rb +111 -0
- data/app/subscribers/foreman_webhooks/event_subscriber.rb +9 -0
- data/app/views/api/v2/webhook_templates/base.json.rabl +5 -0
- data/app/views/api/v2/webhook_templates/create.json.rabl +5 -0
- data/app/views/api/v2/webhook_templates/index.json.rabl +5 -0
- data/app/views/api/v2/webhook_templates/main.json.rabl +7 -0
- data/app/views/api/v2/webhook_templates/show.json.rabl +11 -0
- data/app/views/api/v2/webhook_templates/update.json.rabl +5 -0
- data/app/views/api/v2/webhooks/base.json.rabl +5 -0
- data/app/views/api/v2/webhooks/create.json.rabl +5 -0
- data/app/views/api/v2/webhooks/index.json.rabl +5 -0
- data/app/views/api/v2/webhooks/main.json.rabl +8 -0
- data/app/views/api/v2/webhooks/show.json.rabl +17 -0
- data/app/views/api/v2/webhooks/update.json.rabl +5 -0
- data/app/views/foreman_webhooks/webhook_templates/ansible_tower_-_host_in_inventory.erb +20 -0
- data/app/views/foreman_webhooks/webhook_templates/empty_payload.erb +6 -0
- data/app/views/foreman_webhooks/webhook_templates/webhook_template_-_payload_default.erb +11 -0
- data/app/views/webhook_templates/_alerts.html.erb +3 -0
- data/app/views/webhook_templates/_custom_tab_headers.html.erb +1 -0
- data/app/views/webhook_templates/_custom_tabs.html.erb +5 -0
- data/app/views/webhook_templates/edit.html.erb +3 -0
- data/app/views/webhook_templates/index.html.erb +29 -0
- data/app/views/webhook_templates/new.html.erb +3 -0
- data/app/views/webhooks/_form.html.erb +37 -0
- data/app/views/webhooks/_templates.html.erb +5 -0
- data/app/views/webhooks/edit.html.erb +3 -0
- data/app/views/webhooks/new.html.erb +3 -0
- data/config/routes.rb +45 -0
- data/db/migrate/20191016100128_create_webhook_targets.rb +13 -0
- data/db/migrate/20200831194208_rename_webhook_targets_to_webhooks.rb +7 -0
- data/db/migrate/20200831194514_add_template_to_webhooks.rb +7 -0
- data/db/migrate/20200907232758_rename_webhook_permissions.rb +22 -0
- data/db/migrate/20200908004234_add_columns_to_webhooks.rb +13 -0
- data/db/migrate/20201014115147_rename_ca_file_column.rb +7 -0
- data/db/migrate/20201109135301_add_http_headers.rb +8 -0
- data/db/seeds.d/95_webhook_templates.rb +7 -0
- data/lib/foreman_webhooks.rb +6 -0
- data/lib/foreman_webhooks/engine.rb +80 -0
- data/lib/foreman_webhooks/version.rb +5 -0
- data/lib/tasks/foreman_webhooks_tasks.rake +47 -0
- data/package.json +45 -0
- data/test/controllers/api/v2/webhook_templates_controller_test.rb +200 -0
- data/test/controllers/api/v2/webhooks_controller_test.rb +108 -0
- data/test/factories/webhook.rb +20 -0
- data/test/factories/webhook_target.rb +9 -0
- data/test/factories/webhook_template.rb +16 -0
- data/test/jobs/foreman_webhooks/deliver_webhook_job_test.rb +17 -0
- data/test/models/webhook_test.rb +38 -0
- data/test/test_plugin_helper.rb +8 -0
- data/test/unit/foreman_webhooks/webhook_service_test.rb +53 -0
- data/webpack/ForemanWebhooks/Routes/ForemanWebhooksRoutes.js +12 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/EmptyWebhooksTable/index.js +29 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhookDeleteModal.js +43 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/Components/EnabledCell.js +16 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/Components/Formatters/__tests__/__snapshots__/enabledCellFormatter.test.js.snap +7 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/Components/Formatters/__tests__/enabledCellFormatter.test.js +7 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/Components/Formatters/enabledCellFormatter.js +6 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/Components/Formatters/index.js +1 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/Components/__tests__/EnabledCell.test.js +14 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/Components/__tests__/__snapshots__/EnabledCell.test.js.snap +17 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/WebhooksTable.js +75 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/WebhooksTableSchema.js +41 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/__tests__/WebhooksTable.test.js +57 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/__tests__/__snapshots__/WebhooksTable.test.js.snap +115 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/index.js +25 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/WebhooksIndexPage.js +87 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/__tests__/WebhooksIndexPage.fixtures.js +74 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/__tests__/WebhooksIndexPage.test.js +21 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/__tests__/__snapshots__/WebhooksIndexPage.test.js.snap +68 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/index.js +51 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksPageActions.js +51 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksPageHelpers.js +29 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksPageSelectors.js +85 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/__tests__/WebhooksPageHelpers.test.js +20 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/__tests__/WebhooksPageSelectors.test.js +45 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/__tests__/__snapshots__/WebhooksPageSelectors.test.js.snap +50 -0
- data/webpack/ForemanWebhooks/Routes/Webhooks/constants.js +13 -0
- data/webpack/__mocks__/foremanReact/common/HOC.js +2 -0
- data/webpack/__mocks__/foremanReact/common/I18n.js +7 -0
- data/webpack/__mocks__/foremanReact/common/helpers.js +7 -0
- data/webpack/__mocks__/foremanReact/common/urlHelpers.js +1 -0
- data/webpack/__mocks__/foremanReact/components/ForemanModal/ForemanModalActions.js +2 -0
- data/webpack/__mocks__/foremanReact/components/ForemanModal/ForemanModalHooks.js +10 -0
- data/webpack/__mocks__/foremanReact/components/ForemanModal/index.js +18 -0
- data/webpack/__mocks__/foremanReact/components/Layout/LayoutActions.js +2 -0
- data/webpack/__mocks__/foremanReact/components/Pagination/PaginationWrapper.js +2 -0
- data/webpack/__mocks__/foremanReact/components/common/EmptyState.js +5 -0
- data/webpack/__mocks__/foremanReact/components/common/table.js +5 -0
- data/webpack/__mocks__/foremanReact/constants.js +24 -0
- data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +6 -0
- data/webpack/__mocks__/foremanReact/redux/API/index.js +10 -0
- data/webpack/__mocks__/foremanReact/redux/actions/toasts.js +8 -0
- data/webpack/__mocks__/foremanReact/routes/common/PageLayout/PageLayout.js +10 -0
- data/webpack/index.js +0 -0
- data/webpack/routes_index.js +4 -0
- metadata +195 -0
data/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# ForemanWebhooks
|
|
2
|
+
|
|
3
|
+
Don't call us, we'll call you. This plugins allows to configure Webhooks for Foreman.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
See [How_to_Install_a_Plugin](http://projects.theforeman.org/projects/foreman/wiki/How_to_Install_a_Plugin)
|
|
8
|
+
for how to install Foreman plugins
|
|
9
|
+
|
|
10
|
+
## Contributing
|
|
11
|
+
|
|
12
|
+
Fork and send a Pull Request. Thanks!
|
|
13
|
+
|
|
14
|
+
## Copyright
|
|
15
|
+
|
|
16
|
+
Copyright (c) 2019 Timo Goebel
|
|
17
|
+
|
|
18
|
+
This program is free software: you can redistribute it and/or modify
|
|
19
|
+
it under the terms of the GNU General Public License as published by
|
|
20
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
21
|
+
(at your option) any later version.
|
|
22
|
+
|
|
23
|
+
This program is distributed in the hope that it will be useful,
|
|
24
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
25
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
26
|
+
GNU General Public License for more details.
|
|
27
|
+
|
|
28
|
+
You should have received a copy of the GNU General Public License
|
|
29
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
30
|
+
|
data/Rakefile
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env rake
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
begin
|
|
5
|
+
require 'bundler/setup'
|
|
6
|
+
rescue LoadError
|
|
7
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
|
8
|
+
end
|
|
9
|
+
begin
|
|
10
|
+
require 'rdoc/task'
|
|
11
|
+
rescue LoadError
|
|
12
|
+
require 'rdoc/rdoc'
|
|
13
|
+
require 'rake/rdoctask'
|
|
14
|
+
RDoc::Task = Rake::RDocTask
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
|
18
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
19
|
+
rdoc.title = 'ForemanWebhooks'
|
|
20
|
+
rdoc.options << '--line-numbers'
|
|
21
|
+
rdoc.rdoc_files.include('README.rdoc')
|
|
22
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
|
|
26
|
+
|
|
27
|
+
Bundler::GemHelper.install_tasks
|
|
28
|
+
|
|
29
|
+
require 'rake/testtask'
|
|
30
|
+
|
|
31
|
+
Rake::TestTask.new(:test) do |t|
|
|
32
|
+
t.libs << 'lib'
|
|
33
|
+
t.libs << 'test'
|
|
34
|
+
t.pattern = 'test/**/*_test.rb'
|
|
35
|
+
t.verbose = false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
task default: :test
|
|
39
|
+
|
|
40
|
+
begin
|
|
41
|
+
require 'rubocop/rake_task'
|
|
42
|
+
RuboCop::RakeTask.new
|
|
43
|
+
rescue StandardError => _e
|
|
44
|
+
puts 'Rubocop not loaded.'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
task :default do
|
|
48
|
+
Rake::Task['rubocop'].execute
|
|
49
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Api
|
|
4
|
+
module V2
|
|
5
|
+
class WebhookTemplatesController < V2::BaseController
|
|
6
|
+
include Api::Version2
|
|
7
|
+
include ForemanWebhooks::Controller::Parameters::WebhookTemplate
|
|
8
|
+
include Foreman::Controller::TemplateImport
|
|
9
|
+
|
|
10
|
+
before_action :find_resource, only: %i[show update destroy clone export]
|
|
11
|
+
|
|
12
|
+
api :GET, '/webhook_templates/', N_('List webhook templates')
|
|
13
|
+
param_group :search_and_pagination, ::Api::V2::BaseController
|
|
14
|
+
add_scoped_search_description_for(WebhookTemplate)
|
|
15
|
+
def index
|
|
16
|
+
@webhook_templates = resource_scope_for_index
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
api :GET, '/webhook_templates/:id', N_('Show webhook template details')
|
|
20
|
+
param :id, :identifier, required: true
|
|
21
|
+
def show; end
|
|
22
|
+
|
|
23
|
+
def_param_group :webhook_template do
|
|
24
|
+
param :webhook_template, Hash, action_aware: true, required: true do
|
|
25
|
+
param :name, String, required: true
|
|
26
|
+
param :description, String
|
|
27
|
+
param :template, String, required: true
|
|
28
|
+
param :snippet, :bool, allow_nil: true
|
|
29
|
+
param :audit_comment, String, allow_nil: true
|
|
30
|
+
param :locked, :bool, desc: N_('Whether or not the template is locked for editing')
|
|
31
|
+
param :default, :bool, desc: N_('Whether or not the template is added automatically to new organizations and locations')
|
|
32
|
+
param_group :taxonomies, ::Api::V2::BaseController
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def_param_group :webhook_template_clone do
|
|
37
|
+
param :webhook_template, Hash, required: true, action_aware: true do
|
|
38
|
+
param :name, String, required: true, desc: N_('Template name')
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
api :POST, '/webhook_templates/', N_('Create a webhook template')
|
|
43
|
+
param_group :webhook_template, as: :create
|
|
44
|
+
def create
|
|
45
|
+
@webhook_template = WebhookTemplate.new(webhook_template_params)
|
|
46
|
+
process_response @webhook_template.save
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
api :POST, '/webhook_templates/import', N_('Import a webhook template')
|
|
50
|
+
param :webhook_template, Hash, required: true, action_aware: true do
|
|
51
|
+
param :name, String, required: true, desc: N_('Template name')
|
|
52
|
+
param :template, String, required: true, desc: N_('Template contents including metadata')
|
|
53
|
+
param_group :taxonomies, ::Api::V2::BaseController
|
|
54
|
+
end
|
|
55
|
+
param_group :template_import_options, ::Api::V2::BaseController
|
|
56
|
+
def import
|
|
57
|
+
@webhook_template = WebhookTemplate.import!(*import_attrs_for(:webhook_template))
|
|
58
|
+
process_response @webhook_template
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
api :PUT, '/webhook_templates/:id', N_('Update a webhook template')
|
|
62
|
+
param :id, :identifier, required: true
|
|
63
|
+
param_group :webhook_template, as: :update
|
|
64
|
+
def update
|
|
65
|
+
process_response @webhook_template.update(webhook_template_params)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
api :DELETE, '/webhook_templates/:id', N_('Delete a webhook template')
|
|
69
|
+
param :id, :identifier, required: true
|
|
70
|
+
def destroy
|
|
71
|
+
process_response @webhook_template.destroy
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
api :POST, '/webhook_templates/:id/clone', N_('Clone a template')
|
|
75
|
+
param :id, :identifier, required: true
|
|
76
|
+
param_group :webhook_template_clone, as: :create
|
|
77
|
+
def clone
|
|
78
|
+
@webhook_template = @webhook_template.dup
|
|
79
|
+
@webhook_template.name = params[:webhook_template][:name]
|
|
80
|
+
process_response @webhook_template.save
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
api :GET, '/webhook_templates/:id/export', N_('Export a webhook template to ERB')
|
|
84
|
+
param :id, :identifier, required: true
|
|
85
|
+
def export
|
|
86
|
+
send_data @webhook_template.to_erb, type: 'text/plain',
|
|
87
|
+
disposition: 'attachment',
|
|
88
|
+
filename: @webhook_template.filename
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
# Overload this method to avoid using search_for method
|
|
94
|
+
def resource_scope_for_index(options = {})
|
|
95
|
+
resource_scope(options).paginate(paginate_options)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def action_permission
|
|
99
|
+
case params[:action]
|
|
100
|
+
when 'clone', 'import'
|
|
101
|
+
'create'
|
|
102
|
+
when 'export'
|
|
103
|
+
'view'
|
|
104
|
+
else
|
|
105
|
+
super
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Api
|
|
4
|
+
module V2
|
|
5
|
+
class WebhooksController < V2::BaseController
|
|
6
|
+
include Api::Version2
|
|
7
|
+
include ForemanWebhooks::Controller::Parameters::Webhook
|
|
8
|
+
include Foreman::Controller::TemplateImport
|
|
9
|
+
|
|
10
|
+
before_action :find_resource, only: %i[show update destroy]
|
|
11
|
+
|
|
12
|
+
api :GET, '/webhooks/', N_('List Webhooks')
|
|
13
|
+
param_group :search_and_pagination, ::Api::V2::BaseController
|
|
14
|
+
def index
|
|
15
|
+
@webhooks = resource_scope_for_index
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
api :GET, '/webhooks/:id', N_('Show Webhook details')
|
|
19
|
+
param :id, :identifier, required: true
|
|
20
|
+
def show; end
|
|
21
|
+
|
|
22
|
+
def_param_group :webhook do
|
|
23
|
+
param :webhook, Hash, action_aware: true, required: true do
|
|
24
|
+
param :name, String, required: true
|
|
25
|
+
param :target_url, String, required: true
|
|
26
|
+
param :http_method, Webhook::ALLOWED_HTTP_METHODS
|
|
27
|
+
param :http_content_type, String
|
|
28
|
+
param :event, String, required: true
|
|
29
|
+
param :webhook_template_id, :identifier
|
|
30
|
+
param :enabled, :boolean
|
|
31
|
+
param :verify_ssl, :boolean
|
|
32
|
+
param :ssl_ca_certs, String, N_('X509 Certification Authorities concatenated in PEM format')
|
|
33
|
+
param :user, String
|
|
34
|
+
param :password, String
|
|
35
|
+
param :http_headers, String
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
api :POST, '/webhooks/', N_('Create a Webhook')
|
|
40
|
+
param_group :webhook, as: :create
|
|
41
|
+
|
|
42
|
+
def create
|
|
43
|
+
@webhook = Webhook.new(webhook_params)
|
|
44
|
+
process_response @webhook.save
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
api :PUT, '/webhooks/:id', N_('Update a Webhook')
|
|
48
|
+
param :id, :identifier, required: true
|
|
49
|
+
param_group :webhook, as: :update
|
|
50
|
+
def update
|
|
51
|
+
process_response @webhook.update(webhook_params)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
api :DELETE, '/webhooks/:id', N_('Delete a Webhook')
|
|
55
|
+
param :id, :identifier, required: true
|
|
56
|
+
def destroy
|
|
57
|
+
process_response @webhook.destroy
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ForemanWebhooks
|
|
4
|
+
module Controller
|
|
5
|
+
module Parameters
|
|
6
|
+
module Webhook
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
class_methods do
|
|
10
|
+
def webhook_params_filter
|
|
11
|
+
Foreman::ParameterFilter.new(::Webhook).tap do |filter|
|
|
12
|
+
filter.permit :name,
|
|
13
|
+
:target_url,
|
|
14
|
+
:webhook_template_id,
|
|
15
|
+
:event,
|
|
16
|
+
:http_method,
|
|
17
|
+
:http_content_type,
|
|
18
|
+
:enabled,
|
|
19
|
+
:verify_ssl,
|
|
20
|
+
:ssl_ca_certs,
|
|
21
|
+
:user,
|
|
22
|
+
:password,
|
|
23
|
+
:http_headers
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def webhook_params
|
|
29
|
+
self.class.webhook_params_filter.filter_params(params, parameter_filter_context)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ForemanWebhooks
|
|
4
|
+
module Controller
|
|
5
|
+
module Parameters
|
|
6
|
+
module WebhookTemplate
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
include Foreman::Controller::Parameters::Taxonomix
|
|
9
|
+
include Foreman::Controller::Parameters::Template
|
|
10
|
+
|
|
11
|
+
class_methods do
|
|
12
|
+
def webhook_template_params_filter
|
|
13
|
+
Foreman::ParameterFilter.new(::WebhookTemplate).tap do |filter|
|
|
14
|
+
add_taxonomix_params_filter(filter)
|
|
15
|
+
add_template_params_filter(filter)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def webhook_template_params
|
|
21
|
+
self.class.webhook_template_params_filter.filter_params(params, parameter_filter_context)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def organization_params
|
|
25
|
+
self.class.organization_params_filter(::WebhookTemplate).filter_params(params, parameter_filter_context)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def location_params
|
|
29
|
+
self.class.location_params_filter(::WebhookTemplate).filter_params(params, parameter_filter_context)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class WebhooksController < ::ApplicationController
|
|
4
|
+
include ForemanWebhooks::Controller::Parameters::Webhook
|
|
5
|
+
include Foreman::Controller::AutoCompleteSearch
|
|
6
|
+
|
|
7
|
+
before_action :find_resource, only: %i[edit update destroy]
|
|
8
|
+
|
|
9
|
+
def new
|
|
10
|
+
@webhook = Webhook.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create
|
|
14
|
+
@webhook = Webhook.new(webhook_params)
|
|
15
|
+
if @webhook.save
|
|
16
|
+
process_success success_redirect: '/webhooks'
|
|
17
|
+
else
|
|
18
|
+
process_error
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def edit; end
|
|
23
|
+
|
|
24
|
+
def update
|
|
25
|
+
if @webhook.update(webhook_params)
|
|
26
|
+
process_success success_redirect: '/webhooks'
|
|
27
|
+
else
|
|
28
|
+
process_error
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def destroy
|
|
33
|
+
if @webhook.destroy
|
|
34
|
+
process_success
|
|
35
|
+
else
|
|
36
|
+
process_error
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ForemanWebhooks
|
|
4
|
+
class DeliverWebhookJob < ::ApplicationJob
|
|
5
|
+
queue_as :default
|
|
6
|
+
|
|
7
|
+
def perform(options)
|
|
8
|
+
webhook = Webhook.unscoped.find_by(id: options[:webhook_id])
|
|
9
|
+
WebhookService.new(
|
|
10
|
+
webhook: webhook,
|
|
11
|
+
headers: options[:headers],
|
|
12
|
+
url: options[:url],
|
|
13
|
+
event_name: options[:event_name],
|
|
14
|
+
payload: options[:payload]
|
|
15
|
+
).execute
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def webhook_id
|
|
19
|
+
arguments.first['webhook_id'] unless arguments.first.nil?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def humanized_name
|
|
23
|
+
webhook = webhook_id && Webhook.unscoped.find_by(id: webhook_id)
|
|
24
|
+
(webhook && (_('Deliver webhook %s') % webhook.name)) || _('Deliver webhook')
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ForemanWebhooks
|
|
4
|
+
module Renderer
|
|
5
|
+
module Scope
|
|
6
|
+
class WebhookTemplate < ::Foreman::Renderer::Scope::Template
|
|
7
|
+
extend ApipieDSL::Class
|
|
8
|
+
|
|
9
|
+
apipie :class, 'Macros related to Webhook payload customization' do
|
|
10
|
+
name 'Webhook Template'
|
|
11
|
+
sections only: %w[all webhooks]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(**args)
|
|
15
|
+
super
|
|
16
|
+
@defaults = {
|
|
17
|
+
event_name: @event_name,
|
|
18
|
+
webhook_id: @webhook_id,
|
|
19
|
+
context: @context
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
apipie :method, 'Creates final payload' do
|
|
24
|
+
required :hash, Hash, 'Key=value object with with data that should be present in payload'
|
|
25
|
+
keyword :with_defaults, [true, false], 'If set to true, adds default entries to the payload', default: true
|
|
26
|
+
returns String, 'JSON string with the final payload'
|
|
27
|
+
example 'payload({ id: @object.id, name: @object.name }) #=> "{ "id": 1, "name": "host.example.com", "context": { ... }, "event_name": "host_created.event.foreman" }"'
|
|
28
|
+
end
|
|
29
|
+
def payload(hash, with_defaults: true)
|
|
30
|
+
hash.merge!(@defaults) if with_defaults
|
|
31
|
+
hash.to_json
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Webhook < ApplicationRecord
|
|
4
|
+
audited
|
|
5
|
+
include Authorizable
|
|
6
|
+
include Encryptable
|
|
7
|
+
|
|
8
|
+
extend FriendlyId
|
|
9
|
+
friendly_id :name
|
|
10
|
+
include Parameterizable::ByIdName
|
|
11
|
+
|
|
12
|
+
EVENT_POSTFIX = ".#{Foreman::Observable::DEFAULT_NAMESPACE}"
|
|
13
|
+
|
|
14
|
+
EVENT_ALLOWLIST = %w[
|
|
15
|
+
host_created host_updated host_destroyed
|
|
16
|
+
hostgroup_created hostgroup_updated hostgroup_destroyed
|
|
17
|
+
user_created user_updated user_destroyed
|
|
18
|
+
domain_created domain_updated domain_destroyed
|
|
19
|
+
subnet_created subnet_updated subnet_destroyed
|
|
20
|
+
build_entered build_exited status_changed
|
|
21
|
+
].map { |e| e + EVENT_POSTFIX }.freeze
|
|
22
|
+
|
|
23
|
+
DEFAULT_PAYLOAD_TEMPLATE = 'Webhook Template - Payload Default'
|
|
24
|
+
|
|
25
|
+
ALLOWED_HTTP_METHODS = %w[POST GET PUT DELETE PATCH].freeze
|
|
26
|
+
|
|
27
|
+
encrypts :password
|
|
28
|
+
|
|
29
|
+
attribute :events, :string, array: true, default: []
|
|
30
|
+
|
|
31
|
+
validates_lengths_from_database
|
|
32
|
+
validates :name, :target_url, :events, presence: true
|
|
33
|
+
validates :target_url, format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]),
|
|
34
|
+
message: _('URL must be valid and schema must be one of: %s') % 'http, https' }
|
|
35
|
+
validates :http_method, inclusion: { in: ALLOWED_HTTP_METHODS }
|
|
36
|
+
|
|
37
|
+
belongs_to :webhook_template, foreign_key: :webhook_template_id
|
|
38
|
+
|
|
39
|
+
before_save :set_default_template, unless: :webhook_template
|
|
40
|
+
|
|
41
|
+
scope :for_event, ->(events) { where('events @> ARRAY[?]::varchar[]', Array(events)) }
|
|
42
|
+
|
|
43
|
+
default_scope -> { order('webhooks.name') }
|
|
44
|
+
|
|
45
|
+
scoped_search on: :name, complete_value: true, default_order: true
|
|
46
|
+
scoped_search on: :target_url, complete_value: :true
|
|
47
|
+
scoped_search on: :enabled, complete_value: { true: true, false: false }
|
|
48
|
+
|
|
49
|
+
def self.available_events
|
|
50
|
+
::Foreman::EventSubscribers.all_observable_events & EVENT_ALLOWLIST
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.deliver(event_name:, payload:)
|
|
54
|
+
for_event(event_name).includes([:webhook_template]).each do |target|
|
|
55
|
+
target.deliver(event_name: event_name, payload: payload) if target.enabled?
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def events=(events)
|
|
60
|
+
events = Array(events).map do |event|
|
|
61
|
+
next event if event.end_with?(EVENT_POSTFIX)
|
|
62
|
+
|
|
63
|
+
event.to_s.underscore + EVENT_POSTFIX
|
|
64
|
+
end
|
|
65
|
+
super(self.class.available_events & events)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def event=(event)
|
|
69
|
+
self.events = event
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def event
|
|
73
|
+
events.first
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def deliver(event_name:, payload:)
|
|
77
|
+
payload_to_deliver = rendered_payload(event_name, payload)
|
|
78
|
+
headers_to_deliver = rendered_headers(event_name, payload)
|
|
79
|
+
url_to_deliver = rendered_targed_url(event_name, payload)
|
|
80
|
+
::ForemanWebhooks::DeliverWebhookJob.perform_later(
|
|
81
|
+
event_name: event_name,
|
|
82
|
+
payload: payload_to_deliver,
|
|
83
|
+
headers: headers_to_deliver,
|
|
84
|
+
url: url_to_deliver,
|
|
85
|
+
webhook_id: id
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def ca_certs_store
|
|
90
|
+
store = OpenSSL::X509::Store.new
|
|
91
|
+
return store if ssl_ca_certs.blank?
|
|
92
|
+
|
|
93
|
+
ssl_ca_certs.split(/(?=-----BEGIN)/).each do |cert|
|
|
94
|
+
store.add_cert(OpenSSL::X509::Certificate.new(cert))
|
|
95
|
+
end
|
|
96
|
+
store
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
raise _(format('Failed to build X509 certificate store for HTTPS client, error: %s', e.message))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def set_default_template
|
|
104
|
+
self.webhook_template = WebhookTemplate.find_by!(name: DEFAULT_PAYLOAD_TEMPLATE)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def variables(event_name, payload)
|
|
108
|
+
{
|
|
109
|
+
event_name: event_name,
|
|
110
|
+
object: payload[:object],
|
|
111
|
+
context: payload[:context],
|
|
112
|
+
payload: payload,
|
|
113
|
+
webhook_id: id
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def render_source(source, event_name, payload)
|
|
118
|
+
scope = Foreman::Renderer.get_scope(
|
|
119
|
+
klass: ForemanWebhooks::Renderer::Scope::WebhookTemplate,
|
|
120
|
+
variables: variables(event_name, payload),
|
|
121
|
+
source: source
|
|
122
|
+
)
|
|
123
|
+
Foreman::Renderer.render(source, scope)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def rendered_payload(event_name, payload)
|
|
127
|
+
webhook_template.render(variables: variables(event_name, payload))
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def rendered_headers(event_name, payload)
|
|
131
|
+
return nil if http_headers.empty?
|
|
132
|
+
|
|
133
|
+
source = Foreman::Renderer::Source::String.new(name: 'HTTP header template', content: http_headers)
|
|
134
|
+
render_source(source, event_name, payload)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def rendered_targed_url(event_name, payload)
|
|
138
|
+
source = Foreman::Renderer::Source::String.new(name: 'HTTP target URL template', content: target_url)
|
|
139
|
+
render_source(source, event_name, payload)
|
|
140
|
+
end
|
|
141
|
+
end
|