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 Oct 29, 2024
1 parent f909477 commit 29dc629
Show file tree
Hide file tree
Showing 13 changed files with 258 additions and 25 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.")
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
34 changes: 33 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,39 @@ 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)
git_repo.fetch
logger.debug "cloned '#{@repo}' to '#{@dir}'"
git_repo
end
end
end
3 changes: 1 addition & 2 deletions app/services/foreman_templates/template_exporter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ 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

setup_git_branch git_repo
dump_files!
Expand Down
5 changes: 2 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,8 @@ 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
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
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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Field as FormikField } from 'formik';
import { translate as __ } from 'foremanReact/common/I18n';
import CommonForm from 'foremanReact/components/common/forms/CommonForm';
import { InputGroup } from 'patternfly-react';
import { FieldLevelHelp } from 'patternfly-react/dist/js/components/FieldLevelHelp';

import {
tooltipContent,
label,
} from './NewTemplateSyncForm/NewTemplateSyncFormHelpers';
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`;

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 }) => {
// Changing name to camel case here would unnecessarily complicate the code
// eslint-disable-next-line camelcase
if (form.values[syncType]?.http_proxy_policy === 'selected') {
if (proxyIdSetting.value === '') {
return (
<CommonForm
label={label(proxyIdSetting)}
required
tooltipHelp={
<FieldLevelHelp content={tooltipContent(proxyIdSetting)} />
}
>
<InputGroup>
<div>{__('No HTTP proxies available')}</div>
</InputGroup>
</CommonForm>
);
}
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;
17 changes: 5 additions & 12 deletions webpack/components/NewTemplateSync/components/SyncSettingField.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FieldLevelHelp } from 'patternfly-react';
import { translate as __ } from 'foremanReact/common/I18n';

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

const SyncSettingField = ({ setting, resetField, disabled, syncType }) => {
const label = settingObj => `${__(settingObj.fullName)} `;

const fieldSelector = settingObj => {
if (settingObj.settingsType === 'boolean') {
return 'checkbox';
Expand All @@ -21,14 +22,6 @@ const SyncSettingField = ({ setting, resetField, disabled, syncType }) => {
return 'text';
};

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

return (
<TextButtonField
name={`${syncType}.${setting.name}`}
Expand All @@ -40,7 +33,7 @@ const SyncSettingField = ({ setting, resetField, disabled, syncType }) => {
fieldSelector={fieldSelector}
disabled={disabled}
fieldRequired={setting.required}
tooltipHelp={<FieldLevelHelp content={tooltipContent} />}
tooltipHelp={<FieldLevelHelp content={tooltipContent(setting)} />}
>
{setting.value}
</TextButtonField>
Expand Down
Loading

0 comments on commit 29dc629

Please sign in to comment.