Skip to content

Commit

Permalink
Fixes #37900 - Allow syncing templates through HTTP proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
adamlazik1 committed Nov 5, 2024
1 parent aff0e8e commit ea24790
Show file tree
Hide file tree
Showing 17 changed files with 389 additions and 28 deletions.
4 changes: 3 additions & 1 deletion app/controllers/api/v2/template_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ class TemplateController < ::Api::V2::BaseController
param :repo, String, :required => false, :desc => N_("Override the default repo from settings.")
param :filter, String, :required => false, :desc => N_("Export templates with names matching this regex (case-insensitive; snippets are not filtered).")
param :negate, :bool, :required => false, :desc => N_("Negate the prefix (for purging).")
param :dirname, String, :required => false, :desc => N_("The directory within Git repo containing the templates")
param :dirname, String, :required => false, :desc => N_("Directory within Git repo containing the templates.")
param :http_proxy_policy, ForemanTemplates.http_proxy_policy_types.keys, :required => false, :desc => N_("HTTP proxy policy for template sync.")
param :http_proxy_id, :number, :required => false, :desc => N_("ID of an HTTP proxy to use for template sync. Use this parameter together with `'http_proxy_policy':'selected'`")
end

api :POST, "/templates/import/", N_("Initiate Import")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module TemplateParams

class_methods do
def filter_params_list
%i(verbose repo branch dirname filter negate metadata_export_mode)
%i(verbose repo branch dirname filter negate metadata_export_mode http_proxy_policy http_proxy_id)
end

def extra_import_params
Expand Down
17 changes: 16 additions & 1 deletion app/controllers/ui_template_syncs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ def render_errors(messages, severity = 'danger')
private

def setting_definitions(short_names)
short_names.map { |name| Foreman.settings.find("template_sync_#{name}") }
settings = short_names.map { |name| Foreman.settings.find("template_sync_#{name}") }
settings << http_proxy_id_setting
settings
end

def http_proxy_id_setting
proxy_list = HttpProxy.authorized(:view_http_proxies).with_taxonomy_scope.each_with_object({}) { |proxy, hash| hash[proxy.id] = proxy.name }
default_proxy_id = proxy_list.keys.first || ""
OpenStruct.new(id: 'template_sync_http_proxy_id',
name: 'template_sync_http_proxy_id',
description: N_('Select an HTTP proxy to use for template sync. You can add HTTP proxies on the Infrastructure > HTTP proxies page.'),
settings_type: :string,
value: default_proxy_id,
default: default_proxy_id,
full_name: N_('HTTP proxy'),
select_values: proxy_list)
end
end
33 changes: 32 additions & 1 deletion app/services/foreman_templates/action.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'securerandom'

module ForemanTemplates
class Action
delegate :logger, :to => :Rails
Expand All @@ -15,7 +17,7 @@ def self.repo_start_with
end

def self.setting_overrides
%i(verbose prefix dirname filter repo negate branch)
%i(verbose prefix dirname filter repo negate branch http_proxy_policy)
end

def method_missing(method, *args, &block)
Expand Down Expand Up @@ -53,9 +55,38 @@ def verify_path!(path)
private

def assign_attributes(args = {})
@http_proxy_id = args[:http_proxy_id]
self.class.setting_overrides.each do |attribute|
instance_variable_set("@#{attribute}", args[attribute.to_sym] || Setting["template_sync_#{attribute}".to_sym])
end
end

protected

def init_git_repo
git_repo = Git.init(@dir)

case @http_proxy_policy
when 'global'
http_proxy_url = Setting[:http_proxy]
when 'selected'
http_proxy = HttpProxy.authorized(:view_http_proxies).with_taxonomy_scope.find(@http_proxy_id)
http_proxy_url = http_proxy.full_url

