Skip to content

Commit

Permalink
Fixes #36971 - GUI to allow cloning of Ansible roles from VCS
Browse files Browse the repository at this point in the history
  • Loading branch information
Thorben-D committed Mar 28, 2024
1 parent 00f9d79 commit d01402a
Show file tree
Hide file tree
Showing 25 changed files with 1,651 additions and 4 deletions.
146 changes: 146 additions & 0 deletions app/controllers/api/v2/vcs_clone_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# frozen_string_literal: true

module Api
module V2
class VcsCloneController < ::Api::V2::BaseController
include ::ForemanAnsible::ProxyAPI

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/:proxy_name/ansible/vcs_clone/repo_information',
N_('Queries metadata about the repo')
param :proxy_name, Array, N_('Name of the SmartProxy'), :required => true
param :vcs_url, String, N_('Url of the repo'), :required => true
error 400, :desc => N_('Parameter unfulfilled / invalid repo-info')
def repo_information
vcs_url = params.require(:vcs_url)
render json: @proxy_api.repo_information(vcs_url)
end

api :GET, '/smart_proxies/:proxy_name/ansible/vcs_clone/roles',
N_('Returns an array of roles installed on the provided proxy')
formats ['json']
param :proxy_name, Array, N_('Name of the SmartProxy'), :required => true
error 400, :desc => N_('Parameter unfulfilled')
def installed_roles
render json: @proxy_api.list_installed
end

api :POST, '/smart_proxies/:proxy_name/ansible/vcs_clone/roles',
N_('Launches a task to install the provided role')
formats ['json']
param :repo_info, Hash, :desc => N_('Dictionary containing info about the role to be installed') do
param :vcs_url, String, :desc => N_('Url of the repo'), :required => true
param :name, String, :desc => N_('Name of the repo'), :required => true
param :ref, String, :desc => N_('Branch / Tag / Commit reference'), :required => true
end
param :smart_proxy, Array, N_('SmartProxy the role should get installed to')
error 400, :desc => N_('Parameter unfulfilled')
def install_role
payload = verify_install_role_parameters(params)
start_vcs_task(payload, :install)
end

api :PUT, '/smart_proxies/:proxy_name/ansible/vcs_clone/roles',
N_('Launches a task to update the provided role')
formats ['json']
param :repo_info, Hash, :desc => N_('Dictionary containing info about the role to be installed') do
param :vcs_url, String, :desc => N_('Url of the repo'), :required => true
param :name, String, :desc => N_('Name of the repo'), :required => true
param :ref, String, :desc => N_('Branch / Tag / Commit reference'), :required => true
end
param :smart_proxy, Array, N_('SmartProxy the role should get installed to')
error 400, :desc => N_('Parameter unfulfilled')
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/:proxy_name/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 that should be deleted')
param :smart_proxy, Array, N_('SmartProxy the role should get deleted from')
error 400, :desc => N_('Parameter unfulfilled')
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 does not have foreman_ansible installed / is not capable of cloning from VCS')
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,
:name,
:ref
]
).to_h
end

def verify_install_role_parameters(params)
payload = permit_parameters params
%w[vcs_url 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("",
{ :primary => true, :class => 'roles-import' },
link_to(_("Import from VCS..."), "#vcs_import"))
end

def import_time(role)
_('%s ago') % time_ago_in_words(role.updated_at)
end
Expand Down
14 changes: 14 additions & 0 deletions app/jobs/clone_ansible_role.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

class CloneAnsibleRole < ::ApplicationJob
queue_as :default

def humanized_name
_('Clone Ansible Role from VCS')
end

def perform(repo_info, proxy)
vcs_cloner = ForemanAnsible::VcsCloner.new(proxy)
vcs_cloner.install_role repo_info
end
end
14 changes: 14 additions & 0 deletions app/jobs/delete_ansible_role.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

class DeleteAnsibleRole < ::ApplicationJob
queue_as :default

def humanized_name
_('Delete Ansible Role')
end

def perform(role_name, proxy)
vcs_cloner = ForemanAnsible::VcsCloner.new(proxy)
vcs_cloner.delete_role role_name
end
end
14 changes: 14 additions & 0 deletions app/jobs/update_ansible_role.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

class UpdateAnsibleRole < ::ApplicationJob
queue_as :default

def humanized_name
_('Update Ansible Role from VCS')
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
case e.http_code
when 400
raise Foreman::Exception.new N_('Error requesting repo-info. Check Smartproxy 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
17 changes: 17 additions & 0 deletions app/services/foreman_ansible/vcs_cloner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

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
13 changes: 11 additions & 2 deletions app/views/ansible_roles/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
<%= webpacked_plugins_js_for :foreman_ansible %>
<%= webpacked_plugins_css_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 +51,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 %>
<% 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"})%>
11 changes: 11 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@
post :multiple_play_roles
end
end
resources :smart_proxies, :only => [] do
member do
scope '/ansible' do
get 'repo_information', to: 'vcs_clone#repo_information'
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
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' => [:repo_information, :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

0 comments on commit d01402a

Please sign in to comment.