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 #36971 - GUI to allow cloning of Ansible roles from VCS #676

Closed
Closed
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
150 changes: 150 additions & 0 deletions app/controllers/api/v2/vcs_clone_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
module Api
module V2
class VcsCloneController < ::Api::V2::BaseController
include ::ForemanAnsible::ProxyAPI
include ::Api::Version2

resource_description do
api_version 'v2'
api_base_url '/ansible/api'
end

def_param_group :repo_information do
param :repo_info, Hash, :desc => N_('Hash containing info about the Ansible role to be installed') do
param :vcs_url, String, :desc => N_('URL of the repository'), :required => true
param :role_name, String, :desc => N_('Name of the Ansible role'), :required => true
param :ref, String, :desc => N_('Branch / Tag / Commit reference'), :required => true
end
end

Thorben-D marked this conversation as resolved.
Show resolved Hide resolved
rescue_from ActionController::ParameterMissing do |e|
render json: { 'error' => e.message }, status: :bad_request
end

skip_before_action :verify_authenticity_token

before_action :set_proxy_api

api :GET, '/smart_proxies/:smart_proxy_id/ansible/vcs_clone/repository_metadata',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
api :GET, '/smart_proxies/:smart_proxy_id/ansible/vcs_clone/repository_metadata',
api :GET, '/smart_proxies/:smart_proxy_id/vcs_clone/repository_metadata',

There is no need to include ansible in the url path. IMHO, adding the api_base_url '/ansible/api', is enough.

N_('Retrieve metadata about the repository associated with a Smart Proxy.')
param :smart_proxy_id, Array, N_('Name of the Smart Proxy'), :required => true
param :vcs_url, String, N_('URL of the repository'), :required => true
error 400, :desc => N_('Invalid or missing parameters')
def repository_metadata
vcs_url = params.require(:vcs_url)
render json: @proxy_api.repo_information(vcs_url)
end

api :GET, '/smart_proxies/:smart_proxy_id/ansible/vcs_clone/roles',
N_('Returns an array of Ansible roles installed on the provided Smart Proxy')
formats ['json']
param :smart_proxy_id, Array, N_('Name of the SmartProxy'), :required => true
error 400, :desc => N_('Invalid or missing parameters')
def installed_roles
render json: @proxy_api.list_installed
end

api :POST, '/smart_proxies/:smart_proxy_id/ansible/vcs_clone/roles',
N_('Launches a task to install the provided role')
formats ['json']
param_group :repo_information
param :smart_proxy_id, Array, N_('Smart Proxy where the role should be installed')
error 400, :desc => N_('Invalid or missing parameters')
def install_role
payload = verify_install_role_parameters(params)
start_vcs_task(payload, :install)
end

api :PUT, '/smart_proxies/:smart_proxy_id/ansible/vcs_clone/roles',
N_('Launches a task to update the provided role')
formats ['json']
param_group :repo_information
param :smart_proxy_id, Array, N_('Smart Proxy where the role should be installed')
error 400, :desc => N_('Invalid or missing parameters')
def update_role
payload = verify_update_role_parameters(params)
payload['name'] = params.require(:role_name)
start_vcs_task(payload, :update)
end

api :DELETE, '/smart_proxies/:smart_proxy_id/ansible/vcs_clone/roles/:role_name',
N_('Launches a task to delete the provided role')
formats ['json']
param :role_name, String, :desc => N_('Name of the role to be deleted')
param :smart_proxy_id, Array, N_('Smart Proxy to delete the role from')
error 400, :desc => N_('Invalid or missing parameters')
def delete_role
payload = params.require(:role_name)
start_vcs_task(payload, :delete)
end

private

def set_proxy_api
unless params[:id]
msg = _('Smart proxy id is required')
return render_error('custom_error', :status => :unprocessable_entity, :locals => { :message => msg })
end
ansible_proxy = SmartProxy.find_by(id: params[:id])
if ansible_proxy.nil?
msg = _('Smart proxy does not exist')
return render_error('custom_error', :status => :bad_request, :locals => { :message => msg })
else unless ansible_proxy.has_capability?('Ansible', 'vcs_clone')
msg = _('Smart Proxy is missing foreman_ansible installation or Git cloning capability')
return render_error('custom_error', :status => :bad_request, :locals => { :message => msg })
end
end
@proxy = ansible_proxy
@proxy_api = find_proxy_api(ansible_proxy)
end

def permit_parameters(params)
params.require(:vcs_clone).
permit(
repo_info: [
:vcs_url,
:role_name,
:ref
]
).to_h
end

