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 29, 2023
1 parent 6cd18e6 commit 6a7c5fd
Show file tree
Hide file tree
Showing 8 changed files with 302 additions and 1 deletion.
2 changes: 2 additions & 0 deletions lib/smart_proxy_ansible.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ module Ansible
require 'smart_proxy_ansible/playbooks_reader'
require 'smart_proxy_ansible/reader_helper'
require 'smart_proxy_ansible/variables_extractor'
require 'smart_proxy_ansible/vcs_cloner'
require 'git'
end
end
30 changes: 30 additions & 0 deletions lib/smart_proxy_ansible/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,36 @@ class Api < Sinatra::Base
PlaybooksReader.playbooks(params[:playbooks_names]).to_json
end

get '/vcs_clone/repo_information' do
repo_info = VCSCloner.repo_information(params)
status repo_info.status
body repo_info.payload.to_json
end

get '/vcs_clone/get_installed' do
get_installed = VCSCloner.list_installed_roles
status get_installed.status
body get_installed.payload.to_json
end

post '/vcs_clone/install' do
install = VCSCloner.install(params['repo_info'])
status install.status
body install.payload.to_json
end

put '/vcs_clone/update' do
update = VCSCloner.update(params['repo_info'])
status update.status
body update.payload.to_json
end

delete '/vcs_clone/delete/:role_name' do
delete = VCSCloner.delete(params)
status delete.status
body delete.payload.to_json
end

private

def extract_variables(role_name)
Expand Down
1 change: 1 addition & 0 deletions lib/smart_proxy_ansible/plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Plugin < Proxy::Plugin

load_classes ::Proxy::Ansible::ConfigurationLoader
load_validators :validate_settings => ::Proxy::Ansible::ValidateSettings
capability :vcs_clone
validate :validate!, :validate_settings => nil
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/smart_proxy_ansible/roles_reader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module Ansible
# Implements the logic needed to read the roles and associated information
class RolesReader
class << self
DEFAULT_ROLES_PATH = '/etc/ansible/roles:/usr/share/ansible/roles'.freeze
DEFAULT_ROLES_PATH = '/etc/ansible/roles:/usr/share/ansible/roles:/var/lib/foreman-proxy/ansible/roles'.freeze

def list_roles
roles = roles_path.split(':').map { |path| read_roles(path) }.flatten
Expand Down
98 changes: 98 additions & 0 deletions lib/smart_proxy_ansible/vcs_cloner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# frozen_string_literal: true

require_relative 'vcs_cloner_helper'
require 'net/http'

module Proxy
include ::Proxy::Log

module Ansible
Response = Struct.new(:status, :payload)

# VCSCloner. This class performs cloning and updating of Ansible-Roles sourced from Git
class VCSCloner
class << self
# Queries metadata about a given repository.
# Requires parameter "vcs_url"
# Returns 200 and the data if the query was successful
# Returns 400 if a parameter is unfulfilled or invalid repo-info was provided
def repo_information(payload)
return Response.new(400, 'Check parameters') unless payload.key? 'vcs_url'

vcs_url = payload['vcs_url']
remote = Git.ls_remote(vcs_url).slice('head', 'branches', 'tags')
remote['vcs_url'] = vcs_url
Response.new(200, remote)
rescue Git::GitExecuteError => e
Response.new(400, "Git Error: #{e}")
end

# Returns an array of installed roles
# Uses RolesReader.list_roles
def list_installed_roles
Response.new(200, RolesReader.list_roles)
end

# Clones a new role from the provided information.
# Requires hash with keys "vcs_url", "name" and "ref"
# Returns 201 if a role was created
# Returns 400 if a parameter is unfulfilled or invalid repo-info was provided
# Returns 409 if a role with "name" already exists
def install(repo_info)
return Response.new(400, 'Check parameters') unless VcsClonerHelper.correct_repo_info(repo_info)

if VcsClonerHelper.role_exists repo_info['name']
return Response.new(409,
"Role \"#{repo_info['name']}\" already exists.")
end

begin VcsClonerHelper.install_role repo_info
Response.new(201, "Role \"#{repo_info['name']}\" has been created.")
rescue Git::GitExecuteError => e
Response.new(400, "Git Error: #{e}")
end
end