if URI(http_proxy_url).scheme == 'https' && http_proxy.cacert.present?
proxy_cert = "#{@dir}/.git/foreman_templates_proxy_cert_#{SecureRandom.hex(8)}.crt"
File.write(proxy_cert, http_proxy.cacert)
git_repo.config('http.proxySSLCAInfo', proxy_cert)
end
end

if http_proxy_url.present?
git_repo.config('http.proxy', http_proxy_url)
end

git_repo.add_remote('origin', @repo)
logger.debug "cloned '#{@repo}' to '#{@dir}'"
git_repo
end
end
end
4 changes: 2 additions & 2 deletions app/services/foreman_templates/template_exporter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ def export_to_git
@dir = Dir.mktmpdir
return if branch_missing?

git_repo = Git.clone(@repo, @dir)
logger.debug "cloned '#{@repo}' to '#{@dir}'"
git_repo = init_git_repo
git_repo.fetch

setup_git_branch git_repo
dump_files!
Expand Down
6 changes: 3 additions & 3 deletions app/services/foreman_templates/template_importer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ def import_from_git
@dir = Dir.mktmpdir

begin
logger.debug "cloned '#{@repo}' to '#{@dir}'"
gitrepo = Git.clone(@repo, @dir)
if @branch
gitrepo = init_git_repo
gitrepo.fetch
if @branch.present?
logger.debug "checking out branch '#{@branch}'"
gitrepo.checkout(@branch)
end
Expand Down
6 changes: 5 additions & 1 deletion lib/foreman_templates.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require 'foreman_templates/engine'

module ForemanTemplates
BASE_SETTING_NAMES = %w(repo branch dirname filter negate).freeze
BASE_SETTING_NAMES = %w(repo branch dirname filter negate http_proxy_policy).freeze
IMPORT_SETTING_NAMES = (BASE_SETTING_NAMES | %w(prefix associate force lock)).freeze
EXPORT_SETTING_NAMES = (BASE_SETTING_NAMES | %w(metadata_export_mode commit_msg)).freeze

Expand All @@ -16,4 +16,8 @@ def self.lock_types
def self.metadata_export_mode_types
{ 'refresh' => _('Refresh'), 'keep' => _('Keep'), 'remove' => _('Remove') }
end

def self.http_proxy_policy_types
{ 'global' => _('Global default HTTP proxy'), 'none' => _('No HTTP proxy'), 'selected' => _('Custom HTTP proxy') }
end
end
6 changes: 6 additions & 0 deletions lib/foreman_templates/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ class Engine < ::Rails::Engine
description: N_('Custom commit message for templates export'),
default: 'Templates export made by a Foreman user',
full_name: N_('Commit message'))
setting('template_sync_http_proxy_policy',
type: :string,
description: N_('Should an HTTP proxy be used for template sync?'),
default: 'global',
full_name: N_('HTTP proxy policy'),
collection: -> { ForemanTemplates.http_proxy_policy_types })
end
end

Expand Down
12 changes: 12 additions & 0 deletions test/functional/api/v2/template_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ class TemplateControllerTest < ::ActionController::TestCase
assert_response :success
end

test "should import from git through proxy" do
proxy = FactoryBot.create(:http_proxy)
post :import, params: { 'repo' => "#{template_fixtures_path}/core", 'prefix' => '', 'http_proxy_policy' => 'selected', 'http_proxy_id' => proxy.id}

Check failure on line 18 in test/functional/api/v2/template_controller_test.rb

View workflow job for this annotation

GitHub Actions / Rubocop / Rubocop

Layout/SpaceInsideHashLiteralBraces: Space inside } missing.
assert_response :success
end

test "import should fail when provided with invalid proxy id" do
post :import, params: { 'repo' => "#{template_fixtures_path}/core", 'prefix' => '', 'http_proxy_policy' => 'selected', 'http_proxy_id' => 'invalid ID'}

Check failure on line 23 in test/functional/api/v2/template_controller_test.rb

View workflow job for this annotation

