Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #37900 - Allow syncing templates through HTTP proxy #191

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
adamruzicka marked this conversation as resolved.
Show resolved Hide resolved
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