# Updates a role with the provided information.
# Installs a role if it does not yet exist
# Requires hash with keys "vcs_url", "name" and "ref"
# Returns 200 if a role was updated
# Returns 201 if a role was created
# Returns 400 if a parameter is unfulfilled or invalid repo-info was provided
def update(repo_info)
return Response.new(400, 'Check parameters') unless VcsClonerHelper.correct_repo_info repo_info

begin
if VcsClonerHelper.role_exists repo_info['name']
VcsClonerHelper.update_role repo_info
Response.new(200, "Role \"#{repo_info['name']}\" has been updated.")
else
VcsClonerHelper.install_role repo_info
Response.new(201, "Role \"#{repo_info['name']}\" has been created.")
end
rescue Git::GitExecuteError => e
Response.new(400, "Git Error: #{e}")
end
end

# Deletes a role with the given name.
# Installs a role if it does not yet exist
# Requires parameter role_name
# Returns 200 if a role was deleted / never existed
# Returns 400 if a parameter is unfulfilled
def delete(payload)
return Response.new(400, 'Check parameters') unless payload.key? 'role_name'

role_name = payload['role_name']
unless VcsClonerHelper.role_exists role_name
return Response.new(200,
"Role \"#{role_name}\" does not exist. Request ignored.")
end

VcsClonerHelper.delete_role role_name
Response.new(200, "Role \"#{role_name}\" has been deleted.")
end
end
end
end
end
45 changes: 45 additions & 0 deletions lib/smart_proxy_ansible/vcs_cloner_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

module Proxy
module Ansible
# Implements VCS-Cloning logic and helper functions
class VcsClonerHelper
class << self
DEFAULT_BASE_PATH = Pathname('/var/lib/foreman-proxy/ansible/roles')

def repo_path(role_name)
DEFAULT_BASE_PATH.join(role_name)
end

def correct_repo_info(repo_info)
%w[vcs_url name ref].each do |param|
return false unless repo_info.key?(param)
end
end

def role_exists(role_name)
repo_path(role_name).exist?
end

def install_role(repo_info)
git = Git.init(repo_path(repo_info['name']))
git.add_remote('origin', repo_info['vcs_url'])
git.fetch
git.checkout(repo_info['ref'])
end

def update_role(repo_info)
git = Git.open(repo_path(repo_info['name']))
git.remove_remote('origin')
git.add_remote('origin', repo_info['vcs_url'])
git.fetch
git.checkout(repo_info['ref'])
end

def delete_role(role_name)
FileUtils.rm_r repo_path(role_name)
end
end
end
end
end
2 changes: 2 additions & 0 deletions smart_proxy_ansible.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Gem::Specification.new do |gem|
gem.license = 'GPL-3.0'
gem.required_ruby_version = '>= 2.5'

gem.add_dependency 'git', '~> 1.0'

gem.add_development_dependency 'rake', '~> 13.0'
gem.add_development_dependency('mocha', '~> 1')
gem.add_development_dependency('webmock', '~> 3')
Expand Down
123 changes: 123 additions & 0 deletions test/vcs_cloner_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# frozen_string_literal: true

require 'test_helper'
require 'git'

require_relative '../lib/smart_proxy_ansible/vcs_cloner'
require_relative '../lib/smart_proxy_ansible/roles_reader'

# Tests VCSCloner class
class VcsClonerTest < Minitest::Test
Response = Proxy::Ansible::Response

describe '#repo_information' do
payload = {
'vcs_url' => 'https://github.com/theforeman/smart_proxy_ansible.git'
}
demo_info = {
'head' => {},
'branches' => {},
'tags' => {}
}
test 'requests repo information' do
Git.stubs(:ls_remote).returns(demo_info)
response = Proxy::Ansible::VCSCloner.repo_information payload
assert_equal Response.new(200, payload.merge(demo_info)), response
end

