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
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) }

render :json => 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

render :json => 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
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
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

attributes :id, :product_id, :progress, :sync_id, :state, :raw_state
attributes :start_time, :finish_time, :duration, :display_size, :size
attributes :is_running, :error_details
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 => :results

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
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
65 changes: 65 additions & 0 deletions test/controllers/api/v2/sync_status_controller_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
require 'katello_test_helper'

module Katello
class Api::V2::SyncStatusControllerTest < ActionController::TestCase
def models
@organization = get_organization
@repository = katello_repositories(:fedora_17_x86_64)
@product = katello_products(:fedora)
end

def permissions
@sync_permission = :sync_products
end

def build_task_stub
task_attrs = [:id, :label, :pending, :execution_plan, :resumable?,
:username, :started_at, :ended_at, :state, :result, :progress,
:input, :humanized, :cli_example, :errors].inject({}) { |h, k| h.update k => nil }
task_attrs[:output] = {}
stub('task', task_attrs).mimic!(::ForemanTasks::Task)
end

def setup
setup_controller_defaults_api
login_user(User.find(users(:admin).id))
models
permissions
ForemanTasks.stubs(:async_task).returns(build_task_stub)
end

def test_index
@controller.expects(:collect_repos).returns([])
@controller.expects(:collect_all_repo_statuses).returns({})

get :index, params: { :organization_id => @organization.id }

assert_response :success
end

def test_poll
@controller.expects(:format_sync_progress).returns({})

get :poll, params: { :repository_ids => [@repository.id], :organization_id => @organization.id }

assert_response :success
end

def test_sync
@controller.expects(:latest_task).returns(nil)
@controller.expects(:format_sync_progress).returns({})

post :sync, params: { :repository_ids => [@repository.id], :organization_id => @organization.id }

assert_response :success
end

def test_destroy
Repository.any_instance.expects(:cancel_dynflow_sync)

delete :destroy, params: { :id => @repository.id }

assert_response :success
end
end
end
5 changes: 5 additions & 0 deletions webpack/containers/Application/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,18 @@ import ContainerImages from '../../scenes/ContainerImages';
import ManifestDetails from '../../scenes/ContainerImages/Synced/Details';
import FlatpakRemotes from '../../scenes/FlatpakRemotes';
import FlatpakRemoteDetails from '../../scenes/FlatpakRemotes/Details';
import SyncStatus from '../../scenes/SyncStatus';

// eslint-disable-next-line import/prefer-default-export
export const links = [
{
path: 'redhat_repositories',
component: WithOrganization(withHeader(Repos, { title: __('RH Repos') })),
},
{
path: 'sync_management',
component: WithOrganization(withHeader(SyncStatus, { title: __('Sync Status') })),
},
{
path: 'subscriptions',
component: WithOrganization(withHeader(Subscriptions, { title: __('Subscriptions') })),
Expand Down
17 changes: 17 additions & 0 deletions webpack/scenes/SyncStatus/SyncStatus.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Sticky toolbar and table header coordination
// Target the toolbar by its ouiaId with more specific selector
.pf-v5-c-toolbar.pf-m-sticky[data-ouia-component-id="sync-status-toolbar"] {
position: sticky;
top: 0;
z-index: 400;
box-shadow: none;
}

// Target the table by its ouiaId and offset the sticky header
.pf-v5-c-table[data-ouia-component-id="sync-status-table"] {
thead {
position: sticky;
top: 70px;
z-index: 300;
}
}
69 changes: 69 additions & 0 deletions webpack/scenes/SyncStatus/SyncStatusActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { API_OPERATIONS, get, post, APIActions } from 'foremanReact/redux/API';
import { translate as __ } from 'foremanReact/common/I18n';
import api, { orgId } from '../../services/api';
import SYNC_STATUS_KEY, {
SYNC_STATUS_POLL_KEY,
SYNC_REPOSITORIES_KEY,
CANCEL_SYNC_KEY,
} from './SyncStatusConstants';
import { getResponseErrorMsgs } from '../../utils/helpers';

export const syncStatusErrorToast = error => getResponseErrorMsgs(error.response);

export const getSyncStatus = (extraParams = {}) => get({
type: API_OPERATIONS.GET,
key: SYNC_STATUS_KEY,
url: api.getApiUrl('/sync_status'),
params: {
organization_id: orgId(),
...extraParams,
},
errorToast: error => syncStatusErrorToast(error),
});

export const pollSyncStatus = (repositoryIds, extraParams = {}) => get({
type: API_OPERATIONS.GET,
key: SYNC_STATUS_POLL_KEY,
url: api.getApiUrl('/sync_status/poll'),
params: {
repository_ids: repositoryIds,
organization_id: orgId(),
...extraParams,
},
errorToast: error => syncStatusErrorToast(error),
});

export const syncRepositories = (repositoryIds, handleSuccess, handleError) => post({
type: API_OPERATIONS.POST,
key: SYNC_REPOSITORIES_KEY,
url: api.getApiUrl('/sync_status/sync'),
params: {
repository_ids: repositoryIds,
organization_id: orgId(),
},
handleSuccess: (response) => {
if (handleSuccess) {
handleSuccess(response);
}
// The API returns an array of sync status objects
// Just show a simple success message
return __('Repository synchronization started');
},
handleError,
successToast: () => __('Repository synchronization started'),
errorToast: (error) => {
const message = getResponseErrorMsgs(error?.response);
return message || __('Failed to start repository synchronization');
},
});

export const cancelSync = (repositoryId, handleSuccess) => APIActions.delete({
type: API_OPERATIONS.DELETE,
key: CANCEL_SYNC_KEY,
url: api.getApiUrl(`/sync_status/${repositoryId}`),
handleSuccess,
successToast: () => __('Sync canceled'),
errorToast: error => syncStatusErrorToast(error),
});

export default getSyncStatus;
Loading
Loading