GitHub Actions / Rubocop / Rubocop

Layout/SpaceInsideHashLiteralBraces: Space inside } missing.
assert_response :error
puts response.msg
end

test "should export to filesystem" do
Dir.mktmpdir do |tmpdir|
post :export, params: { 'repo' => tmpdir, 'metadata_export_mode' => 'keep' }
Expand Down
83 changes: 83 additions & 0 deletions test/unit/template_importer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,65 @@ def audit_comment
assert_equal succ, res.first
end

test 'import should fail if invalid http proxy id is provided' do
setup_settings
imp = ForemanTemplates::TemplateImporter.new
imp.instance_variable_set(:@http_proxy_id, 'invalid ID')
imp.instance_variable_set(:@http_proxy_policy, 'selected')
assert_raises(ActiveRecord::RecordNotFound) do
res = imp.import!

Check failure on line 167 in test/unit/template_importer_test.rb

View workflow job for this annotation

GitHub Actions / Rubocop / Rubocop

Lint/UselessAssignment: Useless assignment to variable - `res`.
end
end

test 'should import through custom http proxy' do
setup_settings
@imp = ForemanTemplates::TemplateImporter.new
proxy = FactoryBot.create(:http_proxy)
@imp.instance_variable_set(:@http_proxy_policy, 'selected')
@imp.instance_variable_set(:@http_proxy_id, proxy.id)
@imp.stubs(:import_from_git).returns(show_repo_proxy_url)
repo_proxy_url = @imp.import!
assert_equal proxy.full_url, repo_proxy_url
end

test 'should import through global http proxy' do
setup_settings
@imp = ForemanTemplates::TemplateImporter.new
@imp.stubs(:import_from_git).returns(show_repo_proxy_url)
repo_proxy_url = @imp.import!
assert_equal Setting[:http_proxy], repo_proxy_url
end

test 'should import without using http proxy if global proxy is not set' do
setup_settings
Setting['http_proxy'] = ""
@imp = ForemanTemplates::TemplateImporter.new
@imp.stubs(:import_from_git).returns(show_repo_proxy_url)
repo_proxy_url = @imp.import!
assert_nil repo_proxy_url
end

test 'should import without using http proxy' do
setup_settings
@imp = ForemanTemplates::TemplateImporter.new
@imp.instance_variable_set(:@http_proxy_policy, 'none')
@imp.stubs(:import_from_git).returns(show_repo_proxy_url)
repo_proxy_url = @imp.import!
assert_nil repo_proxy_url
end

test 'should import through https proxy using custom CA certificate' do
setup_settings
@imp = ForemanTemplates::TemplateImporter.new
custom_cert = 'Custom proxy CA cert'
proxy = FactoryBot.create(:http_proxy, :cacert => custom_cert, :url => 'https://localhost:8888')
@imp.instance_variable_set(:@http_proxy_policy, 'selected')
@imp.instance_variable_set(:@http_proxy_id, proxy.id)
@imp.stubs(:import_from_git).returns(show_repo_proxy_cert)
proxy_cert = @imp.import!
assert_equal custom_cert, proxy_cert
end

test 'should import files from filesystem' do
setup_settings :repo => @engine_root, :dirname => '/test/templates/core'
imp = ForemanTemplates::TemplateImporter.new
Expand Down Expand Up @@ -351,6 +410,8 @@ def setup_settings(opts = {})
Setting['template_sync_repo'] = default_repo
Setting['template_sync_negate'] = false
Setting['template_sync_branch'] = default_branch
Setting['template_sync_http_proxy_policy'] = 'global'
Setting['http_proxy'] = 'https://localhost:8888'
end

def assert_both_equal_nil(expected, actual)
Expand All @@ -366,5 +427,27 @@ def assert_both_equal_nil(expected, actual)
def find_result(results, template_name)
results.find { |res| res.name == template_name }
end