def verify_install_role_parameters(params)
payload = permit_parameters params
%w[vcs_url role_name ref].each do |param|
raise ActionController::ParameterMissing.new(param) unless payload['repo_info'].key?(param)
end
payload
end

def verify_update_role_parameters(params)
payload = permit_parameters params
%w[vcs_url ref].each do |param|
raise ActionController::ParameterMissing.new(param) unless payload['repo_info'].key?(param)
end
payload
end

def start_vcs_task(op_info, operation)
case operation
when :update
job = UpdateAnsibleRole.perform_later(op_info, @proxy)
when :install
job = CloneAnsibleRole.perform_later(op_info, @proxy)
when :delete
job = DeleteAnsibleRole.perform_later(op_info, @proxy)
else
raise Foreman::Exception.new(N_('Unsupported operation'))
end

task = ForemanTasks::Task.find_by(external_id: job.provider_job_id)

render json: {
task: task
}, status: :ok
rescue Foreman::Exception
head :internal_server_error
end
end
end
end
6 changes: 6 additions & 0 deletions app/helpers/foreman_ansible/ansible_roles_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ def ansible_proxy_import(hash)
ansible_proxy_links(hash))
end

def vcs_import
select_action_button("",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not display this button if the user does not have the required permissions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed this as well. Will add later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I am not sure how to handle this... Ideally, this would be handled on the React side.

{ :primary => true, :class => 'roles-import' },
link_to(_("Download from Git"), "#vcs_download"))
end

def import_time(role)
_('%s ago') % time_ago_in_words(role.updated_at)
end
Expand Down
12 changes: 12 additions & 0 deletions app/jobs/clone_ansible_role.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class CloneAnsibleRole < ::ApplicationJob
queue_as :default

def humanized_name
_('Download Ansible Role from Git')
end

def perform(repo_info, proxy)
vcs_cloner = ForemanAnsible::VcsCloner.new(proxy)
vcs_cloner.install_role repo_info
end
end
12 changes: 12 additions & 0 deletions app/jobs/delete_ansible_role.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class DeleteAnsibleRole < ::ApplicationJob
queue_as :default

def humanized_name
_('Delete Ansible Role from Smart Proxy')
end

def perform(role_name, proxy)
vcs_cloner = ForemanAnsible::VcsCloner.new(proxy)
vcs_cloner.delete_role role_name
end
end
12 changes: 12 additions & 0 deletions app/jobs/update_ansible_role.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class UpdateAnsibleRole < ::ApplicationJob
queue_as :default

def humanized_name
_('Update Ansible Role from Git')
end

def perform(repo_info, proxy)
vcs_cloner = ForemanAnsible::VcsCloner.new(proxy)
vcs_cloner.update_role repo_info
end
end
59 changes: 58 additions & 1 deletion app/lib/proxy_api/ansible.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module ProxyAPI
# ProxyAPI for Ansible
class Ansible < ::ProxyAPI::Resource
def initialize(args)
@url = args[:url] + '/ansible/'
@url = "#{args[:url]}/ansible/"
super args
end

Expand Down Expand Up @@ -53,5 +53,62 @@ def playbooks(playbooks_names = [])
rescue *PROXY_ERRORS => e
raise ProxyException.new(url, e, N_('Unable to get playbooks from Ansible'))
end

def repo_information(vcs_url)
parse(get("vcs_clone/repo_information?vcs_url=#{vcs_url}"))
rescue *PROXY_ERRORS, RestClient::Exception => e
raise e unless e.is_a? RestClient::RequestFailed
Comment on lines +59 to +60
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd only catch the specific exception:

Suggested change
rescue *PROXY_ERRORS, RestClient::Exception => e
raise e unless e.is_a? RestClient::RequestFailed
rescue RestClient::RequestFailed => e

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had something different in my mind, but just now realized that your suggestion works fine and is cleaner...
I'll add it after the new changes have been reviewed.

case e.http_code
when 400
raise Foreman::Exception.new N_('Error requesting repository metadata. Check Smart Proxy log.')
else
raise
end
end

def list_installed
parse(get('vcs_clone/roles'))
rescue *PROXY_ERRORS
raise Foreman::Exception.new N_('Error requesting installed roles. Check log.')
end

def install_role(repo_info)
parse(post(repo_info, 'vcs_clone/roles'))
rescue *PROXY_ERRORS, RestClient::Exception => e
raise e unless e.is_a? RestClient::RequestFailed
case e.http_code
when 409
raise Foreman::Exception.new N_('A repo with the name %s already exists.') % repo_info['repo_info']&.[]('name')
when 400
raise Foreman::Exception.new N_('Git Error. Check log.')
else
raise
end
end

