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 Dec 14, 2023
1 parent e82bbcc commit 2870271
Show file tree
Hide file tree
Showing 21 changed files with 1,339 additions and 3 deletions.
80 changes: 80 additions & 0 deletions app/controllers/api/v2/vcs_clone_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# 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

rescue_from Git::GitExecuteError do |_e|
head :internal_server_error
end

api :GET, '/vcs_clone/get_repo_info', N_('Returns information about the repo')
param :vcs_url, String, N_('Url of the repo'), :required => true
error 400, :desc => N_('Parameter unfulfilled')
error 500, :desc => N_('Git error')
def repo_information
vcs_url = params.require(:vcs_url)
remote = Git.ls_remote(vcs_url).slice('head', 'branches', 'tags')
remote['vcs_url'] = vcs_url
render json: remote
end

api :GET, '/vcs_clone/get_installed_roles', N_('Returns an array of roles installed on the provided proxy')
formats ['json']
param :smart_proxy, Array, N_('Name of the SmartProxy'), :required => true
error 400, :desc => N_('Parameter unfulfilled')
error 500, :desc => N_('Internal server error')
def installed_roles
smart_proxy = params.require(:smart_proxy)
ansible_proxy = SmartProxy.find_by(name: smart_proxy)
proxy_api = find_proxy_api(ansible_proxy)
installed = proxy_api.list_installed
render json: installed
rescue Foreman::Exception
head :internal_server_error
end

api :POST, '/vcs_clone/install', 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
param :update, String, :desc => N_('Whether an existing role with the provided name should be updated'), :default => false
end
param :smart_proxy, Array, N_('Array of SmartProxies the role should get installed to')
error 400, :desc => N_('Parameter unfulfilled')
error 500, :desc => N_('Internal server error')
def install_role
payload = params.require(:vcs_clone).
permit(
repo_info: [
:vcs_url,
:name,
:ref,
:update
],
smart_proxy: []
)

payload['repo_info']['update'] = false unless payload['repo_info'].key? 'update'

# 'smart_proxy' is an array to later allow a role to be installed on multiple SPs
ansible_proxy = SmartProxy.find_by(name: payload['smart_proxy'][0])

job = CloneAnsibleRole.perform_later(payload['repo_info'], ansible_proxy)
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
22 changes: 21 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,25 @@ def playbooks(playbooks_names = [])
rescue *PROXY_ERRORS => e
raise ProxyException.new(url, e, N_('Unable to get playbooks from Ansible'))
end

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

def vcs_clone_install(repo_info)
parse(post(repo_info, 'vcs_clone/install'))
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 %rName already exists.') % repo_info['repo_name']
when 500
raise Foreman::Exception.new N_('Git Error. Check log.')
else
raise e
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 @@
# frozen_string_literal: true

module ForemanAnsible
class VcsCloner
include ::ForemanAnsible::ProxyAPI

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

def install_role(info)
proxy_api.vcs_clone_install info
end
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"})%>
8 changes: 8 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@
end
end

resources :vcs_clone, :only => [] do
collection do
get 'get_repo_information', to: 'vcs_clone#repo_information'
post 'install_role', to: 'vcs_clone#install_role'
get 'get_installed_roles', to: 'vcs_clone#installed_roles'
end
end

resources :ansible_override_values, :only => [:create, :destroy]

resources :ansible_inventories, :only => [] do
Expand Down
1 change: 1 addition & 0 deletions foreman_ansible.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ Gem::Specification.new do |s|
s.add_dependency 'deface', '< 2.0'
s.add_dependency 'foreman_remote_execution', '>= 9.0', '< 13'
s.add_dependency 'foreman-tasks', '>= 7.0', '< 10'
s.add_dependency 'git', '~> 1.0'
end
1 change: 1 addition & 0 deletions lib/foreman_ansible/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require 'fast_gettext'
require 'gettext_i18n_rails'
require 'foreman_ansible/remote_execution'
require 'git'

module ForemanAnsible
# This engine connects ForemanAnsible with Foreman core
Expand Down
2 changes: 2 additions & 0 deletions 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]}
end

role 'Ansible Roles Manager',
Expand Down
58 changes: 58 additions & 0 deletions test/functional/api/v2/vcs_clone_controller_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

require 'test_plugin_helper'

module Api
module V2
class VcsCloneControllerTest < ActionController::TestCase
describe('#repo_information') do
test 'requests repo-information' do
Git.stubs(:ls_remote).returns({ 'head' => {}, 'branches' => {}, 'tags' => {} })

get :repo_information, params: { "vcs_url": 'https://github.com/theforeman/foreman.git' }, session: set_session_user
response = JSON.parse(@response.body)
assert_response :success
assert_equal({ 'head' => {},
'branches' => {},
'tags' => {},
'vcs_url' => 'https://github.com/theforeman/foreman.git' }, response)
end

test 'handles missing parameter' do
get :repo_information, params: { "vcs_urll": 'https://github.com/theforeman/foreman.git' }, session: set_session_user
response = JSON.parse(@response.body)
assert_response :bad_request
assert_equal({ 'error' => "param is missing or the value is empty: vcs_url\nDid you mean? vcs_urll\n vcs_clone\n controller\n action" },
response)
end

test 'handles exception in "GIT" module' do
Git.stubs(:ls_remote).raises(Git::GitExecuteError)

get :repo_information, params: { "vcs_url": 'https://github.com/theforeman/foreman.git' }, session: set_session_user
assert_response :internal_server_error
end
end

describe '#installed_roles' do
test 'requests installed roles' do
roles_array = %w[ansible_role_1 ansible_role_2]

ProxyAPI::Ansible.any_instance.expects(:list_installed).returns(roles_array)

get :installed_roles, params: { "smart_proxy": FactoryBot.create(:smart_proxy, :with_ansible).name }, session: set_session_user
response = JSON.parse(@response.body)

assert_response :success
assert_equal(roles_array, response)
end

test 'handles erroneous smart_proxy value' do
get :installed_roles, params: { "smart_proxy": 'something_unknown' }, session: set_session_user

assert_response :internal_server_error
end
end
end
end
end
Loading

0 comments on commit 2870271

Please sign in to comment.