# rubocop:disable Metrics/ClassLength require "pulpcore_client" module Katello module Pulp3 class Repository include Katello::Util::HttpProxy attr_accessor :repo attr_accessor :smart_proxy delegate :root, to: :repo delegate :pulp3_api, to: :smart_proxy COPY_UNIT_PAGE_SIZE = 10_000 def initialize(repo, smart_proxy) @repo = repo @smart_proxy = smart_proxy end def self.version_href?(href) /.*\/versions\/\d*\//.match(href) end def self.publication_href?(href) href.include?('/publications/') end def partial_repo_path fail NotImplementedError end def with_mirror_adapter if smart_proxy.pulp_primary? return self else return RepositoryMirror.new(self) end end def self.api(smart_proxy) api_class = RepositoryTypeManager.find_by(:pulp3_service_class, self).pulp3_api_class api_class ? api_class.new(smart_proxy) : Katello::Pulp3::Api::Core.new(smart_proxy) end def core_api Katello::Pulp3::Api::Core.new(smart_proxy) end def api @api ||= self.class.api(smart_proxy) end def ignore_404_exception(*) yield rescue api.class.api_exception_class => e raise e unless e.code == 404 nil end def published? !repo.publication_href.nil? end def repair(repository_version_href) data = api.class.repository_version_class.new api.repository_versions_api.repair(repository_version_href, data) end def skip_types nil end def content_service Katello::Pulp3::Content end def create_remote remote_file_data = api.class.remote_class.new(remote_options) reformat_api_exception do response = api.remotes_api.create(remote_file_data) repo.update!(:remote_href => response.pulp_href) end end # When updating a repository, we need to update the remote, but this is # an async task. If some validation occurs, we won't know about it until # the task runs. Errors during a repository update task are very difficult to # handle once the task is in its run phase, so this creates a test remote # with a random name in order to validate the remote's configuration def create_test_remote test_remote_options = remote_options test_remote_options[:name] = test_remote_name remote_file_data = api.class.remote_class.new(test_remote_options) reformat_api_exception do response = api.remotes_api.create(remote_file_data) #delete is async, but if its not properly deleted, orphan cleanup will take care of it later delete_remote(response.pulp_href) end end def test_remote_name "test_remote_#{SecureRandom.uuid}" end def reformat_api_exception yield rescue api.class.client_module::ApiError => exception body = JSON.parse(exception.response_body) rescue body body = body.values.join(',') if body.respond_to?(:values) raise ::Katello::Errors::Pulp3Error, body end def update_remote href = repo.remote_href if remote_options[:url].blank? if href repo.update(remote_href: nil) delete_remote href end else if href remote_partial_update else create_remote end end end def remote_partial_update api.remotes_api.partial_update(repo.remote_href, remote_options) end def delete_remote(href = repo.remote_href) ignore_404_exception { api.remotes_api.delete(href) } if href end def self.instance_for_type(repo, smart_proxy) Katello::RepositoryTypeManager.enabled_repository_types[repo.root.content_type].pulp3_service_class.new(repo, smart_proxy) end def should_purge_empty_contents? false end def generate_backend_object_name "#{root.label}-#{repo.id}#{rand(9999)}" end def repository_reference RepositoryReference.find_by(:root_repository_id => repo.root_id, :content_view_id => repo.content_view.id) end def distribution_reference DistributionReference.find_by(:repository_id => repo.id) end def create_mirror_entities RepositoryMirror.new(self).create_entities end def refresh_mirror_entities RepositoryMirror.new(self).refresh_entities end def mirror_needs_updates? RepositoryMirror.new(self).needs_updates? end def refresh_if_needed tasks = [] tasks << update_remote #always update remote tasks << update_distribution if distribution_needs_update? tasks.compact end def get_remote(href = repo.remote_href) api.remotes_api.read(href) end def get_distribution(href = distribution_reference.href) api.get_distribution(href) end def distribution_needs_update? if distribution_reference expected = secure_distribution_options(relative_path).except(:name).compact actual = get_distribution.to_hash expected != actual.slice(*expected.keys) elsif repo.environment true else false end end def compute_remote_options(computed_options = remote_options) computed_options.except(:name, :client_key) end def create(force = false) if force || !repository_reference response = api.repositories_api.create(create_options) RepositoryReference.where( root_repository_id: repo.root_id, content_view_id: repo.content_view.id).destroy_all RepositoryReference.where( root_repository_id: repo.root_id, content_view_id: repo.content_view.id, repository_href: response.pulp_href).create! response end end def update api.repositories_api.update(repository_reference.try(:repository_href), create_options) end def list(options) api.repositories_api.list(options).results end def read api.repositories_api.read(repository_reference.try(:repository_href)) end def delete_repository(repo_reference = repository_reference) href = repo_reference.try(:repository_href) repo_reference.try(:destroy) ignore_404_exception { api.repositories_api.delete(href) } if href end def sync(options = {}) repository_sync_url_data = api.class.repository_sync_url_class.new(sync_url_params(options)) [api.repositories_api.sync(repository_reference.repository_href, repository_sync_url_data)] end def sync_url_params(_sync_options) params = {remote: repo.remote_href, mirror: repo.root.mirror_on_sync} params[:skip_types] = skip_types if skip_types params end def create_publication publication_data = api.class.publication_class.new(publication_options(repo.version_href)) api.publications_api.create(publication_data) end def publication_options(repository_version) { repository_version: repository_version } end def relative_path repo.relative_path.sub(/^\//, '') end def refresh_distributions if repo.docker? dist = lookup_distributions(base_path: repo.container_repository_name).first else dist = lookup_distributions(base_path: repo.relative_path).first end # First check if the distribution exists if dist dist_ref = distribution_reference # If we have a DistributionReference, update the distribution if dist_ref return update_distribution # If no DistributionReference, create a DistributionReference and return else save_distribution_references([dist.pulp_href]) return [] end end # So far, it looks like there is no distribution. Try to create one. begin create_distribution(relative_path) rescue api.class.client_module::ApiError => e # Now it seems there is a distribution. Fetch it and save the reference. if e.message.include?("\"base_path\":[\"This field must be unique.\"]") || e.message.include?("\"base_path\":[\"Overlaps with existing distribution\"") dist = lookup_distributions(base_path: repo.relative_path).first save_distribution_references([dist.pulp_href]) return [] else raise e end end end def create_distribution(path) distribution_data = api.class.distribution_class.new(secure_distribution_options(path)) api.distributions_api.create(distribution_data) end def lookup_distributions(args) api.distributions_api.list(args).results end def update_distribution if distribution_reference options = secure_distribution_options(relative_path).except(:name) distribution_reference.update(:content_guard_href => options[:content_guard]) api.distributions_api.partial_update(distribution_reference.href, options) end end def copy_units_by_href(unit_hrefs) tasks = [] unit_hrefs.each_slice(COPY_UNIT_PAGE_SIZE) do |slice| tasks << create_version(:add_content_units => slice) end tasks end def copy_all(source_repository, mirror: false) if mirror data = api.class.add_remove_content_class.new( base_version: source_repository.version_href) [api.repositories_api.modify(repository_reference.repository_href, data)] elsif api.respond_to? :copy_api data = api.class.copy_class.new data.config = [{ source_repo_version: source_repository.version_href, dest_repo: repository_reference.repository_href }] [api.copy_api.copy_content(data)] else copy_content_for_source(source_repository) end end def copy_version(from_repository) create_version(:base_version => from_repository.version_href) end def version_zero? repo.version_href.ends_with?('/versions/0/') end def delete_version ignore_404_exception { api.repository_versions_api.delete(repo.version_href) } unless version_zero? end def create_version(options = {}) api.repositories_api.modify(repository_reference.repository_href, options) end def save_distribution_references(hrefs) hrefs.each do |href| pulp3_distribution_data = api.get_distribution(href) path, content_guard_href = pulp3_distribution_data&.base_path, pulp3_distribution_data&.content_guard unless distribution_reference # Ensure that duplicates won't be created in the case of a race condition DistributionReference.where(path: path, href: href, repository_id: repo.id, content_guard_href: content_guard_href).first_or_create! end end end def delete_distributions if (dist_ref = distribution_reference) ignore_404_exception { api.delete_distribution(dist_ref.href) } dist_ref.destroy! end end def delete_distributions_by_path path = relative_path dists = lookup_distributions(base_path: path) task = api.delete_distribution(dists.first.pulp_href) if dists.first Katello::Pulp3::DistributionReference.where(:path => path).destroy_all task end def common_remote_options remote_options = { tls_validation: root.verify_ssl_on_sync, name: generate_backend_object_name, url: root.url, proxy_url: root.http_proxy&.url, proxy_username: root.http_proxy&.username, proxy_password: root.http_proxy&.password, total_timeout: Setting[:sync_connect_timeout] } remote_options[:url] = root.url unless root.url.blank? remote_options[:download_concurrency] = root.download_concurrency unless root.download_concurrency.blank? if !root.upstream_username.blank? && !root.upstream_password.blank? remote_options.merge!(username: root.upstream_username, password: root.upstream_password) end remote_options.merge!(ssl_remote_options) end def create_options { name: generate_backend_object_name }.merge!(specific_create_options) end def specific_create_options {} end def secure_distribution_options(path) secured_distribution_options = {} if root.unprotected secured_distribution_options[:content_guard] = nil else secured_distribution_options[:content_guard] = ::Katello::Pulp3::ContentGuard.first.pulp_href end secured_distribution_options.merge!(distribution_options(path)) end def ssl_remote_options if root.redhat? && Katello::Resources::CDN::CdnResource.redhat_cdn?(root.url) { client_cert: root.product.certificate, client_key: root.product.key, ca_cert: Katello::Repository.feed_ca_cert(root.url) } elsif root.custom? { client_cert: root.ssl_client_cert&.content, client_key: root.ssl_client_key&.content, ca_cert: root.ssl_ca_cert&.content } else {} end end def lookup_version(href) api.repository_versions_api.read(href) if href rescue api.class.api_exception_class => e Rails.logger.error "Exception when calling repository_versions_api->read: #{e}" nil end def lookup_publication(href) api.publications_api.read(href) if href rescue api.class.api_exception_class => e Rails.logger.error "Exception when calling publications_api->read: #{e}" nil end def remove_content(content_units) if repo.root.content_type == "docker" api.repositories_api.remove(repository_reference.repository_href, content_units: content_units.map(&:pulp_id)) else api.repositories_api.modify(repository_reference.repository_href, remove_content_units: content_units.map(&:pulp_id)) end end def add_content(content_unit_href) content_unit_href = [content_unit_href] unless content_unit_href.is_a?(Array) api.repositories_api.modify(repository_reference.repository_href, add_content_units: content_unit_href) end def add_content_for_repo(repository_href, content_unit_href) content_unit_href = [content_unit_href] unless content_unit_href.is_a?(Array) api.repositories_api.modify(repository_href, add_content_units: content_unit_href) end def unit_keys(uploads) uploads.map do |upload| upload.except('id') end end def retain_package_versions_count return 0 if root.retain_package_versions_count.nil? || root.mirror_on_sync? root.retain_package_versions_count.to_i end end end end