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 authored and adamruzicka committed Nov 29, 2024
1 parent 85064cf commit 12c0989
Show file tree
Hide file tree
Showing 16 changed files with 323 additions and 27 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. If you choose 'selected', provide the `http_proxy_id` parameter.")
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
22 changes: 21 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,26 @@ 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}") }
proxy_policy_setting = Foreman.settings.find('template_sync_http_proxy_policy').dup
proxy_id_setting = http_proxy_id_setting
# if default value is 'Custom HTTP proxy' but there is no proxy to select, value must be changed
proxy_policy_setting.value = proxy_policy_setting.default = 'none' if proxy_id_setting.value == '' && proxy_policy_setting.value == 'selected'
settings << proxy_policy_setting
settings << 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
4 changes: 4 additions & 0 deletions lib/foreman_templates.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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? If you select Custom HTTP proxy, you will be prompted to select one.'),
default: 'global',
full_name: N_('HTTP proxy policy'),
collection: -> { ForemanTemplates.http_proxy_policy_types })
end
end

Expand Down
70 changes: 70 additions & 0 deletions test/unit/action_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,75 @@ class ActionTest < ActiveSupport::TestCase
end
end
end

context 'sync through http_proxy' do
before do
@template_sync_service = Action.new(:repo => 'https://github.com/theforeman/community-templates.git')
end

test 'should sync through custom http proxy' do
proxy = FactoryBot.create(:http_proxy)
@template_sync_service.instance_variable_set(:@http_proxy_policy, 'selected')
@template_sync_service.instance_variable_set(:@http_proxy_id, proxy.id)
assert_equal proxy.full_url, show_repo_proxy_url
end

test 'sync should fail if invalid http proxy id is provided' do
@template_sync_service.instance_variable_set(:@http_proxy_policy, 'selected')
@template_sync_service.instance_variable_set(:@http_proxy_id, 'invalid ID')
assert_raises(ActiveRecord::RecordNotFound) do
@template_sync_service.send(:init_git_repo)
end
end

test 'should sync through https proxy using custom CA certificate' do
custom_cert = 'Custom proxy CA cert'
proxy = FactoryBot.create(:http_proxy, :cacert => custom_cert, :url => 'https://localhost:8888')
@template_sync_service.instance_variable_set(:@http_proxy_policy, 'selected')
@template_sync_service.instance_variable_set(:@http_proxy_id, proxy.id)
assert_equal custom_cert, show_repo_proxy_cert
end

test 'should sync through global http proxy' do
Setting[:http_proxy] = 'https://localhost:8888'
@template_sync_service.instance_variable_set(:@http_proxy_policy, 'global')
assert_equal Setting[:http_proxy], show_repo_proxy_url
end

test 'should sync without using http proxy if global proxy is not set' do
Setting[:http_proxy] = ""
@template_sync_service.instance_variable_set(:@http_proxy_policy, 'global')
assert_nil show_repo_proxy_url
end

test 'should sync without using http proxy' do
@template_sync_service.instance_variable_set(:@http_proxy_policy, 'none')
assert_nil show_repo_proxy_url
end

private

def show_repo_proxy_url
dir = Dir.mktmpdir
@template_sync_service.instance_variable_set(:@dir, dir)
begin
repo = @template_sync_service.send(:init_git_repo)
repo.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
@template_sync_service.instance_variable_set(:@dir, dir)
begin
repo = @template_sync_service.send(:init_git_repo)
File.read(repo.config('http.proxysslcainfo'))
ensure
FileUtils.remove_entry_secure(dir) if File.exist?(dir)
end
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 @@ -24,9 +26,9 @@ 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"),
Expand All @@ -41,3 +43,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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Field as FormikField } from 'formik';

import ProxySettingField from './ProxySettingField';

const ProxySettingsFields = ({
proxyPolicySetting,
proxyIdSetting,
syncType,
resetField,
formProps: { isSubmitting },
}) => {
if (Object.keys(proxyPolicySetting).length === 0) {
return <></>;
}
const proxyPolicyFieldName = `${syncType}.http_proxy_policy`;
const proxyIdFieldName = `${syncType}.http_proxy_id`;

// removes the custom proxy option if no proxy is available
if (proxyIdSetting.value === '') {
proxyPolicySetting = proxyPolicySetting.set(
'selection',
proxyPolicySetting.selection.slice(0, 2)
);
}

return (
<React.Fragment>
<FormikField
name={proxyPolicyFieldName}
render={({ field, form }) => (
<ProxySettingField
setting={proxyPolicySetting}
resetField={resetField}
field={field}
form={form}
fieldName={proxyPolicyFieldName}
/>
)}
/>
<FormikField
name={proxyIdFieldName}
render={({ field, form }) => {
if (
proxyIdSetting.value !== '' &&
// Changing name to camel case here would unnecessarily complicate the code
// eslint-disable-next-line camelcase
form.values[syncType]?.http_proxy_policy === 'selected'
) {
return (
<ProxySettingField
setting={proxyIdSetting}
resetField={resetField}
field={field}
form={form}
fieldName={proxyIdFieldName}
/>
);
}
return <></>;
}}
/>
</React.Fragment>
);
};

ProxySettingsFields.propTypes = {
proxyPolicySetting: PropTypes.object,
proxyIdSetting: PropTypes.object,
syncType: PropTypes.string.isRequired,
resetField: PropTypes.func.isRequired,
formProps: PropTypes.object,
};

ProxySettingsFields.defaultProps = {
formProps: {},
proxyPolicySetting: {},
proxyIdSetting: {},
};

export default ProxySettingsFields;
Loading

0 comments on commit 12c0989

Please sign in to comment.