def update_role(repo_info)
name = repo_info.delete('name')
parse(put(repo_info, "vcs_clone/roles/#{name}"))
rescue *PROXY_ERRORS, RestClient::Exception => e
raise e unless e.is_a? RestClient::RequestFailed
case e.http_code
when 400
raise Foreman::Exception.new N_('Error updating %s. Check Smartproxy log.') % name
else
raise
end
end

def delete_role(role_name)
parse(delete("vcs_clone/roles/#{role_name}"))
rescue *PROXY_ERRORS, RestClient::Exception => e
raise e unless e.is_a? RestClient::RequestFailed
case e.http_code
when 400
raise Foreman::Exception.new N_('Error deleting %s. Check Smartproxy log.') % role_name
else
raise
end
end
end
end
15 changes: 15 additions & 0 deletions app/services/foreman_ansible/vcs_cloner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module ForemanAnsible
class VcsCloner
include ::ForemanAnsible::ProxyAPI

def initialize(proxy = nil)
@ansible_proxy = proxy
end

delegate :install_role, to: :proxy_api

delegate :update_role, to: :proxy_api

delegate :delete_role, to: :proxy_api
end
end
12 changes: 10 additions & 2 deletions app/views/ansible_roles/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@

<%= webpacked_plugins_js_for :foreman_ansible %>

<%= csrf_meta_tag %>

<% title _("Ansible Roles") %>

<% title_actions ansible_proxy_import(hash_for_import_ansible_roles_path),
documentation_button('#4.1ImportingRoles', :root_url => ansible_doc_url) %>
<% title_actions ansible_proxy_import(hash_for_import_ansible_roles_path), vcs_import,
documentation_button('#4.1ImportingRoles', :root_url => ansible_doc_url)
%>

<table class="<%= table_css_classes 'table-fixed' %>">
<thead>
Expand Down Expand Up @@ -44,4 +50,6 @@
</tbody>
</table>

<%= react_component('VcsCloneModalContent')%>

<%= will_paginate_with_info @ansible_roles %>
6 changes: 6 additions & 0 deletions app/views/ansible_roles/welcome.html.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
<%= webpacked_plugins_js_for :foreman_ansible %>
<%= webpacked_plugins_css_for :foreman_ansible %>
Thorben-D marked this conversation as resolved.
Show resolved Hide resolved

<% content_for(:title, _("Ansible Roles")) %>
<div class="blank-slate-pf">
<div class="blank-slate-pf-icon">
Expand All @@ -10,5 +13,8 @@
<p><%= link_to(_('Learn more about this in the documentation.'), documentation_url('#4.1ImportingRoles', :root_url => ansible_doc_url), target: '_blank') %></p>
<div class="blank-slate-pf-secondary-action">
<%= ansible_proxy_import(hash_for_import_ansible_roles_path) %>
<%= vcs_import %>
</div>
</div>

<%= react_component('VcsCloneModalContent', {:title => "Get Ansible-Roles from VCS"})%>
9 changes: 9 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@
post :multiple_play_roles
end
end
resources :smart_proxies, :only => [] do
member do
get 'repository_metadata', to: 'vcs_clone#repository_metadata'
get 'roles', to: 'vcs_clone#installed_roles'
post 'roles', to: 'vcs_clone#install_role'
put 'roles/:role_name', to: 'vcs_clone#update_role', constraints: { role_name: %r{[^\/]+} }
delete 'roles/:role_name', to: 'vcs_clone#delete_role', constraints: { role_name: %r{[^\/]+} }
end
end
end
end
end
Expand Down
4 changes: 3 additions & 1 deletion lib/foreman_ansible/register.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@
{ :'api/v2/ansible_inventories' => [:schedule] }
permission :import_ansible_playbooks,
{ :'api/v2/ansible_playbooks' => [:sync, :fetch] }
permission :clone_from_vcs,
{ :'api/v2/vcs_clone' => [:repository_metadata, :installed_roles, :install_role, :update_role, :delete_role] }
end

role 'Ansible Roles Manager',
Expand All @@ -170,7 +172,7 @@
:import_ansible_roles, :view_ansible_variables, :view_lookup_values,
:create_lookup_values, :edit_lookup_values, :destroy_lookup_values,
:create_ansible_variables, :import_ansible_variables,
:edit_ansible_variables, :destroy_ansible_variables, :import_ansible_playbooks]
:edit_ansible_variables, :destroy_ansible_variables, :import_ansible_playbooks, :clone_from_vcs]

role 'Ansible Tower Inventory Reader',
[:view_hosts, :view_hostgroups, :view_facts, :generate_report_templates, :generate_ansible_inventory,
Expand Down
Loading
Loading