Skip to content
Open
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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -569,3 +569,4 @@ spec/ # RSpec tests
```

This codebase follows Foreman plugin conventions and integrates deeply with Foreman's architecture, extending its capabilities with content and subscription management features.
- Use RABL for API views, not 'render :json'.
90 changes: 90 additions & 0 deletions app/controllers/katello/api/v2/sync_status_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
module Katello
class Api::V2::SyncStatusController < Api::V2::ApiController
include SyncManagementHelper::RepoMethods

before_action :find_optional_organization, :only => [:index, :poll, :sync]
before_action :find_repository, :only => [:destroy]

api :GET, "/sync_status", N_("Get sync status for all repositories in an organization")
param :organization_id, :number, :desc => N_("ID of an organization"), :required => false
def index
org = @organization || current_organization_object
fail HttpErrors::NotFound, _("Organization required") if org.nil?

products = org.library.products.readable
redhat_products, custom_products = products.partition(&:redhat?)
redhat_products.sort_by! { |p| p.name.downcase }
custom_products.sort_by! { |p| p.name.downcase }

sorted_products = redhat_products + custom_products

@product_tree = collect_repos(sorted_products, org.library, false)

# Filter out products and intermediate nodes with no repositories
@product_tree = filter_empty_nodes(@product_tree)

@repo_statuses = collect_all_repo_statuses(sorted_products, org.library)

respond_for_index(:collection => {:products => @product_tree, :repo_statuses => @repo_statuses})
end

api :GET, "/sync_status/poll", N_("Poll sync status for specified repositories")
param :repository_ids, Array, :desc => N_("List of repository IDs to poll"), :required => true
param :organization_id, :number, :desc => N_("ID of an organization"), :required => false
def poll
repos = Repository.where(:id => params[:repository_ids]).readable
statuses = repos.map { |repo| format_sync_progress(repo) }

respond_for_index(:collection => statuses)
end

api :POST, "/sync_status/sync", N_("Synchronize repositories")
param :repository_ids, Array, :desc => N_("List of repository IDs to sync"), :required => true
param :organization_id, :number, :desc => N_("ID of an organization"), :required => false
def sync
collected = []
repos = Repository.where(:id => params[:repository_ids]).syncable

repos.each do |repo|
if latest_task(repo).try(:state) != 'running'
ForemanTasks.async_task(::Actions::Katello::Repository::Sync, repo)
end
collected << format_sync_progress(repo)
end

respond_for_index(:collection => collected)
end

api :DELETE, "/sync_status/:id", N_("Cancel repository synchronization")
param :id, :number, :desc => N_("Repository ID"), :required => true
def destroy
@repository.cancel_dynflow_sync
render :json => {:message => _("Sync canceled")}
end

private

def find_repository
@repository = Repository.where(:id => params[:id]).syncable.first
fail HttpErrors::NotFound, _("Repository not found or not syncable") if @repository.nil?
end

def format_sync_progress(repo)
::Katello::SyncStatusPresenter.new(repo, latest_task(repo)).sync_progress
end

def latest_task(repo)
repo.latest_dynflow_sync
end

def collect_all_repo_statuses(products, env)
statuses = {}
products.each do |product|
product.repos(env).each do |repo|
statuses[repo.id] = format_sync_progress(repo)
end
end
statuses
end
end
end
84 changes: 0 additions & 84 deletions app/controllers/katello/sync_management_controller.rb

This file was deleted.

44 changes: 38 additions & 6 deletions app/helpers/katello/sync_management_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,26 +29,58 @@ def any_syncable?
end

module RepoMethods
# Format a repository as a hash for the API
def format_repo(repo)
{
:id => repo.id,
:name => repo.name,
:type => "repo",
}
end

# Recursively check if a node has any repositories
def repos?(node)
return true if node[:repos].present? && node[:repos].any?
return false if node[:children].blank?
node[:children].any? { |child| repos?(child) }
end

# Filter out nodes with no repositories
def filter_empty_nodes(nodes)
nodes.select { |node| repos?(node) }.map do |node|
if node[:children].present?
node.merge(:children => filter_empty_nodes(node[:children]))
else
node
end
end
end

# returns all repos in hash representation with minors and arch children included
def collect_repos(products, env, include_feedless = true)
products.map do |prod|
minor_repos, repos_without_minor = collect_minor(prod.repos(env, nil, include_feedless))
{ :name => prod.name, :object => prod, :id => prod.id, :type => "product", :repos => repos_without_minor,
:children => minors(minor_repos), :organization => prod.organization.name }
{ :name => prod.name, :object => prod, :id => prod.id, :type => "product",
:repos => repos_without_minor.map { |r| format_repo(r) },
:children => minors(minor_repos, prod.id), :organization => prod.organization.name }
end
end

# returns all minors in hash representation with arch children included
def minors(minor_repos)
def minors(minor_repos, product_id)
minor_repos.map do |minor, repos|
{ :name => minor, :id => minor, :type => "minor", :children => arches(repos), :repos => [] }
minor_id = "#{product_id}-#{minor}"
{ :name => minor, :id => minor_id, :type => "minor",
:children => arches(repos, minor_id), :repos => [] }
end
end

# returns all archs in hash representation
def arches(arch_repos)
def arches(arch_repos, parent_id)
collect_arches(arch_repos).map do |arch, repos|
{ :name => arch, :id => arch, :type => "arch", :children => [], :repos => repos }
arch_id = "#{parent_id}-#{arch}"
{ :name => arch, :id => arch_id, :type => "arch", :children => [],
:repos => repos.map { |r| format_repo(r) } }
end
end

Expand Down
17 changes: 16 additions & 1 deletion app/presenters/katello/sync_status_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,27 @@ def sync_progress
:display_size => display_output,
:size => display_output,
:is_running => @task.pending && @task.state != 'paused',
:error_details => @task.errors,
:error_details => error_details_messages,
}
end

private

def error_details_messages
return nil unless @task && (@task.result == 'error' || @task.result == 'warning')

errors = @task.humanized[:errors]
return nil if errors.blank?

messages = if errors.is_a?(String)
errors.split("\n").reject(&:blank?)
else
[errors.to_s]
end

messages.empty? ? nil : { messages: messages }
end

def empty_task(repo)
state = 'never_synced'
{
Expand Down
9 changes: 9 additions & 0 deletions app/views/katello/api/v2/sync_status/index.json.rabl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
object false

node :products do
@product_tree
end

node :repo_statuses do
@repo_statuses
end
5 changes: 5 additions & 0 deletions app/views/katello/api/v2/sync_status/poll.json.rabl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
collection @collection

node do |item|
item
end
5 changes: 5 additions & 0 deletions app/views/katello/api/v2/sync_status/sync.json.rabl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
collection @collection

node do |item|
item
end
11 changes: 3 additions & 8 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,15 @@
scope :katello, :path => '/katello' do
match ':kt_path/auto_complete_search', :action => :auto_complete_search, :controller => :auto_complete_search, :via => :get

resources :sync_management, :only => [:destroy] do
collection do
get :index
get :sync_status
post :sync
end
end

match '/remote_execution' => 'remote_execution#create', :via => [:post]
end

get '/katello/providers/redhat_provider', to: redirect('/redhat_repositories')
match '/redhat_repositories' => 'react#index', :via => [:get]

get '/katello/sync_management', to: redirect('/sync_management')
match '/sync_management' => 'react#index', :via => [:get]

match '/subscriptions' => 'react#index', :via => [:get]
match '/subscriptions/*page' => 'react#index', :via => [:get]

Expand Down
7 changes: 7 additions & 0 deletions config/routes/api/v2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,13 @@ class ActionDispatch::Routing::Mapper
get :auto_complete_search, :on => :collection
put :sync
end

api_resources :sync_status, :only => [:index, :destroy] do
collection do
get :poll
post :sync
end
end
end # module v2
end # '/api' namespace
end # '/katello' namespace
Expand Down
2 changes: 1 addition & 1 deletion lib/katello/permission_creator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ def product_permissions
'katello/api/v2/products_bulk_actions' => [:sync_products],
'katello/api/v2/repositories_bulk_actions' => [:sync_repositories, :reclaim_space_from_repositories],
'katello/api/v2/sync' => [:index],
'katello/sync_management' => [:index, :sync_status, :product_status, :sync, :destroy],
'katello/api/v2/sync_status' => [:index, :poll, :sync, :destroy],
},
:resource_type => 'Katello::Product',
:finder_scope => :syncable
Expand Down
3 changes: 2 additions & 1 deletion lib/katello/plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@
menu :top_menu,
:sync_status,
:caption => N_('Sync Status'),
:url_hash => {:controller => 'katello/sync_management',
:url => '/sync_management',
:url_hash => {:controller => 'katello/api/v2/sync_status',
:action => 'index'},
:engine => Katello::Engine,
:turbolinks => false
Expand Down
4 changes: 2 additions & 2 deletions spec/helpers/sync_management_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,12 @@ module Katello
end

describe "#minors" do
subject { object.minors('1' => [Repository.new(:root => RootRepository.new)]).first }
subject { object.minors({'1' => [Repository.new(:root => RootRepository.new)]}, 'test-product').first }
it { value(subject.keys).must_include(:id, :name) }
end

describe "#arches" do
subject { object.arches([Repository.new(:root => RootRepository.new(:arch => 'i386'))]).first }
subject { object.arches([Repository.new(:root => RootRepository.new(:arch => 'i386'))], 'test-parent').first }
it { value(subject.keys).must_include(:id, :name) }
end
end
Expand Down
Loading
Loading