def show_repo_proxy_url
dir = Dir.mktmpdir
@imp.instance_variable_set(:@dir, dir)
begin
gitrepo = @imp.send(:init_git_repo)
gitrepo.config.to_h['http.proxy']
ensure
FileUtils.remove_entry_secure(dir) if File.exist?(dir)
end
end

def show_repo_proxy_cert
dir = Dir.mktmpdir
@imp.instance_variable_set(:@dir, dir)
begin
gitrepo = @imp.send(:init_git_repo)
File.read(gitrepo.config('http.proxysslcainfo'))
ensure
FileUtils.remove_entry_secure(dir) if File.exist?(dir)
end
end
end
end
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import * as Yup from 'yup';
import React from 'react';
import { translate as __ } from 'foremanReact/common/I18n';

export const redirectToResult = history => () =>
history.push({ pathname: '/template_syncs/result' });
Expand All @@ -15,6 +17,9 @@ const repoFormat = formatAry => value => {
return value && valid;
};

const httpProxyAvailable = proxyId => value =>
value !== 'selected' || proxyId.value !== '';

export const syncFormSchema = (syncType, settingsObj, validationData) => {
const schema = (settingsObj[syncType].asMutable() || []).reduce(
(memo, setting) => {
Expand All @@ -24,14 +29,30 @@ export const syncFormSchema = (syncType, settingsObj, validationData) => {
repo: Yup.string()
.test(
'repo-format',
`Invalid repo format, must start with one of: ${validationData.repo.join(
', '
)}`,
`${__(
'Invalid repo format, must start with one of: '
)}${validationData.repo.join(', ')}`,
repoFormat(validationData.repo)
)
.required("can't be blank"),
};
}
if (setting.name === 'http_proxy_policy') {
return {
...memo,
http_proxy_policy: Yup.mixed().test(
'http-proxy-available',
__(
'No HTTP proxies available. Please select a different HTTP proxy policy or switch to a different taxonomy context.'
),
httpProxyAvailable(
settingsObj[syncType].find(
obj => obj.id === 'template_sync_http_proxy_id'
)
)
),
};
}
return memo;
},
{}
Expand All @@ -41,3 +62,13 @@ export const syncFormSchema = (syncType, settingsObj, validationData) => {
[syncType]: Yup.object().shape(schema),
});
};

export const tooltipContent = setting => (
<div
dangerouslySetInnerHTML={{
__html: __(setting.description),
}}
/>
);

export const label = setting => `${__(setting.fullName)}`;
44 changes: 44 additions & 0 deletions webpack/components/NewTemplateSync/components/ProxySettingField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import { get } from 'lodash';

import { FieldLevelHelp } from 'patternfly-react';
import RenderField from './TextButtonField/RenderField';
import ButtonTooltip from './ButtonTooltip';

import {
tooltipContent,
label,
} from './NewTemplateSyncForm/NewTemplateSyncFormHelpers';

const ProxySettingField = ({ setting, resetField, field, form, fieldName }) => (
<RenderField
label={label(setting)}
fieldSelector={_ => 'select'}
tooltipHelp={<FieldLevelHelp content={tooltipContent(setting)} />}
buttonAttrs={{
buttonText: <ButtonTooltip tooltipId={fieldName} />,
buttonAction: () =>
resetField(fieldName, setting.value)(form.setFieldValue),
}}
blank={{}}
item={setting}
disabled={false}
fieldRequired
meta={{
touched: get(form.touched, fieldName),
error: get(form.errors, fieldName),
}}
input={field}
/>
);

ProxySettingField.propTypes = {
setting: PropTypes.object.isRequired,
resetField: PropTypes.func.isRequired,
field: PropTypes.object.isRequired,
form: PropTypes.object.isRequired,
fieldName: PropTypes.string.isRequired,
};

export default ProxySettingField;
Loading

0 comments on commit ea24790

Please sign in to comment.