test 'handles a missing parameter correctly' do
response = Proxy::Ansible::VCSCloner.repo_information({})
assert_equal Response.new(400, 'Check parameters'), response
end
end
describe '#list_installed_roles' do
demo_roles = %w[role1 role2 role3]
test 'correctly lists installed roles' do
Proxy::Ansible::RolesReader.stubs(:list_roles).returns(demo_roles)
response = Proxy::Ansible::VCSCloner.list_installed_roles
assert_equal Response.new(200, demo_roles), response
end
end
describe '#install' do
demo_repo_info = {
'vcs_url' => 'https://some.git.url',
'name' => 'best.role.ever',
'ref' => 'master'
}
test 'installs a role' do
Proxy::Ansible::VcsClonerHelper.stubs(:role_exists).returns(false)
Proxy::Ansible::VcsClonerHelper.stubs(:install_role).returns(true)
response = Proxy::Ansible::VCSCloner.install demo_repo_info
assert_equal Response.new(201, 'Role "best.role.ever" has been created.'), response
end

test 'handles a conflict properly' do
Proxy::Ansible::VcsClonerHelper.stubs(:role_exists).returns(true)
response = Proxy::Ansible::VCSCloner.install demo_repo_info
assert_equal Response.new(409, 'Role "best.role.ever" already exists.'), response
end

test 'handles missing parameter properly' do
response = Proxy::Ansible::VCSCloner.install({
'name' => 'best.role.ever',
'ref' => 'master'
})
assert_equal Response.new(400, 'Check parameters'), response
end
test 'handles Git error' do
Proxy::Ansible::VcsClonerHelper.stubs(:install_role).raises(Git::GitExecuteError.new)
response = Proxy::Ansible::VCSCloner.install demo_repo_info
assert_equal Response.new(400, 'Git Error: Git::GitExecuteError'), response
end
end
describe '#update' do
demo_repo_info = {
'vcs_url' => 'https://some.git.url',
'name' => 'best.role.ever',
'ref' => 'master'
}
test 'updates a role' do
Proxy::Ansible::VcsClonerHelper.stubs(:role_exists).returns(true)
Proxy::Ansible::VcsClonerHelper.stubs(:update_role).returns(true)
response = Proxy::Ansible::VCSCloner.update demo_repo_info
assert_equal Response.new(200, 'Role "best.role.ever" has been updated.'), response
end
test 'installs a role' do
Proxy::Ansible::VcsClonerHelper.stubs(:role_exists).returns(false)
Proxy::Ansible::VcsClonerHelper.stubs(:install_role).returns(true)
response = Proxy::Ansible::VCSCloner.update demo_repo_info
assert_equal Response.new(201, 'Role "best.role.ever" has been created.'), response
end
test 'handles missing parameter properly' do
response = Proxy::Ansible::VCSCloner.update({
'name' => 'best.role.ever',
'ref' => 'master'
})
assert_equal Response.new(400, 'Check parameters'), response
end
test 'handles Git error' do
Proxy::Ansible::VcsClonerHelper.stubs(:role_exists).returns(true)
Proxy::Ansible::VcsClonerHelper.stubs(:update_role).raises(Git::GitExecuteError.new)
response = Proxy::Ansible::VCSCloner.update demo_repo_info
assert_equal Response.new(400, 'Git Error: Git::GitExecuteError'), response
end
end
describe '#delete' do
test 'deletes a role' do
Proxy::Ansible::VcsClonerHelper.stubs(:role_exists).returns(true)
Proxy::Ansible::VcsClonerHelper.stubs(:delete_role).returns(true)
response = Proxy::Ansible::VCSCloner.delete({ 'role_name' => 'best.role.ever' })
assert_equal Response.new(200, 'Role "best.role.ever" has been deleted.'), response
end

test 'skips deleting a role' do
Proxy::Ansible::VcsClonerHelper.stubs(:role_exists).returns(false)
response = Proxy::Ansible::VCSCloner.delete({ 'role_name' => 'best.role.ever' })
assert_equal Response.new(200, 'Role "best.role.ever" does not exist. Request ignored.'), response
end
test 'handles missing parameter properly' do
response = Proxy::Ansible::VCSCloner.delete({})
assert_equal Response.new(400, 'Check parameters'), response
end
end
end

0 comments on commit 6a7c5fd

Please sign in to comment.