diff --git a/app/controllers/katello/api/v2/content_view_components_controller.rb b/app/controllers/katello/api/v2/content_view_components_controller.rb index 4fc835c4ea8..d77d9bce919 100644 --- a/app/controllers/katello/api/v2/content_view_components_controller.rb +++ b/app/controllers/katello/api/v2/content_view_components_controller.rb @@ -47,7 +47,7 @@ def show_all CAST (#{kcc}.composite_content_view_id as BOOLEAN) ASC, #{kc}.name SQL query = Katello::ContentView.readable.in_organization(@organization) - query = query&.non_composite&.non_default&.generated_for_none + query = query&.non_composite&.non_default&.non_rolling&.generated_for_none component_cv_ids = Katello::ContentViewComponent.where(composite_content_view_id: @view.id).select(:content_view_id) query = case params[:status] when "Not added" @@ -145,23 +145,23 @@ def update def get_total(status) case status when 'All' - return Katello::ContentView.non_default.non_composite.in_organization(@organization).count + return Katello::ContentView.non_default.non_composite.non_rolling.in_organization(@organization).count when 'Added' return Katello::ContentViewComponent.where(composite_content_view_id: @view.id).count when 'Not added' - return Katello::ContentView.non_default.non_composite.in_organization(@organization).count - Katello::ContentViewComponent.where(composite_content_view_id: @view.id).count + return Katello::ContentView.non_default.non_composite.non_rolling.in_organization(@organization).count - Katello::ContentViewComponent.where(composite_content_view_id: @view.id).count else - return Katello::ContentView.non_default.non_composite.in_organization(@organization).count + return Katello::ContentView.non_default.non_composite.non_rolling.in_organization(@organization).count end end def find_composite_content_view - @view = ContentView.composite.non_default.readable.find_by(id: params[:composite_content_view_id]) + @view = ContentView.composite.non_default.non_rolling.readable.find_by(id: params[:composite_content_view_id]) throw_resource_not_found(name: 'composite content view', id: params[:composite_content_view_id]) if @view.nil? end def find_composite_content_view_for_edit - @view = ContentView.composite.non_default.editable.find_by(id: params[:composite_content_view_id]) + @view = ContentView.composite.non_default.non_rolling.editable.find_by(id: params[:composite_content_view_id]) throw_resource_not_found(name: 'composite content view', id: params[:composite_content_view_id]) if @view.nil? end diff --git a/app/controllers/katello/api/v2/content_view_filters_controller.rb b/app/controllers/katello/api/v2/content_view_filters_controller.rb index 2d9ac705783..8acda4ba093 100644 --- a/app/controllers/katello/api/v2/content_view_filters_controller.rb +++ b/app/controllers/katello/api/v2/content_view_filters_controller.rb @@ -43,6 +43,9 @@ def index_relation param :repository_ids, Array, :desc => N_("list of repository ids") param :description, String, :desc => N_("description of the filter") def create + if @view.rolling? + fail HttpErrors::BadRequest, _("It's not possible to create a filter for a rolling content view.") + end params[:type] = "erratum" if (params[:type] == "erratum_date" || params[:type] == "erratum_id") filter = ContentViewFilter.create_for(params[:type], filter_params.merge(:content_view => @view)) respond :resource => filter diff --git a/app/controllers/katello/api/v2/content_view_versions_controller.rb b/app/controllers/katello/api/v2/content_view_versions_controller.rb index f7917c47528..07a1ba94bce 100644 --- a/app/controllers/katello/api/v2/content_view_versions_controller.rb +++ b/app/controllers/katello/api/v2/content_view_versions_controller.rb @@ -60,6 +60,9 @@ def show param :environment_ids, Array, :desc => N_("Identifiers for Lifecycle Environment") param :description, String, :desc => N_("The description for the content view version promotion") def promote + if @view.rolling? + fail HttpErrors::BadRequest, _("It's not possible to promote a rolling content view.") + end is_force = ::Foreman::Cast.to_bool(params[:force]) task = async_task(::Actions::Katello::ContentView::Promote, @content_view_version, @environments, is_force, params[:description]) @@ -97,6 +100,9 @@ def republish_repositories api :DELETE, "/content_view_versions/:id", N_("Remove content view version") param :id, :number, :desc => N_("Content view version identifier"), :required => true def destroy + if @view.rolling? + fail HttpErrors::BadRequest, _("It's not possible to destroy a version of a rolling content view.") + end task = async_task(::Actions::Katello::ContentViewVersion::Destroy, @content_view_version) respond_for_async :resource => task end diff --git a/app/controllers/katello/api/v2/content_views_controller.rb b/app/controllers/katello/api/v2/content_views_controller.rb index a182c1eaa48..8f215cc132e 100644 --- a/app/controllers/katello/api/v2/content_views_controller.rb +++ b/app/controllers/katello/api/v2/content_views_controller.rb @@ -13,6 +13,7 @@ class Api::V2::ContentViewsController < Api::V2::ApiController wrap_parameters :include => (ContentView.attribute_names + %w(repository_ids component_ids)) + around_action :add_to_environment, :only => [:create] resource_description do api_version "v2" end @@ -84,6 +85,7 @@ def index_relation param :name, String, :desc => N_("Name of the content view"), :required => true param :label, String, :desc => N_("Content view label") param :composite, :bool, :desc => N_("Composite content view") + param :rolling, :bool, :desc => N_("Rolling content view") param_group :content_view def create @content_view = ContentView.create!(view_params) do |view| @@ -192,6 +194,9 @@ def remove param :key_content_view_id, :number, :desc => N_("content view to reassign orphaned activation keys to") param :key_environment_id, :number, :desc => N_("environment to reassign orphaned activation keys to") def bulk_delete_versions + if @content_view.rolling? + fail HttpErrors::BadRequest, _("It's not possible to bulk remove versions from a rolling content view.") + end params[:bulk_content_view_version_ids] ||= {} versions = find_bulk_items(bulk_params: params[:bulk_content_view_version_ids], @@ -237,6 +242,9 @@ def destroy param :name, String, :required => true, :desc => N_("New content view name") def copy @content_view = Katello::ContentView.readable.find_by(:id => params[:id]) + if @content_view.rolling? + fail HttpErrors::BadRequest, _("It's not possible to copy a rolling content view.") + end throw_resource_not_found(name: 'content_view', id: params[:id]) if @content_view.blank? ensure_non_default new_content_view = @content_view.copy(params[:content_view][:name]) @@ -246,6 +254,9 @@ def copy private def validate_publish_params! + if @content_view.rolling? + fail HttpErrors::BadRequest, _("It's not possible to publish a rolling content view.") + end if params[:repos_units].present? && @content_view.composite? fail HttpErrors::BadRequest, _("Directly setting package lists on composite content views is not allowed. Please " \ "update the components, then re-publish the composite.") @@ -284,11 +295,14 @@ def ensure_non_generated def view_params attrs = [:name, :description, :auto_publish, :solve_dependencies, :import_only, :default, :created_at, :updated_at, :next_version, {:component_ids => []}] - attrs.push(:label, :composite) if action_name == "create" + attrs.push(:label, :composite, :rolling) if action_name == "create" if (!@content_view || !@content_view.composite?) attrs.push({:repository_ids => []}, :repository_ids) end - params.require(:content_view).permit(*attrs).to_h + result = params.require(:content_view).permit(*attrs).to_h + # sanitize repository_ids to be a list of integers + result[:repository_ids] = result[:repository_ids].map(&:to_i) if result[:repository_ids].present? + result end def find_environment @@ -307,5 +321,11 @@ def add_use_latest_records(module_records, selected_latest_versions) end module_records end + + def add_to_environment + yield + return unless params[:rolling] + async_task(::Actions::Katello::ContentView::AddToEnvironment, @content_view.create_new_version, @content_view.organization.library) + end end end diff --git a/app/controllers/katello/api/v2/exports_controller.rb b/app/controllers/katello/api/v2/exports_controller.rb index 55f6b50c776..3ff84dc3515 100644 --- a/app/controllers/katello/api/v2/exports_controller.rb +++ b/app/controllers/katello/api/v2/exports_controller.rb @@ -108,6 +108,9 @@ def find_exportable_content_view_version @version = ContentViewVersion.exportable.find_by_id(params[:id]) throw_resource_not_found(name: 'content view version', id: params[:id]) if @version.blank? @view = @version.content_view + if @view.rolling? + fail HttpErrors::BadRequest, _("It's not possible to export a rolling content view.") + end end def find_history diff --git a/app/lib/actions/helpers/rolling_cv_repos.rb b/app/lib/actions/helpers/rolling_cv_repos.rb new file mode 100644 index 00000000000..8cb06c0ffbb --- /dev/null +++ b/app/lib/actions/helpers/rolling_cv_repos.rb @@ -0,0 +1,17 @@ +module Actions + module Helpers + module RollingCVRepos + def update_rolling_content_views(repo) + concurrence do + repos = repo.root.repositories.in_environment(repo.environment).where( + content_view_version: ::Katello::ContentViewVersion.where(content_view: ::Katello::ContentView.rolling) + ) + + repos.each do |rolling_repo| + plan_action(::Actions::Katello::ContentView::RefreshRollingRepo, rolling_repo) + end + end + end + end + end +end diff --git a/app/lib/actions/katello/content_view/add_rolling_repo_clone.rb b/app/lib/actions/katello/content_view/add_rolling_repo_clone.rb new file mode 100644 index 00000000000..cf89308d5ce --- /dev/null +++ b/app/lib/actions/katello/content_view/add_rolling_repo_clone.rb @@ -0,0 +1,28 @@ +module Actions + module Katello + module ContentView + class AddRollingRepoClone < Actions::EntryAction + def plan(content_view, repository_ids) + library = content_view.organization.library + + concurrence do + ::Katello::Repository.where(id: repository_ids).each do |repository| + sequence do + clone = content_view.get_repo_clone(library, repository).first + if clone.nil? + clone = repository.build_clone(content_view: content_view, environment: library) + clone.save! + end + plan_action(RefreshRollingRepo, clone) + + view_env_cp_id = content_view.content_view_environment(library).cp_id + content_id = repository.content_id + plan_action(Actions::Candlepin::Environment::AddContentToEnvironment, :view_env_cp_id => view_env_cp_id, :content_id => content_id) + end + end + end + end + end + end + end +end diff --git a/app/lib/actions/katello/content_view/refresh_rolling_repo.rb b/app/lib/actions/katello/content_view/refresh_rolling_repo.rb new file mode 100644 index 00000000000..a11ea6582c3 --- /dev/null +++ b/app/lib/actions/katello/content_view/refresh_rolling_repo.rb @@ -0,0 +1,30 @@ +module Actions + module Katello + module ContentView + class RefreshRollingRepo < Actions::EntryAction + def plan(repository) + action_subject repository + sequence do + plan_self(repository_id: repository.id) + plan_action(Pulp3::Repository::RefreshDistribution, repository, SmartProxy.pulp_primary) + plan_action(Repository::IndexContent, id: repository.id, source_repository_id: repository.library_instance.id) + end + end + + def run + repository = ::Katello::Repository.find(input[:repository_id]) + library_instance = repository.library_instance + # ensure IndexContent is not skipped! + repository.last_contents_changed = DateTime.now if repository.version_href != library_instance.version_href + + repository.version_href = library_instance.version_href + repository.publication_href = library_instance.publication_href + if repository.deb_using_structured_apt? + repository.content_id = library_instance.content_id + end + repository.save! + end + end + end + end +end diff --git a/app/lib/actions/katello/content_view/remove_rolling_repo_clone.rb b/app/lib/actions/katello/content_view/remove_rolling_repo_clone.rb new file mode 100644 index 00000000000..b823acbc4fe --- /dev/null +++ b/app/lib/actions/katello/content_view/remove_rolling_repo_clone.rb @@ -0,0 +1,28 @@ +module Actions + module Katello + module ContentView + class RemoveRollingRepoClone < Actions::EntryAction + def plan(content_view, repository_ids) + library = content_view.organization.library + + clone_repo_ids = [] + concurrence do + ::Katello::Repository.where(id: repository_ids).each do |repository| + clone_repo = content_view.get_repo_clone(library, repository).first + next if clone_repo.nil? + + clone_repo_ids << clone_repo.id + plan_action(Actions::Pulp3::Repository::DeleteDistributions, clone_repo.id, SmartProxy.pulp_primary) + end + plan_action(Candlepin::Environment::SetContent, content_view, library, content_view.content_view_environment(library)) + end + plan_self(repository_ids: clone_repo_ids) + end + + def run + ::Katello::Repository.where(id: input[:repository_ids]).destroy_all + end + end + end + end +end diff --git a/app/lib/actions/katello/content_view/update.rb b/app/lib/actions/katello/content_view/update.rb index b54d53b6e80..47b2d624dc3 100644 --- a/app/lib/actions/katello/content_view/update.rb +++ b/app/lib/actions/katello/content_view/update.rb @@ -28,6 +28,14 @@ def plan(content_view, content_view_params) end end + if content_view.rolling? && content_view_params.key?(:repository_ids) + repo_ids_to_add = content_view_params[:repository_ids] - content_view.repository_ids + repo_ids_to_remove = content_view.repository_ids - content_view_params[:repository_ids] + + plan_action(AddRollingRepoClone, content_view, repo_ids_to_add) if repo_ids_to_add.any? + plan_action(RemoveRollingRepoClone, content_view, repo_ids_to_remove) if repo_ids_to_remove.any? + end + content_view.update!(content_view_params) end end diff --git a/app/lib/actions/katello/repository/import_upload.rb b/app/lib/actions/katello/repository/import_upload.rb index cce4a7514cc..a2e8dc47e72 100644 --- a/app/lib/actions/katello/repository/import_upload.rb +++ b/app/lib/actions/katello/repository/import_upload.rb @@ -3,6 +3,8 @@ module Actions module Katello module Repository class ImportUpload < Actions::EntryAction + include Helpers::RollingCVRepos + # rubocop:disable Metrics/MethodLength def plan(repository, uploads, options = {}) action_subject(repository) @@ -52,6 +54,9 @@ def plan(repository, uploads, options = {}) plan_action(Katello::Repository::MetadataGenerate, repository, force_publication: true) if generate_metadata plan_action(Actions::Katello::Applicability::Repository::Regenerate, :repo_ids => [repository.id]) if generate_applicability plan_self(repository_id: repository.id, sync_capsule: sync_capsule, upload_results: upload_results) + + # Refresh rolling CVs that have this repository + update_rolling_content_views(repository) end end # rubocop:enable Metrics/MethodLength diff --git a/app/lib/actions/katello/repository/sync.rb b/app/lib/actions/katello/repository/sync.rb index cb5203dbbd5..881b829eb3e 100644 --- a/app/lib/actions/katello/repository/sync.rb +++ b/app/lib/actions/katello/repository/sync.rb @@ -5,6 +5,7 @@ module Repository class Sync < Actions::EntryAction extend ApipieDSL::Class include Helpers::Presenter + include Helpers::RollingCVRepos include ::Actions::ObservableAction middleware.use Actions::Middleware::ExecuteIfContentsChanged @@ -56,6 +57,7 @@ def plan(repo, options = {}) end plan_self(:id => repo.id, :sync_result => output, :skip_metadata_check => skip_metadata_check, :validate_contents => validate_contents, :contents_changed => output[:contents_changed]) + update_rolling_content_views(repo) plan_action(Katello::Repository::SyncHook, :id => repo.id) end end diff --git a/app/lib/actions/katello/repository/upload_files.rb b/app/lib/actions/katello/repository/upload_files.rb index 839e206db8d..345da36d361 100644 --- a/app/lib/actions/katello/repository/upload_files.rb +++ b/app/lib/actions/katello/repository/upload_files.rb @@ -6,6 +6,8 @@ module Actions module Katello module Repository class UploadFiles < Actions::EntryAction + include Helpers::RollingCVRepos + def plan(repository, files, content_type = nil, options = {}) action_subject(repository) repository.check_ready_to_act! @@ -38,6 +40,9 @@ def plan(repository, files, content_type = nil, options = {}) plan_action(FinishUpload, repository, content_type: content_type, upload_actions: upload_actions) plan_self(tmp_files: tmp_files) plan_action(Actions::Katello::Applicability::Repository::Regenerate, :repo_ids => [repository.id]) if generate_applicability + + # Refresh rolling CVs that have this repository + update_rolling_content_views(repository) end ensure # Delete tmp files when some exception occurred. Would be @@ -46,7 +51,8 @@ def plan(repository, files, content_type = nil, options = {}) end def run - ForemanTasks.async_task(Repository::CapsuleSync, ::Katello::Repository.find(input[:repository][:id])) if Setting[:foreman_proxy_content_auto_sync] + repository = ::Katello::Repository.find(input[:repository][:id]) + ForemanTasks.async_task(Repository::CapsuleSync, repository) if Setting[:foreman_proxy_content_auto_sync] rescue ::Katello::Errors::CapsuleCannotBeReached # skip any capsules that cannot be connected to end diff --git a/app/models/katello/content_view.rb b/app/models/katello/content_view.rb index e13cb2f9735..a4354f8aa2a 100644 --- a/app/models/katello/content_view.rb +++ b/app/models/katello/content_view.rb @@ -71,6 +71,16 @@ class ContentView < Katello::Model validates :composite, inclusion: { in: [false], message: "Composite Content Views can not solve dependencies" }, if: :solve_dependencies + validates :rolling, :inclusion => [true, false] + validates :rolling, + inclusion: { in: [false], message: "Rolling content views can not solve dependencies" }, + if: :solve_dependencies + validates :rolling, + inclusion: { in: [false], message: "Rolling content views can not be composite" }, + if: :composite + validates :rolling, + inclusion: { in: [false], message: "Rolling content views can not be import only" }, + if: :import_only validates :import_only, :inclusion => [true, false] validates :import_only, inclusion: { in: [false], message: "Import-only Content Views can not be Composite" }, @@ -93,6 +103,8 @@ class ContentView < Katello::Model scope :non_default, -> { where(:default => false) } scope :composite, -> { where(:composite => true) } scope :non_composite, -> { where(:composite => [nil, false]) } + scope :rolling, -> { where(:rolling => true) } + scope :non_rolling, -> { where(:rolling => [nil, false]) } scope :generated, -> { where.not(:generated_for => :none) } scope :generated_for_repository, -> { where(:generated_for => [:repository_export, @@ -113,6 +125,7 @@ class ContentView < Katello::Model scoped_search :on => :organization_id, :complete_value => true, :only_explicit => true, :validator => ScopedSearch::Validators::INTEGER scoped_search :on => :label, :complete_value => true scoped_search :on => :composite, :complete_value => { :true => true, :false => false } + scoped_search :on => :rolling, :complete_value => { :true => true, :false => false } scoped_search :on => :generated_for, :complete_value => true scoped_search :on => :default # just for ordering scoped_search :on => :name, :complete_value => true, @@ -802,7 +815,7 @@ def cv_repo_indexed_after_last_published? end def unpublishable? - default? || import_only? || generated? + default? || import_only? || generated? || rolling? end def needs_publish? diff --git a/app/models/katello/content_view_component.rb b/app/models/katello/content_view_component.rb index 61379aed36c..d89c25d010b 100644 --- a/app/models/katello/content_view_component.rb +++ b/app/models/katello/content_view_component.rb @@ -53,6 +53,10 @@ def ensure_valid_content_view errors.add(:base, _("Cannot add default content view to composite content view")) end + if view.rolling? + errors.add(:base, _("Cannot add rolling content view to composite content view")) + end + if attached_content_view_ids.include?(view.id) errors.add(:base, _("Another component already includes content view with ID %s" % view.id)) end diff --git a/app/models/katello/content_view_version.rb b/app/models/katello/content_view_version.rb index 530912a23bc..85daf75b1b4 100644 --- a/app/models/katello/content_view_version.rb +++ b/app/models/katello/content_view_version.rb @@ -45,6 +45,7 @@ class ContentViewVersion < Katello::Model :class_name => "Katello::ContentViewVersion", :inverse_of => :components has_many :published_in_composite_content_views, through: :composites, source: :content_view delegate :default, :default?, to: :content_view + delegate :rolling, :rolling?, to: :content_view validates_lengths_from_database @@ -383,7 +384,7 @@ def content_counts_map end def check_ready_to_promote!(to_env) - fail _("Default content view versions cannot be promoted") if default? + fail _("Default and Rolling content view versions cannot be promoted") if default? || rolling? content_view.check_composite_action_allowed!(to_env) content_view.check_docker_repository_names!(to_env) content_view.check_orphaned_content_facets!(environments: [to_env]) diff --git a/app/models/katello/repository.rb b/app/models/katello/repository.rb index 3abccdeb136..d0a1b9f3f9b 100644 --- a/app/models/katello/repository.rb +++ b/app/models/katello/repository.rb @@ -718,6 +718,8 @@ def environmental_instances(view) def archived_instance if self.environment_id.nil? || self.library_instance_id.nil? self + elsif self.content_view.rolling? + self.library_instance else self.content_view_version.archived_repos.where(:root_id => self.root_id).first end diff --git a/app/views/katello/api/v2/capsule_content/sync_status.json.rabl b/app/views/katello/api/v2/capsule_content/sync_status.json.rabl index 298ad550f7d..029d384c47c 100644 --- a/app/views/katello/api/v2/capsule_content/sync_status.json.rabl +++ b/app/views/katello/api/v2/capsule_content/sync_status.json.rabl @@ -67,6 +67,7 @@ child @lifecycle_environments => :lifecycle_environments do :label => content_view.label, :name => content_view.name, :composite => content_view.composite, + :rolling => content_view.rolling, :last_published => content_view.versions.empty? ? nil : content_view.versions.in_environment(env).first&.created_at, :default => content_view.default, :up_to_date => @capsule.repos_pending_sync(env, content_view).empty?, diff --git a/app/views/katello/api/v2/content_facet/base.json.rabl b/app/views/katello/api/v2/content_facet/base.json.rabl index 399bf8b278a..bcff2aa2103 100644 --- a/app/views/katello/api/v2/content_facet/base.json.rabl +++ b/app/views/katello/api/v2/content_facet/base.json.rabl @@ -14,6 +14,7 @@ child :content_view_environments => :content_view_environments do id: cve.content_view&.id, name: cve.content_view&.name, composite: cve.content_view&.composite, + rolling: cve.content_view&.rolling, content_view_version: cve.content_view_version&.version, content_view_version_id: cve.content_view_version&.id, content_view_version_latest: cve.content_view_version&.latest?, @@ -46,6 +47,7 @@ node :content_view do |content_facet| :id => content_view.id, :name => content_view.name, :composite => content_view.composite?, + :rolling => content_view.rolling?, } end end diff --git a/app/views/katello/api/v2/content_views/base.json.rabl b/app/views/katello/api/v2/content_views/base.json.rabl index 21548d18200..ac094008873 100644 --- a/app/views/katello/api/v2/content_views/base.json.rabl +++ b/app/views/katello/api/v2/content_views/base.json.rabl @@ -2,6 +2,7 @@ extends 'katello/api/v2/common/identifier' extends 'katello/api/v2/common/org_reference' attributes :composite +attributes :rolling attributes :component_ids, :duplicate_repositories_to_publish attributes :default attributes :version_count diff --git a/app/views/katello/api/v2/hosts/base.json.rabl b/app/views/katello/api/v2/hosts/base.json.rabl index 4bdf894928d..5a53e849348 100644 --- a/app/views/katello/api/v2/hosts/base.json.rabl +++ b/app/views/katello/api/v2/hosts/base.json.rabl @@ -21,6 +21,7 @@ if @facet :id => content_view.id, :name => content_view.name, :composite => content_view.composite?, + :rolling => content_view.rolling?, } end end diff --git a/app/views/katello/api/v2/organizations/show.json.rabl b/app/views/katello/api/v2/organizations/show.json.rabl index 20c14715266..220db07d02a 100644 --- a/app/views/katello/api/v2/organizations/show.json.rabl +++ b/app/views/katello/api/v2/organizations/show.json.rabl @@ -32,10 +32,12 @@ node :default_content_view_id do |org| end node(:composite_content_views_count) { Katello::ContentView.readable&.in_organization(Organization.current)&.composite&.count } +node(:rolling_content_views_count) { Katello::ContentView.readable&.in_organization(Organization.current)&.rolling&.count } node(:content_view_components_count) do Katello::ContentView.readable&. in_organization(Organization.current)&. non_composite&. + non_rolling&. non_default&. ignore_generated&.count end diff --git a/db/migrate/20241022122325_add_rolling_to_katello_content_views.rb b/db/migrate/20241022122325_add_rolling_to_katello_content_views.rb new file mode 100644 index 00000000000..fb9969bfa50 --- /dev/null +++ b/db/migrate/20241022122325_add_rolling_to_katello_content_views.rb @@ -0,0 +1,5 @@ +class AddRollingToKatelloContentViews < ActiveRecord::Migration[6.1] + def change + add_column :katello_content_views, :rolling, :boolean, :default => false + end +end diff --git a/test/actions/katello/content_view_test.rb b/test/actions/katello/content_view_test.rb index c299b85ebda..94dd32a97dd 100644 --- a/test/actions/katello/content_view_test.rb +++ b/test/actions/katello/content_view_test.rb @@ -99,6 +99,211 @@ class PublishTest < TestBase end end + class RefreshRollingRepoTest < TestBase + let(:action_class) { ::Actions::Katello::ContentView::RefreshRollingRepo } + let(:content_view) { katello_content_views(:rolling_view) } + let(:repository_deb) { katello_repositories(:debian_10_amd64) } + let(:library) { katello_environments(:library) } + let(:clone_deb) do + FactoryBot.create :katello_repository, + root: repository_deb.root, + library_instance: repository_deb, + content_view_version: content_view.versions.first, + environment: library + end + + before do + repository_deb.version_href = 'foo' + repository_deb.publication_href = 'bar' + repository_deb.save! + clone_deb.save! + end + + it 'plans' do + action.stubs(:task).returns(success_task) + refute_equal repository_deb.version_href, clone_deb.version_href + + plan_action(action, clone_deb) + + assert_action_planned_with action, ::Actions::Pulp3::Repository::RefreshDistribution, clone_deb, SmartProxy.pulp_primary + assert_action_planned_with action, ::Actions::Katello::Repository::IndexContent, id: clone_deb.id, source_repository_id: repository_deb.id + end + + it 'triggers with sync' do + sync_action = create_action ::Actions::Katello::Repository::Sync + sync_action.stubs(:task).returns(success_task) + + plan_action(sync_action, repository_deb) + + assert_action_planned_with sync_action, action_class, clone_deb + end + + it 'triggers with upload_files' do + upload_action = create_action ::Actions::Katello::Repository::UploadFiles + upload_action.stubs(:task).returns(success_task) + upload_action.stubs(:prepare_tmp_files).returns([{path: 'some/path'}]) + + plan_action(upload_action, repository_deb, [{path: 'nowhere'}]) + + assert_action_planned_with upload_action, action_class, clone_deb + end + + it 'triggers with import_upload' do + import_action = create_action ::Actions::Katello::Repository::ImportUpload + import_action.stubs(:task).returns(success_task) + file = File.join(::Katello::Engine.root, 'test', 'fixtures', 'files', 'frigg_1.0_ppc64.deb') + #action.expects(:action_subject).with(custom_repository) + + plan_action import_action, repository_deb, [{:path => file, :filename => 'frigg_1.0_ppc64.deb'}] + + assert_action_planned_with import_action, action_class, clone_deb + end + + it 'updates pulp_hrefs' do + last_changed = DateTime.new(2024, 12, 2.5) + DateTime.stubs(:now).returns(last_changed) + action_class.any_instance.expects(:plan_action).twice + + ForemanTasks.sync_task(action_class, clone_deb) + + clone_deb.reload + assert_equal repository_deb.version_href, clone_deb.version_href + assert_equal repository_deb.publication_href, clone_deb.publication_href + assert_equal repository_deb.content_id, clone_deb.content_id + assert_equal last_changed, clone_deb.last_contents_changed + end + end + + class AddRollingRepoCloneTest < TestBase + let(:action_class) { ::Actions::Katello::ContentView::AddRollingRepoClone } + let(:content_view) { katello_content_views(:rolling_view) } + let(:repository_rpm) { katello_repositories(:fedora_17_x86_64) } + let(:repository_deb) { katello_repositories(:debian_10_amd64) } + let(:library) { katello_environments(:library) } + let(:clone_rpm) do + FactoryBot.create :katello_repository, + root: repository_rpm.root, + content_view_version: content_view.versions.first, + environment: library + end + let(:clone_deb) do + FactoryBot.create :katello_repository, + root: repository_deb.root, + content_view_version: content_view.versions.first, + environment: library + end + let(:cv_env) { content_view.content_view_environment(library) } + + before do + [repository_rpm, repository_deb].each do |repository| + repository.version_href = 'foo' + repository.publication_href = 'bar' + repository.save! + end + end + + it 'plans multiple' do + action.stubs(:task).returns(success_task) + + Katello::Repository.any_instance.expects(:build_clone).returns(clone_deb) + Katello::Repository.any_instance.expects(:build_clone).returns(clone_rpm) + plan_action(action, content_view, [repository_rpm.id, repository_deb.id]) + + assert_action_planned_with action, ::Actions::Katello::ContentView::RefreshRollingRepo, clone_rpm + assert_action_planned_with action, ::Actions::Katello::ContentView::RefreshRollingRepo, clone_deb + + assert_action_planned_with action, ::Actions::Candlepin::Environment::AddContentToEnvironment, + view_env_cp_id: cv_env.cp_id, content_id: repository_rpm.content_id + assert_action_planned_with action, ::Actions::Candlepin::Environment::AddContentToEnvironment, + view_env_cp_id: cv_env.cp_id, content_id: repository_deb.content_id + end + + it 'plan refresh for existing' do + clone_deb.library_instance = repository_deb + clone_deb.version_href = 'some_version' + clone_deb.publication_href = 'some_publication' + clone_deb.save! + + refute_equal repository_deb.version_href, clone_deb.version_href + refute_equal repository_deb.publication_href, clone_deb.publication_href + # double-add + plan_action(action, content_view, [repository_deb.id]) + + assert_equal 1, content_view.get_repo_clone(library, repository_deb).count + assert_action_planned_with action, ::Actions::Katello::ContentView::RefreshRollingRepo, clone_deb + assert_action_planned_with action, ::Actions::Candlepin::Environment::AddContentToEnvironment, + view_env_cp_id: cv_env.cp_id, content_id: repository_deb.content_id + end + + it 'plans nothing' do + plan_action(action, content_view, []) + + refute_action_planned action, ::Actions::Katello::ContentView::RefreshRollingRepo + refute_action_planned action, ::Actions::Candlepin::Environment::AddContentToEnvironment + end + end + + class RemoveRollingRepoCloneTest < TestBase + let(:action_class) { ::Actions::Katello::ContentView::RemoveRollingRepoClone } + let(:content_view) { katello_content_views(:rolling_view) } + let(:repository_rpm) { katello_repositories(:fedora_17_x86_64) } + let(:repository_deb) { katello_repositories(:debian_10_amd64) } + let(:library) { katello_environments(:library) } + let(:clone_rpm) do + FactoryBot.create :katello_repository, + root: repository_rpm.root, + library_instance: repository_rpm, + content_view_version: content_view.versions.first, + environment: library + end + let(:clone_deb) do + FactoryBot.create :katello_repository, + root: repository_deb.root, + library_instance: repository_deb, + content_view_version: content_view.versions.first, + environment: library + end + let(:primary) { SmartProxy.pulp_primary } + + before do + [repository_rpm, repository_deb].each do |repository| + repository.version_href = 'foo' + repository.publication_href = 'bar' + repository.save! + end + end + + it 'plans remove multiple' do + clone_rpm.save! + clone_deb.save! + + plan_action(action, content_view, [repository_rpm.id, repository_deb.id]) + + assert_action_planned_with action, ::Actions::Pulp3::Repository::DeleteDistributions, clone_rpm.id, primary + assert_action_planned_with action, ::Actions::Pulp3::Repository::DeleteDistributions, clone_deb.id, primary + + cv_env = content_view.content_view_environment(library) + assert_action_planned_with action, ::Actions::Candlepin::Environment::SetContent, content_view, library, cv_env + end + + it 'plan ignores gone repo' do + clone_rpm.destroy + + plan_action(action, content_view, [repository_rpm.id]) + + refute_action_planned action, ::Actions::Pulp3::Repository::DeleteDistributions + + cv_env = content_view.content_view_environment(library) + assert_action_planned_with action, ::Actions::Candlepin::Environment::SetContent, content_view, library, cv_env + end + + it 'plans nothing' do + plan_action(action, content_view, []) + refute_action_planned action, ::Actions::Pulp3::Repository::DeleteDistributions + assert_action_planned action, ::Actions::Candlepin::Environment::SetContent + end + end + class PromoteToEnvironmentTest < TestBase let(:action_class) { ::Actions::Katello::ContentView::PromoteToEnvironment } @@ -393,6 +598,25 @@ class UpdateTest < TestBase end end + class UpdateRollingTest < TestBase + let(:action_class) { ::Actions::Katello::ContentView::Update } + let(:add_rolling_repo_action_class) { ::Actions::Katello::ContentView::AddRollingRepoClone } + let(:remove_rolling_repo_action_class) { ::Actions::Katello::ContentView::RemoveRollingRepoClone } + let(:content_view) { katello_content_views(:rolling_view) } + let(:repository) { katello_repositories(:rhel_6_x86_64) } + let(:action) { create_action action_class } + + it 'plans add/remove repo' do + action.expects(:action_subject).with(content_view) + old_repo_ids = content_view.repository_ids + + plan_action action, content_view, 'repository_ids' => [repository.id] + + assert_action_planned_with action, add_rolling_repo_action_class, content_view, [repository.id] + assert_action_planned_with action, remove_rolling_repo_action_class, content_view, old_repo_ids + end + end + class DestroyTest < TestBase let(:action_class) { ::Actions::Katello::ContentView::Destroy } diff --git a/test/controllers/api/v2/content_views_controller_test.rb b/test/controllers/api/v2/content_views_controller_test.rb index 98cc4e1300e..14bbfca2e87 100644 --- a/test/controllers/api/v2/content_views_controller_test.rb +++ b/test/controllers/api/v2/content_views_controller_test.rb @@ -243,8 +243,8 @@ def test_update_repositories_strings params = { :repository_ids => [repository.id.to_s] } assert_sync_task(::Actions::Katello::ContentView::Update) do |_content_view, content_view_params| - assert_equal content_view_params.key?(:repository_ids), true - assert_equal content_view_params[:repository_ids], params[:repository_ids] + assert content_view_params.key?(:repository_ids) + assert_equal [repository.id], content_view_params[:repository_ids] end put :update, params: { :id => @library_dev_staging_view.id, :content_view => params } diff --git a/test/factories/content_view_factory.rb b/test/factories/content_view_factory.rb index a3043aad81a..21a7a0286fa 100644 --- a/test/factories/content_view_factory.rb +++ b/test/factories/content_view_factory.rb @@ -11,5 +11,9 @@ trait :import_only do import_only { true } end + + trait :rolling do + rolling { true } + end end end diff --git a/test/fixtures/models/katello_content_view_environments.yml b/test/fixtures/models/katello_content_view_environments.yml index 7e55fc60f36..f1c418334ab 100644 --- a/test/fixtures/models/katello_content_view_environments.yml +++ b/test/fixtures/models/katello_content_view_environments.yml @@ -98,3 +98,13 @@ library_dev_view_acme: content_view_version_id: <%= ActiveRecord::FixtureSet.identify(:library_dev_view_version) %> created_at: <%= Time.now %> updated_at: <%= Time.now %> + +rolling_view_library: + name: Rolling view library environment + label: 'rolling_view_library' + cp_id: 14 + environment_id: <%= ActiveRecord::FixtureSet.identify(:library) %> + content_view_id: <%= ActiveRecord::FixtureSet.identify(:rolling_view) %> + content_view_version_id: <%= ActiveRecord::FixtureSet.identify(:rolling_view_library_version) %> + created_at: <%= Time.now %> + updated_at: <%= Time.now %> diff --git a/test/fixtures/models/katello_content_view_repositories.yml b/test/fixtures/models/katello_content_view_repositories.yml index 7e80607c1b5..68009430e67 100644 --- a/test/fixtures/models/katello_content_view_repositories.yml +++ b/test/fixtures/models/katello_content_view_repositories.yml @@ -37,3 +37,7 @@ library_view_python: import_only_view_fedora_17_x86_64: content_view_id: <%= ActiveRecord::FixtureSet.identify(:import_only_view) %> repository_id: <%= ActiveRecord::FixtureSet.identify(:fedora_17_x86_64) %> + +rolling_view_fedora_17_x86_64: + content_view_id: <%= ActiveRecord::FixtureSet.identify(:rolling_view) %> + repository_id: <%= ActiveRecord::FixtureSet.identify(:fedora_17_x86_64) %> diff --git a/test/fixtures/models/katello_content_view_versions.yml b/test/fixtures/models/katello_content_view_versions.yml index 4cfa9137446..b20d1d29f79 100644 --- a/test/fixtures/models/katello_content_view_versions.yml +++ b/test/fixtures/models/katello_content_view_versions.yml @@ -34,3 +34,6 @@ library_view_solve_deps_version: content_view_id: <%= ActiveRecord::FixtureSet.identify(:library_view_solve_deps) %> major: 1 +rolling_view_library_version: + content_view_id: <%= ActiveRecord::FixtureSet.identify(:rolling_view) %> + major: 1 diff --git a/test/fixtures/models/katello_content_views.yml b/test/fixtures/models/katello_content_views.yml index 3197de439dd..663d2982b7a 100644 --- a/test/fixtures/models/katello_content_views.yml +++ b/test/fixtures/models/katello_content_views.yml @@ -204,3 +204,14 @@ library_view_solve_deps: created_at: <%= Time.now %> updated_at: <%= Time.now %> organization_id: <%= ActiveRecord::FixtureSet.identify(:empty_organization) %> + +rolling_view: + name: Rolling CV + description: A rolling content view + label: 'rolling_cv' + default: false + rolling: true + next_version: 2 + created_at: <%= Time.now %> + updated_at: <%= Time.now %> + organization_id: <%= ActiveRecord::FixtureSet.identify(:empty_organization) %> diff --git a/webpack/scenes/ContentViews/Copy/__tests__/contentViewCopyResult.fixtures.json b/webpack/scenes/ContentViews/Copy/__tests__/contentViewCopyResult.fixtures.json index 84f7f14b41b..dd303b6f2df 100644 --- a/webpack/scenes/ContentViews/Copy/__tests__/contentViewCopyResult.fixtures.json +++ b/webpack/scenes/ContentViews/Copy/__tests__/contentViewCopyResult.fixtures.json @@ -1,6 +1,7 @@ { "content_host_count": 0, "composite": false, + "rolling": false, "component_ids": [], "default": false, "version_count": 0, diff --git a/webpack/scenes/ContentViews/Create/CreateContentViewForm.js b/webpack/scenes/ContentViews/Create/CreateContentViewForm.js index 62b594b160c..03e602fb435 100644 --- a/webpack/scenes/ContentViews/Create/CreateContentViewForm.js +++ b/webpack/scenes/ContentViews/Create/CreateContentViewForm.js @@ -18,6 +18,7 @@ const CreateContentViewForm = ({ setModalOpen }) => { const [description, setDescription] = useState(''); const [composite, setComposite] = useState(false); const [component, setComponent] = useState(true); + const [rolling, setRolling] = useState(false); const [autoPublish, setAutoPublish] = useState(false); const [dependencies, setDependencies] = useState(false); const [redirect, setRedirect] = useState(false); @@ -56,7 +57,8 @@ const CreateContentViewForm = ({ setModalOpen }) => { label, description, composite, - solve_dependencies: dependencies, + rolling, + solve_dependencies: (dependencies && !(rolling || composite)), auto_publish: (autoPublish && composite), })); }; @@ -70,7 +72,11 @@ const CreateContentViewForm = ({ setModalOpen }) => { if (redirect) { const { id } = response; - if (composite) { window.location.assign(`/content_views/${id}#/contentviews`); } else { window.location.assign(`/content_views/${id}#/repositories`); } + if (composite) { + window.location.assign(`/content_views/${id}#/contentviews`); + } else { + window.location.assign(`/content_views/${id}#/repositories`); + } } const submitDisabled = !name?.length || !label?.length || saving || redirect || labelValidated === 'error'; @@ -127,37 +133,51 @@ const CreateContentViewForm = ({ setModalOpen }) => { - + } + icon={} id="component" - title={__('Content view')} - onClick={() => { setComponent(true); setComposite(false); }} + title={__('Regular content view')} + onClick={() => { setComponent(true); setComposite(false); setRolling(false); }} isSelected={component} > - {__('Single content view consisting of e.g. repositories')} + {__('Contains a set of versioned and optionally filtered repositories')} - + } + icon={} id="composite" title={__('Composite content view')} - onClick={() => { setComposite(true); setComponent(false); }} + onClick={() => { setComposite(true); setComponent(false); setRolling(false); }} isSelected={composite} > - {__('Consisting of multiple content views')} + {__('Contains a set of regular content views')} + + + + } + id="rolling" + title={__('Rolling content view')} + onClick={() => { setComposite(false); setComponent(false); setRolling(true); }} + isSelected={rolling} + > + {__('Contains a set of repositories that always contain the latest synced content')} - {!composite && + {!composite && !rolling && { expect(getByText('Name')).toBeInTheDocument(); expect(getByText('Label')).toBeInTheDocument(); expect(getByText('Composite content view')).toBeInTheDocument(); - expect(getByText('Content view')).toBeInTheDocument(); + expect(getByText('Regular content view')).toBeInTheDocument(); + expect(getByText('Rolling content view')).toBeInTheDocument(); expect(getByText('Solve dependencies')).toBeInTheDocument(); expect(queryByText('Auto publish')).not.toBeInTheDocument(); diff --git a/webpack/scenes/ContentViews/Delete/__tests__/CvData.fixtures.json b/webpack/scenes/ContentViews/Delete/__tests__/CvData.fixtures.json index 7542e9c7a68..c3b2cacb8aa 100644 --- a/webpack/scenes/ContentViews/Delete/__tests__/CvData.fixtures.json +++ b/webpack/scenes/ContentViews/Delete/__tests__/CvData.fixtures.json @@ -14,6 +14,7 @@ "results": [ { "composite": false, + "rolling": false, "component_ids": [], "default": false, "version_count": 4, @@ -258,4 +259,4 @@ } } ] -} \ No newline at end of file +} diff --git a/webpack/scenes/ContentViews/Delete/__tests__/affectedHosts.fixtures.json b/webpack/scenes/ContentViews/Delete/__tests__/affectedHosts.fixtures.json index dfac7a6ff42..ea7a03c055c 100644 --- a/webpack/scenes/ContentViews/Delete/__tests__/affectedHosts.fixtures.json +++ b/webpack/scenes/ContentViews/Delete/__tests__/affectedHosts.fixtures.json @@ -113,7 +113,8 @@ "content_view": { "id": 20, "name": "cv3", - "composite": false + "composite": false, + "rolling": false }, "lifecycle_environment": { "id": 2, @@ -138,4 +139,4 @@ } } ] -} \ No newline at end of file +} diff --git a/webpack/scenes/ContentViews/Delete/__tests__/cvDetails.fixtures.json b/webpack/scenes/ContentViews/Delete/__tests__/cvDetails.fixtures.json index 06f50da1803..9665eb21904 100644 --- a/webpack/scenes/ContentViews/Delete/__tests__/cvDetails.fixtures.json +++ b/webpack/scenes/ContentViews/Delete/__tests__/cvDetails.fixtures.json @@ -1,6 +1,7 @@ { "content_host_count": 1, "composite": false, + "rolling": false, "component_ids": [], "default": false, "version_count": 4, @@ -245,4 +246,4 @@ }, "duplicate_repositories_to_publish": [], "errors": null -} \ No newline at end of file +} diff --git a/webpack/scenes/ContentViews/Details/ComponentContentViews/__tests__/compositeCVDetails.fixtures.json b/webpack/scenes/ContentViews/Details/ComponentContentViews/__tests__/compositeCVDetails.fixtures.json index 5feb78ebdff..3f625a0c202 100644 --- a/webpack/scenes/ContentViews/Details/ComponentContentViews/__tests__/compositeCVDetails.fixtures.json +++ b/webpack/scenes/ContentViews/Details/ComponentContentViews/__tests__/compositeCVDetails.fixtures.json @@ -1,6 +1,7 @@ { "content_host_count": 0, "composite": true, + "rolling": false, "component_ids": [ 5, 7 @@ -390,4 +391,4 @@ "promote_or_remove_content_views": true }, "errors": null -} \ No newline at end of file +} diff --git a/webpack/scenes/ContentViews/Details/ComponentContentViews/__tests__/publishedContentViewDetails.fixtures.json b/webpack/scenes/ContentViews/Details/ComponentContentViews/__tests__/publishedContentViewDetails.fixtures.json index 9dc0975d74b..08c20182d60 100644 --- a/webpack/scenes/ContentViews/Details/ComponentContentViews/__tests__/publishedContentViewDetails.fixtures.json +++ b/webpack/scenes/ContentViews/Details/ComponentContentViews/__tests__/publishedContentViewDetails.fixtures.json @@ -1,6 +1,7 @@ { "content_host_count": 0, "composite": false, + "rolling": false, "component_ids": [], "default": false, "version_count": 33, @@ -210,4 +211,4 @@ }, "duplicate_repositories_to_publish": [], "errors": null -} \ No newline at end of file +} diff --git a/webpack/scenes/ContentViews/Details/ContentViewDetails.js b/webpack/scenes/ContentViews/Details/ContentViewDetails.js index 9423f4e458d..53f3ca9c0ad 100644 --- a/webpack/scenes/ContentViews/Details/ContentViewDetails.js +++ b/webpack/scenes/ContentViews/Details/ContentViewDetails.js @@ -81,31 +81,17 @@ export default () => { if (status === STATUS.PENDING) return (); if (status === STATUS.ERROR) return (); - const dropDownItems = [ - { - setCopying(true); - }} - > - {__('Copy')} - , - { - setDeleting(true); - }} - > - {__('Delete')} - , - ]; - const { - name, composite, permissions, environments, versions, + name, composite, rolling, permissions, environments, versions, generated_for: generatedFor, import_only: importOnly, } = details; + + const dropDownItems = []; + if (!rolling) { + dropDownItems.push( { setCopying(true); }} > {__('Copy')}); + } + dropDownItems.push( { setDeleting(true); }} > {__('Delete')}); + const generatedContentView = generatedFor !== 'none'; const detailsTab = { key: 'details', @@ -141,11 +127,11 @@ export default () => { /* eslint-disable no-nested-ternary */ const tabs = [ detailsTab, - versionsTab, + ...(rolling ? [] : [versionsTab]), ...(composite ? [contentViewsTab] : - ((importOnly || generatedContentView) ? + ((importOnly || generatedContentView || rolling) ? [repositoriesTab] : [repositoriesTab, filtersTab])), - historyTab, + ...(rolling ? [] : [historyTab]), ]; /* eslint-enable no-nested-ternary */ @@ -162,7 +148,11 @@ export default () => { - + @@ -170,7 +160,8 @@ export default () => { - {hasPermission(permissions, 'publish_content_views') && + {!rolling && + hasPermission(permissions, 'publish_content_views') &&