Katello/katello

View on GitHub
app/controllers/katello/api/v2/repositories_controller.rb

Summary

Maintainability
F
4 days
Test Coverage
module Katello
  class Api::V2::RepositoriesController < Api::V2::ApiController # rubocop:disable Metrics/ClassLength
    include Katello::Concerns::FilteredAutoCompleteSearch

    generic_repo_wrap_params = []
    RepositoryTypeManager.generic_remote_options(defined_only: true).each do |option|
      generic_repo_wrap_params << option.name
    end

    repo_wrap_params = RootRepository.attribute_names + generic_repo_wrap_params

    wrap_parameters :repository, :include => repo_wrap_params

    CONTENT_CREDENTIAL_GPG_KEY_TYPE = "gpg_key".freeze
    CONTENT_CREDENTIAL_SSL_CA_CERT_TYPE = "ssl_ca_cert".freeze
    CONTENT_CREDENTIAL_SSL_CLIENT_CERT_TYPE = "ssl_client_cert".freeze
    CONTENT_CREDENTIAL_SSL_CLIENT_KEY_TYPE = "ssl_client_key".freeze

    before_action :find_optional_organization, :only => [:index, :auto_complete_search]
    before_action :find_product, :only => [:index, :auto_complete_search]
    before_action :find_product_for_create, :only => [:create]
    before_action :find_organization_from_product, :only => [:create]
    before_action :find_unauthorized_katello_resource, :only => [:gpg_key_content]
    before_action :find_authorized_katello_resource, :only => [:show, :update, :destroy, :sync,
                                                               :remove_content, :upload_content, :republish,
                                                               :import_uploads, :verify_checksum, :reclaim_space]
    before_action :find_content, :only => :remove_content
    before_action :find_organization_from_repo, :only => [:update]
    before_action :error_on_rh_product, :only => [:create]
    before_action :check_import_parameters, :only => [:import_uploads]
    before_action(:only => [:create, :update]) { find_content_credential CONTENT_CREDENTIAL_GPG_KEY_TYPE }
    before_action(:only => [:create, :update]) { find_content_credential CONTENT_CREDENTIAL_SSL_CA_CERT_TYPE }
    before_action(:only => [:create, :update]) { find_content_credential CONTENT_CREDENTIAL_SSL_CLIENT_CERT_TYPE }
    before_action(:only => [:create, :update]) { find_content_credential CONTENT_CREDENTIAL_SSL_CLIENT_KEY_TYPE }
    skip_before_action :authorize, :only => [:gpg_key_content]
    skip_before_action :check_media_type, :only => [:upload_content]

    def custom_index_relation(collection)
      collection.includes(:product)
    end

    def_param_group :repo do
      param :url, String, :desc => N_("repository source url")
      param :os_versions, Array, :desc => N_("Identifies whether the repository should be unavailable on a client with a non-matching OS version.
Pass [] to make repo available for clients regardless of OS version. Maximum length 1; allowed tags are: %s") % Katello::RootRepository::ALLOWED_OS_VERSIONS.join(', ')
      param :gpg_key_id, :number, :desc => N_("id of the gpg key that will be assigned to the new repository"), :allow_nil => true
      param :ssl_ca_cert_id, :number, :desc => N_("Identifier of the content credential containing the SSL CA Cert"), :allow_nil => true
      param :ssl_client_cert_id, :number, :desc => N_("Identifier of the content credential containing the SSL Client Cert"), :allow_nil => true
      param :ssl_client_key_id, :number, :desc => N_("Identifier of the content credential containing the SSL Client Key"), :allow_nil => true
      param :unprotected, :bool, :desc => N_("true if this repository can be published via HTTP")
      param :checksum_type, String, :desc => N_("Checksum of the repository, currently 'sha1' & 'sha256' are supported")
      param :docker_upstream_name, String, :desc => N_("Name of the upstream docker repository")
      param :include_tags, Array, :desc => N_("Comma-separated list of tags to sync for a container image repository")
      param :exclude_tags, Array, :desc => N_("Comma-separated list of tags to exclude when syncing a container image repository. Default: any tag ending in \"-source\"")
      param :download_policy, ["immediate", "on_demand"], :desc => N_("download policy for yum, deb, and docker repos (either 'immediate' or 'on_demand')")
      param :download_concurrency, :number, :desc => N_("Used to determine download concurrency of the repository in pulp3. Use value less than 20. Defaults to 10")
      param :mirroring_policy, Katello::RootRepository::MIRRORING_POLICIES, :desc => N_("Policy to set for mirroring content.  Must be one of %s.") % RootRepository::MIRRORING_POLICIES
      param :verify_ssl_on_sync, :bool, :desc => N_("if true, Katello will verify the upstream url's SSL certifcates are signed by a trusted CA")
      param :upstream_username, String, :desc => N_("Username of the upstream repository user used for authentication")
      param :upstream_password, String, :desc => N_("Password of the upstream repository user used for authentication")
      param :upstream_authentication_token, String, :desc => N_("Upstream authentication token string for yum repositories.")
      param :deb_releases, String, :desc => N_("whitespace-separated list of releases to be synced from deb-archive")
      param :deb_components, String, :desc => N_("whitespace-separated list of repo components to be synced from deb-archive")
      param :deb_architectures, String, :desc => N_("whitespace-separated list of architectures to be synced from deb-archive")
      param :ignorable_content, Array, :desc => N_("List of content units to ignore while syncing a yum repository. Must be subset of %s") % RootRepository::IGNORABLE_CONTENT_UNIT_TYPES.join(",")
      param :ansible_collection_requirements, String, :desc => N_("Contents of requirement yaml file to sync from URL")
      param :ansible_collection_auth_url, String, :desc => N_("The URL to receive a session token from, e.g. used with Automation Hub.")
      param :ansible_collection_auth_token, String, :desc => N_("The token key to use for authentication.")
      param :http_proxy_policy, ::Katello::RootRepository::HTTP_PROXY_POLICIES, :desc => N_("policies for HTTP proxy for content sync")
      param :http_proxy_id, :number, :desc => N_("ID of a HTTP Proxy")
      param :arch, String, :desc => N_("Architecture of content in the repository")
      param :retain_package_versions_count, :number, :desc => N_("The maximum number of versions of each package to keep.")
      param :metadata_expire, :number, :desc => N_("Time to expire yum metadata in seconds. Only relevant for custom yum repositories.")
      RepositoryTypeManager.generic_remote_options(defined_only: true).each do |option|
        param option.name, option.type, :desc => N_(option.description)
      end
    end

    def_param_group :repo_create do
      param :label, String, :required => false
      param :product_id, :number, :required => true, :desc => N_("Product the repository belongs to")
      param :content_type, String, :required => true, :desc => N_("Type of repository. Available types endpoint: /katello/api/repositories/repository_types")
    end

    api :GET, "/repositories", N_("List of enabled repositories")
    api :GET, "/content_views/:id/repositories", N_("List of repositories for a content view")
    api :GET, "/organizations/:organization_id/repositories", N_("List of repositories in an organization")
    api :GET, "/organizations/:organization_id/environments/:environment_id/repositories", _("List repositories in the environment")
    api :GET, "/products/:product_id/repositories", N_("List of repositories for a product")
    api :GET, "/environments/:environment_id/products/:product_id/repositories", N_("List of repositories belonging to a product in an environment")
    param :organization_id, :number, :desc => N_("ID of an organization to show repositories in")
    param :product_id, :number, :desc => N_("ID of a product to show repositories of")
    param :environment_id, :number, :desc => N_("ID of an environment to show repositories in")
    param :content_view_id, :number, :desc => N_("ID of a content view to show repositories in")
    param :content_view_version_id, :number, :desc => N_("ID of a content view version to show repositories in")
    param :deb_id, String, :desc => N_("Id of a deb package to find repositories that contain the deb")
    param :erratum_id, String, :desc => N_("Id of an erratum to find repositories that contain the erratum")
    param :rpm_id, String, :desc => N_("Id of a rpm package to find repositories that contain the rpm")
    param :file_id, String, :desc => N_("Id of a file to find repositories that contain the file")
    param :ansible_collection_id, String, :desc => N_("Id of an ansible collection to find repositories that contain the ansible collection")
    param :library, :bool, :desc => N_("show repositories in Library and the default content view")
    param :archived, :bool, :desc => N_("show archived repositories")
    param :content_type, String, :desc => N_("Limit the repository type. Available types endpoint: /katello/api/repositories/repository_types")
    param :name, String, :desc => N_("name of the repository"), :required => false
    param :label, String, :desc => N_("label of the repository"), :required => false
    param :description, String, :desc => N_("description of the repository")
    param :available_for, String, :desc => N_("interpret specified object to return only Repositories that can be associated with specified object.  Only 'content_view' & 'content_view_version' are supported."),
          :required => false
    param :with_content, String, :desc => N_("Filter repositories by content unit type (erratum, docker_tag, etc.). Check the \"Indexed?\" types here: /katello/api/repositories/repository_types")
    param :download_policy, ::Katello::RootRepository::DOWNLOAD_POLICIES, :desc => N_("limit to only repositories with this download policy")
    param :username, String, :desc => N_("only show the repositories readable by this user with this username")
    param_group :search, Api::V2::ApiController
    add_scoped_search_description_for(Repository)
    def index
      unless params[:content_type].empty? || RepositoryTypeManager.find(params[:content_type])
        msg = _("Invalid params provided - content_type must be one of %s") %
          RepositoryTypeManager.enabled_repository_types.keys.sort.join(",")
        fail HttpErrors::UnprocessableEntity, msg
      end
      unless params[:with_content].empty? || RepositoryTypeManager.find_content_type(params[:with_content], true)
        msg = _("Invalid params provided - with_content must be one of %s") %
          RepositoryTypeManager.indexable_content_types.map(&:label).sort.join(",")
        fail HttpErrors::UnprocessableEntity, msg
      end
      base_args = [index_relation.distinct, :name, :asc]
      options = {:includes => [:environment, {:root => [:gpg_key, :product]}]}

      respond_to do |format|
        format.csv do
          options[:csv] = true
          repos = scoped_search(*base_args, options)
          csv_response(repos,
                       [:id, :name, :description, :label, :content_type, :arch, :url, :major, :minor,
                        :content_label, :pulp_id, :container_repository_name,
                        :download_policy, 'relative_path', 'product.id', 'product.name',
                        'environment_id'],
                       ['Id', 'Name', 'Description', 'label', 'Content Type', 'Arch', 'Url', 'Major', 'Minor',
                        'Content Label', 'Pulp Id', 'Container Repository Name', 'Download Policy', 'Relative Path',
                        'Product Id', 'Product Name',
                        'Environment Id'])
        end
        format.any do
          repos = scoped_search(*base_args, options)
          respond(:collection => repos)
        end
      end
    end

    def index_relation
      query = Repository.readable
      query = query.with_content(params[:with_content]) if params[:with_content]
      query = index_relation_product(query)
      query = query.with_type(params[:content_type]) if params[:content_type]
      query = query.where(:root_id => RootRepository.where(:name => params[:name])) if params[:name]
      query = query.where(:root_id => RootRepository.where(:label => params[:label])) if params[:label]
      query = index_relation_content_unit(query)
      query = index_relation_content_view(query)
      query = index_relation_environment(query)
      query
    end

    def index_relation_product(query)
      query = query.joins(:root => :product).where("#{Product.table_name}.organization_id" => @organization) if @organization
      query = query.joins(:root).where("#{RootRepository.table_name}.product_id" => @product.id) if @product
      query = query.joins(:root).where("#{RootRepository.table_name}.download_policy" => params[:download_policy]) if params[:download_policy]
      query
    end

    def index_relation_content_view(query)
      if params[:content_view_version_id]
        query = query.where(:content_view_version_id => params[:content_view_version_id])
        query = query.archived if ::Foreman::Cast.to_bool params[:archived]
        query = Katello::Repository.where(:id => query.select(:library_instance_id)) if params[:library]
      elsif params[:content_view_id]
        query = filter_by_content_view(query, params[:content_view_id], params[:environment_id], params[:available_for] == 'content_view')
      end
      query
    end

    def index_relation_environment(query)
      if params[:environment_id] && !params[:library]
        query = query.where(:environment_id => params[:environment_id])
      elsif params[:environment_id] && params[:library]
        instances = query.where(:environment_id => params[:environment_id])
        instance_ids = instances.pluck(:library_instance_id).reject(&:blank?)
        instance_ids += instances.where(:library_instance_id => nil)
        query = Repository.where(:id => instance_ids)
      elsif (params[:library] && !params[:environment_id]) || (params[:environment_id].blank? && params[:content_view_version_id].blank? && params[:content_view_id].blank?)
        if params[:available_for] == 'content_view_version'
          query = query.where.not(:content_view_version_id => nil, :environment_id => nil)
        elsif @organization
          query = query.where(:content_view_version_id => @organization.default_content_view.versions.first.id)
        else
          query = query.in_default_view
        end
      end
      query
    end

    def index_relation_content_unit(query)
      if params[:deb_id]
        query = query.joins(:debs)
          .where("#{Deb.table_name}.id" => Deb.with_identifiers(params[:deb_id]))
      end

      if params[:erratum_id]
        query = query.joins(:errata)
          .where("#{Erratum.table_name}.id" => Erratum.with_identifiers(params[:erratum_id]))
      end

      if params[:rpm_id]
        query = query.joins(:rpms)
          .where("#{Rpm.table_name}.id" => Rpm.with_identifiers(params[:rpm_id]))
      end

      if params[:file_id]
        query = query.joins(:files)
          .where("#{FileUnit.table_name}.id" => FileUnit.with_identifiers(params[:file_id]))
      end

      if params[:ansible_collection_id]
        query = query.joins(:ansible_collections)
                    .where("#{AnsibleCollection.table_name}.id" => AnsibleCollection.with_identifiers(params[:ansible_collection_id]))
      end

      generic_type_param = RepositoryTypeManager.generic_content_types.find { |type| params["#{type}_id".to_sym] }
      if generic_type_param
        query = query.joins(:generic_content_units)
                     .where("#{GenericContentUnit.table_name}.id" => GenericContentUnit.with_identifiers(params["#{generic_type_param}_id".to_sym]))
      end

      query
    end

    api :GET, "/repositories/compare/", N_("List :resource")
    param :content_view_version_ids, Array, :desc => N_("content view versions to compare")
    param :repository_id, :number, :desc => N_("Library repository id to restrict comparisons to")
    param :restrict_comparison, String, :desc => N_("Return same, different or all results")

    def compare
      fail _("No content_view_version_ids provided") if params[:content_view_version_ids].empty?
      @versions = ContentViewVersion.readable.where(:id => params[:content_view_version_ids])
      if @versions.count != params[:content_view_version_ids].uniq.length
        missing = params[:content_view_version_ids] - @versions.pluck(:id)
        fail HttpErrors::NotFound, _("Couldn't find content view versions '%s'") % missing.join(',')
      end

      archived_version_repos = Katello::Repository.where(:content_view_version_id => @versions&.pluck(:id))&.archived
      repos = Katello::Repository.where(id: archived_version_repos&.pluck(:library_instance_id))
      repos = repos.where(:root_id => @repo.root_id) if @repo
      repositories = restrict_comparison(repos, @versions, params[:restrict_comparison])
      collection = scoped_search(repositories.distinct, :name, :asc)
      collection[:results] = collection[:results].map { |item| ContentViewVersionComparePresenter.new(item, @versions, @repo) }
      respond_for_index(:collection => collection)
    end

    def restrict_comparison(collection, content_view_versions = nil, compare = 'all')
      case compare
      when 'same'
        same_repo_ids = compare_same(collection, content_view_versions)
        collection.where(id: same_repo_ids)
      when 'different'
        same_repo_ids = compare_same(collection, content_view_versions)
        collection.where.not(id: same_repo_ids)
      else
        collection
      end
    end

    def compare_same(collection, content_view_versions = nil)
      same_repo_ids = []
      collection.each do |repo|
        if (content_view_versions&.pluck(:id)&.- repo.published_in_versions&.pluck(:id))&.empty?
          same_repo_ids << repo.id
        end
      end
      same_repo_ids
    end

    api :POST, "/repositories", N_("Create a custom repository")
    param :name, String, :desc => N_("Name of the repository"), :required => true
    param :description, String, :desc => N_("Description of the repository"), :required => false
    param_group :repo_create
    param_group :repo
    def create
      repo_params = repository_params
      unless RepositoryTypeManager.creatable_by_user?(repo_params[:content_type], false)
        msg = _("Invalid params provided - content_type must be one of %s") %
          RepositoryTypeManager.creatable_repository_types.keys.sort.join(",")
        fail HttpErrors::UnprocessableEntity, msg
      end

      if !repo_params[:url].nil? && URI(repo_params[:url]).userinfo
        fail "Do not include the username/password in the URL. Use the username/password settings instead."
      end

      gpg_key = get_content_credential(repo_params, CONTENT_CREDENTIAL_GPG_KEY_TYPE)
      ssl_ca_cert = get_content_credential(repo_params, CONTENT_CREDENTIAL_SSL_CA_CERT_TYPE)
      ssl_client_cert = get_content_credential(repo_params, CONTENT_CREDENTIAL_SSL_CLIENT_CERT_TYPE)
      ssl_client_key = get_content_credential(repo_params, CONTENT_CREDENTIAL_SSL_CLIENT_KEY_TYPE)

      repo_params[:label] = labelize_params(repo_params)
      repo_params[:arch] = repo_params[:arch] || 'noarch'
      repo_params[:url] = nil if repo_params[:url].blank?
      repo_params[:unprotected] = repo_params.key?(:unprotected) ? repo_params[:unprotected] : true
      repo_params[:gpg_key] = gpg_key
      repo_params[:ssl_ca_cert] = ssl_ca_cert
      repo_params[:ssl_client_cert] = ssl_client_cert
      repo_params[:ssl_client_key] = ssl_client_key

      root = construct_repo_from_params(repo_params)
      sync_task(::Actions::Katello::Repository::CreateRoot, root)
      @repository = root.reload.library_instance
      respond_for_create(:resource => @repository)
    end

    api :GET, "/repositories/repository_types", N_("Show the available repository types")
    param :creatable, :bool, :desc => N_("When set to 'True' repository types that are creatable will be returned")
    def repository_types
      creatable = ::Foreman::Cast.to_bool(params[:creatable])
      repo_types = creatable ? RepositoryTypeManager.creatable_repository_types : RepositoryTypeManager.enabled_repository_types
      render :json => repo_types.values
    end

    api :PUT, "/repositories/:id/republish", N_("Forces a republish of the specified repository, regenerating metadata and symlinks on the filesystem. Not allowed for repositories with the 'Complete Mirroring' mirroring policy.")
    param :id, :number, :desc => N_("Repository identifier"), :required => true
    param :force, :bool, :desc => N_("Force metadata regeneration to proceed. Dangerous when repositories use the 'Complete Mirroring' mirroring policy")
    def republish
      if @repository.mirroring_policy == Katello::RootRepository::MIRRORING_POLICY_COMPLETE && !::Foreman::Cast.to_bool(params[:force])
        fail HttpErrors::BadRequest, _("Metadata republishing is risky on 'Complete Mirroring' repositories. Change the mirroring policy and try again.
Alternatively, use the 'force' parameter to regenerate metadata locally. On the next sync, the upstream repository's metadata will overwrite local metadata for 'Complete Mirroring' repositories.")
      end
      task = async_task(::Actions::Katello::Repository::MetadataGenerate, @repository)
      respond_for_async :resource => task
    end

    api :GET, "/repositories/:id", N_("Show a repository")
    param :id, :number, :required => true, :desc => N_("repository ID")
    param :organization_id, :number, :desc => N_("Organization ID")
    def show
      respond_for_show(:resource => @repository)
    end

    api :POST, "/repositories/:id/sync", N_("Sync a repository")
    param :id, :number, :required => true, :desc => N_("repository ID")
    param :incremental, :bool, :desc => N_("perform an incremental import"), :required => false
    param :skip_metadata_check, :bool, :desc => N_("Force sync even if no upstream changes are detected. Only used with yum or deb repositories."), :required => false
    param :validate_contents, :bool, :desc => N_("Force a sync and validate the checksums of all content. Only used with yum repositories."), :required => false
    def sync
      fail HttpErrors::BadRequest, _("attempted to sync a non-library repository.") unless @repository.library_instance?
      sync_options = {
        :skip_metadata_check => ::Foreman::Cast.to_bool(params[:skip_metadata_check]),
        :validate_contents => ::Foreman::Cast.to_bool(params[:validate_contents]),
        :incremental => ::Foreman::Cast.to_bool(params[:incremental])
      }

      if @repository.url.blank?
        fail HttpErrors::BadRequest, _("attempted to sync without a feed URL")
      end

      task = async_task(::Actions::Katello::Repository::Sync, @repository, sync_options)
      respond_for_async :resource => task
    rescue Errors::InvalidActionOptionError => e
      raise HttpErrors::BadRequest, e.message
    end

    api :POST, "/repositories/:id/verify_checksum", N_("Verify checksum of repository contents")
    param :id, :number, :required => true, :desc => N_("repository ID")
    def verify_checksum
      task = async_task(::Actions::Katello::Repository::VerifyChecksum, @repository)
      respond_for_async :resource => task
    rescue Errors::InvalidActionOptionError => e
      raise HttpErrors::BadRequest, e.message
    end

    api :POST, "/repositories/:id/reclaim_space", N_("Reclaim space from an On Demand repository")
    param :id, :number, :required => true, :desc => N_("repository ID")
    def reclaim_space
      if @repository.download_policy != ::Katello::RootRepository::DOWNLOAD_ON_DEMAND
        fail HttpErrors::BadRequest, _("Only On Demand repositories may have space reclaimed.")
      end
      task = async_task(::Actions::Pulp3::Repository::ReclaimSpace, @repository)
      respond_for_async :resource => task
    rescue Errors::InvalidActionOptionError => e
      raise HttpErrors::BadRequest, e.message
    end

    api :PUT, "/repositories/:id", N_("Update a repository")
    param :id, :number, :required => true, :desc => N_("repository ID")
    param :name, String, :required => false
    param :description, String, :desc => N_("description of the repository"), :required => false
    param_group :repo
    def update
      repo_params = repository_params
      if !repo_params[:url].nil? && URI(repo_params[:url]).userinfo
        fail "Do not include the username/password in the URL. Use the username/password settings instead."
      end

      if @repository.generic?
        generic_remote_options = generic_remote_options_hash(repo_params)
        repo_params[:generic_remote_options] = generic_remote_options.to_json
        RepositoryTypeManager.generic_remote_options.each do |option|
          repo_params&.delete(option.name)
        end
      end

      sync_task(::Actions::Katello::Repository::Update, @repository.root, repo_params)
      respond_for_show(:resource => @repository)
    end

    api :DELETE, "/repositories/:id", N_("Destroy a custom repository")
    param :id, :number, :required => true
    param :remove_from_content_view_versions, :bool, :required => false, :desc => N_("Force delete the repository by removing it from all content view versions")
    param :delete_empty_repo_filters, :bool, :required => false, :desc => N_("Delete content view filters that have this repository as the last associated repository. Defaults to true. If false, such filters will now apply to all repositories in the content view.")
    def destroy
      sync_task(::Actions::Katello::Repository::Destroy, @repository,
                remove_from_content_view_versions: ::Foreman::Cast.to_bool(params.fetch(:remove_from_content_view_versions, false)),
                delete_empty_repo_filters: ::Foreman::Cast.to_bool(params.fetch(:delete_empty_repo_filters, true))
                )
      respond_for_destroy
    end

    api :PUT, "/repositories/:id/remove_packages"
    api :PUT, "/repositories/:id/remove_docker_manifests"
    api :PUT, "/repositories/:id/remove_content"
    desc "Remove content from a repository"
    param :id, :number, :required => true, :desc => "repository ID"
    param 'ids', Array, :required => true, :desc => "Array of content ids to remove"
    param :content_type, String, :required => false, :desc => N_("The type of content to remove (srpm, docker_manifest, etc.). Check removable types here: /katello/api/repositories/repository_types")
    param 'sync_capsule', :bool, :desc => N_("Whether or not to sync an external capsule after upload. Default: true")
    def remove_content
      unless params[:content_type].empty? || RepositoryTypeManager.removable_content_types.map(&:label).include?(params[:content_type])
        msg = _("Invalid params provided - content_type must be one of %s") %
          RepositoryTypeManager.removable_content_types.map(&:label).sort.join(",")
        fail HttpErrors::UnprocessableEntity, msg
      end
      sync_capsule = ::Foreman::Cast.to_bool(params.fetch(:sync_capsule, true))
      fail _("No content ids provided") if @content.blank?
      respond_for_async :resource => sync_task(::Actions::Katello::Repository::RemoveContent, @repository, @content, content_type: params[:content_type], sync_capsule: sync_capsule)
    end

    api :POST, "/repositories/:id/upload_content", N_("Upload content into the repository")
    param :id, :number, :required => true, :desc => N_("repository ID")
    param :content, File, :required => true, :desc => N_("Content files to upload. Can be a single file or array of files.")
    param :content_type, String, :required => false, :desc => N_("The type of content to upload (srpm, file, etc.). Check uploadable types here: /katello/api/repositories/repository_types")
    def upload_content
      fail Katello::Errors::InvalidRepositoryContent, _("Cannot upload Container Image content.") if @repository.docker?
      fail Katello::Errors::InvalidRepositoryContent, _("Cannot upload Ansible collections.") if @repository.ansible_collection?
      unless params[:content_type].empty? || RepositoryTypeManager.uploadable_content_types.map(&:label).include?(params[:content_type])
        msg = _("Invalid params provided - content_type must be one of %s") %
          RepositoryTypeManager.uploadable_content_types.map(&:label).sort.join(",")
        fail HttpErrors::UnprocessableEntity, msg
      end

      filepaths = Array.wrap(params[:content]).compact.collect do |content|
        {path: content.path, filename: content.original_filename}
      end

      if !filepaths.blank?
        sync_task(::Actions::Katello::Repository::UploadFiles, @repository, filepaths, params[:content_type])
        render :json => {:status => "success", :filenames => filepaths.map { |item| item[:filename] }}
      else
        fail HttpErrors::BadRequest, _("No file uploaded")
      end
    rescue Katello::Errors::InvalidRepositoryContent => error
      respond_for_exception(
        error,
        :status => :unprocessable_entity,
        :text => error.message,
        :errors => [error.message],
        :with_logging => true
      )
    end

    api :PUT, "/repositories/:id/import_uploads", N_("Import uploads into a repository")
    param :id, :number, :required => true, :desc => N_("Repository id")
    param :async, :bool, desc: N_("Do not wait for the ImportUpload action to finish. Default: false")
    param 'publish_repository', :bool, :desc => N_("Whether or not to regenerate the repository on disk. Default: true")
    param 'sync_capsule', :bool, :desc => N_("Whether or not to sync an external capsule after upload. Default: true")
    param :content_type, RepositoryTypeManager.uploadable_content_types(false).map(&:label), :required => false, :desc => N_("content type ('deb', 'docker_manifest', 'file', 'ostree_ref', 'rpm', 'srpm')")
    param :uploads, Array, :desc => N_("Array of uploads to import") do
      param 'id', String, :required => true
      param 'content_unit_id', String
      param 'size', String
      param 'checksum', String
      param 'name', String, :desc => N_("Needs to only be set for file repositories or docker tags"), :required => true
      param 'digest', String, :desc => N_("Needs to only be set for docker tags")
    end
    Katello::RepositoryTypeManager.generic_repository_types.each_pair do |_, repo_type|
      repo_type.import_attributes.each do |import_attribute|
        param import_attribute.api_param, import_attribute.type,
            :desc => N_(import_attribute.description)
      end
    end
    def import_uploads
      generate_metadata = ::Foreman::Cast.to_bool(params.fetch(:publish_repository, true))
      sync_capsule = ::Foreman::Cast.to_bool(params.fetch(:sync_capsule, true))
      async = ::Foreman::Cast.to_bool(params.fetch(:async, false))
      if params['uploads'].empty?
        fail HttpErrors::BadRequest, _('No uploads param specified. An array of uploads to import is required.')
      end

      uploads = (params[:uploads] || []).map do |upload|
        upload.permit(:id, :content_unit_id, :size, :checksum, :name, :digest).to_h
      end

      if @repository.content_type != 'docker' && uploads.first['checksum'].nil?
        fail HttpErrors::BadRequest, _('Checksum is a required parameter.')
      end

      if uploads.first['name'].nil?
        fail HttpErrors::BadRequest, _('Name is a required parameter.')
      end

      begin
        upload_args = {
          content_type: params[:content_type],
          generate_metadata: generate_metadata,
          sync_capsule: sync_capsule
        }
        upload_args.merge!(generic_content_type_import_upload_args)

        respond_for_async(resource: send(
          async ? :async_task : :sync_task,
          ::Actions::Katello::Repository::ImportUpload, @repository, uploads, upload_args))
      rescue => e
        raise HttpErrors::BadRequest, e.message
      end
    end

    # returns the content of a repo gpg key, used directly by yum
    # we don't want to authenticate, authorize etc, trying to distinguish between a yum request and normal api request
    # might not always be 100% bullet proof, and its more important that yum can fetch the key.
    api :GET, "/repositories/:id/gpg_key_content", N_("Return the content of a repo gpg key, used directly by yum")
    param :id, :number, :required => true
    def gpg_key_content
      if @repository.root.gpg_key && @repository.root.gpg_key.content.present?
        render(:plain => @repository.root.gpg_key.content, :layout => false)
      else
        head(404)
      end
    end

    api :GET, "/content_types", N_("Return the enabled content types")
    def content_types
      render :json => Katello::RepositoryTypeManager.enabled_content_types.map { |type| Katello::RepositoryTypeManager.find_content_type(type) }
    end

    protected

    def find_product
      if params[:product_id]
        @product = Product.readable.find_by(id: params[:product_id])
        throw_resource_not_found(name: 'product', id: params[:product_id]) if @product.nil?
      end

      find_organization_from_product if @organization.nil? && @product
    end

    def find_product_for_create
      @product = Product.editable.find_by(id: params[:product_id])
      throw_resource_not_found(name: 'product', id: params[:product_id]) if @product.nil?
    end

    def find_content_credential(content_type)
      credential_id = "#{content_type}_id".to_sym
      credential_var = "@#{content_type}"

      if params[credential_id]
        credential_value = ContentCredential.readable.where(:id => params[credential_id], :organization_id => @organization).first
        instance_variable_set(credential_var, credential_value)
        if instance_variable_get(credential_var).nil?
          fail HttpErrors::NotFound, _("Couldn't find %{content_type} with id '%{id}'") % { :content_type => content_type, :id => params[credential_id] }
        end
      end
    end

    # rubocop:disable Metrics/CyclomaticComplexity
    # rubocop:disable Metrics/PerceivedComplexity
    def repository_params
      keys = [:download_policy, :mirroring_policy, :sync_policy, :arch, :verify_ssl_on_sync, :upstream_password,
              :upstream_username, :download_concurrency, :metadata_expire,
              {:os_versions => []}, :deb_releases, :deb_components, :deb_architectures, :description,
              :http_proxy_policy, :http_proxy_id, :retain_package_versions_count, {:ignorable_content => []}
             ]
      keys += [{:include_tags => []}, {:exclude_tags => []}, :docker_upstream_name] if params[:action] == 'create' || @repository&.docker?
      keys += [:upstream_authentication_token] if params[:action] == 'create' || @repository&.yum?
      keys += [:ansible_collection_requirements, :ansible_collection_auth_url, :ansible_collection_auth_token] if params[:action] == 'create' || @repository&.ansible_collection?
      keys += [:label, :content_type] if params[:action] == "create"

      if params[:action] == 'create' || @repository&.generic?
        RepositoryTypeManager.generic_remote_options.each do |option|
          if option.type == Array
            keys += [{option.name => []}]
          elsif option.type == Hash
            keys += [{option.name => {}}]
          else
            keys += [option.name]
          end
        end
      end
      if params[:action] == 'create' || @repository.custom?
        keys += [:url, :gpg_key_id, :ssl_ca_cert_id, :ssl_client_cert_id, :ssl_client_key_id, :unprotected, :name,
                 :checksum_type]
      end
      params.require(:repository).permit(*keys).to_h.with_indifferent_access
    end

    def get_content_credential(repo_params, content_type)
      credential_value = @product.send(content_type)

      unless repo_params["#{content_type}_id".to_sym].blank?
        credential_value = instance_variable_get("@#{content_type}")
      end

      credential_value
    end

    # rubocop:disable Metrics/MethodLength
    def construct_repo_from_params(repo_params) # rubocop:disable Metrics/AbcSize
      root = @product.add_repo(repo_params.slice(:label, :name, :description, :url, :content_type, :arch, :unprotected,
                                                            :gpg_key, :ssl_ca_cert, :ssl_client_cert, :ssl_client_key,
                                                            :checksum_type, :download_policy, :http_proxy_policy,
                                                            :metadata_expire).to_h.with_indifferent_access)
      root.verify_ssl_on_sync = ::Foreman::Cast.to_bool(repo_params[:verify_ssl_on_sync]) if repo_params.key?(:verify_ssl_on_sync)
      root.mirroring_policy = repo_params[:mirroring_policy] || Katello::RootRepository::MIRRORING_POLICY_CONTENT
      root.upstream_username = repo_params[:upstream_username] if repo_params.key?(:upstream_username)
      root.upstream_password = repo_params[:upstream_password] if repo_params.key?(:upstream_password)
      root.http_proxy_policy = repo_params[:http_proxy_policy] if repo_params.key?(:http_proxy_policy)
      root.http_proxy_id = repo_params[:http_proxy_id] if repo_params.key?(:http_proxy_id)

      if root.yum?
        root.upstream_authentication_token = repo_params[:upstream_authentication_token] if repo_params.key?(:upstream_authentication_token)
        root.retain_package_versions_count = repo_params[:retain_package_versions_count] if repo_params.key?(:retain_package_versions_count)
        root.ignorable_content = repo_params[:ignorable_content] if repo_params.key?(:ignorable_content)
        root.os_versions = repo_params.fetch(:os_versions, [])
      end

      if root.docker?
        root.docker_upstream_name = repo_params[:docker_upstream_name] if repo_params[:docker_upstream_name]
        root.include_tags = repo_params.fetch(:include_tags, [])
        root.exclude_tags = repo_params.fetch(:exclude_tags, ['*-source'])
      end

      if root.generic?
        generic_remote_options = generic_remote_options_hash(repo_params)
        root.generic_remote_options = generic_remote_options.to_json
      end

      if root.deb?
        root.deb_releases = repo_params[:deb_releases] if repo_params[:deb_releases]
        root.deb_components = repo_params[:deb_components] if repo_params[:deb_components]
        root.deb_architectures = repo_params[:deb_architectures] if repo_params[:deb_architectures]
      end

      if root.ansible_collection?
        root.ansible_collection_requirements = repo_params[:ansible_collection_requirements] if repo_params[:ansible_collection_requirements]
        root.ansible_collection_auth_url = repo_params[:ansible_collection_auth_url] if repo_params[:ansible_collection_auth_url]
        root.ansible_collection_auth_token = repo_params[:ansible_collection_auth_token] if repo_params[:ansible_collection_auth_token]
      end

      root
    end
    # rubocop:enable Metrics/CyclomaticComplexity,Metrics/MethodLength

    def error_on_rh_product
      fail HttpErrors::BadRequest, _("Red Hat products cannot be manipulated.") if @product.redhat?
    end

    def error_on_rh_repo
      fail HttpErrors::BadRequest, _("Red Hat repositories cannot be manipulated.") if @repository.redhat?
    end

    def find_organization_from_repo
      @organization = @repository.organization
    end

    def find_organization_from_product
      @organization = @product.organization
    end

    def find_content
      content_type = params[:content_type]

      if content_type
        RepositoryTypeManager.check_content_matches_repo_type!(@repository, params[:content_type]) if params[:content_type]
        @content = @repository.units_for_removal(params[:ids], content_type)
      else
        @content = @repository.units_for_removal(params[:ids])
      end

      if @repository.generic?
        if content_type
          RepositoryTypeManager.check_content_matches_repo_type!(@repository, @content.first.content_type)
        else
          RepositoryTypeManager.check_content_matches_repo_type!(@repository, @repository.repository_type.default_managed_content_type.label)
        end
      else
        RepositoryTypeManager.check_content_matches_repo_type!(@repository, @content.first.class::CONTENT_TYPE)
      end
    end

    def filter_by_content_view(query, content_view_id, environment_id, is_available_for)
      if is_available_for
        params[:library] = true
        sub_query = ContentViewRepository.where(:content_view_id => content_view_id).pluck(:repository_id)
        query = query.where("#{Repository.table_name}.id not in (#{sub_query.join(',')})") unless sub_query.empty?
      elsif environment_id
        version = ContentViewVersion.in_environment(environment_id).where(:content_view_id => content_view_id)
        query = query.where(:content_view_version_id => version)
      elsif params[:available_for] != 'content_view_version'
        query = query.joins(:content_view_repositories).where("#{ContentViewRepository.table_name}.content_view_id" => content_view_id)
      else
        version_ids = ContentViewVersion.where(:content_view_id => content_view_id).pluck(:id)
        query = query.where('content_view_version_id IN (?) AND environment_id IS NOT NULL', version_ids)
      end
      query
    end

    def generic_remote_options_hash(repo_params)
      content_type = @repository&.content_type || repo_params[:content_type]
      RepositoryTypeManager.generic_remote_options(content_type: content_type).to_h do |option|
        [option.name, repo_params[option.name].nil? ? option&.default : repo_params[option.name]]
      end
    end

    def generic_content_type_import_upload_args
      args = {}
      @repository.repository_type&.import_attributes&.collect do |import_attribute|
        if params[import_attribute.api_param]
          args[import_attribute.api_param] = params[import_attribute.api_param]
        end
      end
      args
    end

    def check_import_parameters
      @repository.repository_type&.import_attributes&.each do |import_attribute|
        if import_attribute.required && params[import_attribute.api_param].blank?
          fail HttpErrors::UnprocessableEntity, _('%s is required') % import_attribute.api_param
        end
      end
    end
  end
end