# frozen_string_literal: true module API class ProjectContainerRepositories < ::API::Base include PaginationParams include ::API::Helpers::ContainerRegistryHelpers helpers ::API::Helpers::PackagesHelpers REPOSITORY_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge( tag_name: API::NO_SLASH_URL_PART_REGEX) DEFAULT_PAGE_COUNT = 20 before { authorize_read_container_images! } feature_category :container_registry urgency :low params do requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project' end route_setting :authentication, job_token_allowed: true, job_token_scope: :project route_setting :authorization, skip_job_token_policies: true resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'List container repositories within a project' do detail 'This feature was introduced in GitLab 11.8.' success Entities::ContainerRegistry::Repository failure [ { code: 401, message: 'Unauthorized' }, { code: 404, message: 'Not Found' } ] is_array true tags %w[container_registry] end params do use :pagination optional :tags, type: Boolean, default: false, desc: 'Determines if tags should be included' optional :tags_count, type: Boolean, default: false, desc: 'Determines if the tags count should be included' end get ':id/registry/repositories' do repositories = ContainerRepositoriesFinder.new( user: current_user, subject: user_project ).execute track_package_event('list_repositories', :container, project: user_project, namespace: user_project.namespace) present paginate(repositories), with: Entities::ContainerRegistry::Repository, tags: params[:tags], tags_count: params[:tags_count] end desc 'Delete repository' do detail 'This feature was introduced in GitLab 11.8.' success status: :accepted, message: 'Success' failure [ { code: 401, message: 'Unauthorized' }, { code: 404, message: 'Not Found' } ] is_array true tags %w[container_registry] end params do requires :repository_id, type: Integer, desc: 'The ID of the repository' end delete ':id/registry/repositories/:repository_id', requirements: REPOSITORY_ENDPOINT_REQUIREMENTS do authorize_admin_container_image! if Feature.enabled?(:container_registry_protected_containers_delete, user_project&.root_ancestor) && !current_user.can_admin_all_resources? service_response = ContainerRegistry::Protection::CheckRuleExistenceService.for_delete( current_user: current_user, project: repository.project, params: { repository_path: repository.path.to_s } ).execute forbidden!('Deleting protected container repository forbidden.') if service_response[:protection_rule_exists?] end repository.delete_scheduled! track_package_event('delete_repository', :container, project: user_project, namespace: user_project.namespace) status :accepted end desc 'List tags of a repository' do detail 'This feature was introduced in GitLab 11.8.' success Entities::ContainerRegistry::Tag failure [ { code: 401, message: 'Unauthorized' }, { code: 404, message: 'Not Found' }, { code: 405, message: 'Method Not Allowed' } ] is_array true tags %w[container_registry] end params do requires :repository_id, type: Integer, desc: 'The ID of the repository' use :pagination end get ':id/registry/repositories/:repository_id/tags', requirements: REPOSITORY_ENDPOINT_REQUIREMENTS do authorize_read_container_image! paginated_tags = if params[:pagination] == 'keyset' not_allowed! unless repository.gitlab_api_client.supports_gitlab_api? per_page_param = params[:per_page] || DEFAULT_PAGE_COUNT sort_param = params[:sort] == 'desc' ? '-name' : 'name' response = repository.tags_page(page_size: per_page_param, sort: sort_param, last: params[:last]) add_next_link_if_next_page_exists(response) response[:tags] else tags = Kaminari.paginate_array(repository.tags) paginate(tags) end track_package_event('list_tags', :container, project: user_project, namespace: user_project.namespace) present paginated_tags, with: Entities::ContainerRegistry::Tag end desc 'Delete repository tags (in bulk)' do detail 'This feature was introduced in GitLab 11.8.' success status: :accepted, message: 'Success' failure [ { code: 400, message: 'Bad Request' }, { code: 401, message: 'Unauthorized' }, { code: 404, message: 'Not Found' } ] tags %w[container_registry] end params do requires :repository_id, type: Integer, desc: 'The ID of the repository' optional :name_regex_delete, type: String, untrusted_regexp: true, desc: 'The tag name regexp to delete, specify .* to delete all' optional :name_regex, type: String, untrusted_regexp: true, desc: 'The tag name regexp to delete, specify .* to delete all' # require either name_regex (deprecated) or name_regex_delete, it is ok to have both at_least_one_of :name_regex, :name_regex_delete optional :name_regex_keep, type: String, untrusted_regexp: true, desc: 'The tag name regexp to retain' optional :keep_n, type: Integer, desc: 'Keep n of latest tags with matching name' optional :older_than, type: String, desc: 'Delete older than: 1h, 1d, 1month' end delete ':id/registry/repositories/:repository_id/tags', requirements: REPOSITORY_ENDPOINT_REQUIREMENTS do authorize_admin_container_image! message = 'This request has already been made. You can run this at most once an hour for a given container repository' render_api_error!(message, 400) unless obtain_new_cleanup_container_lease # rubocop:disable CodeReuse/Worker CleanupContainerRepositoryWorker.perform_async(current_user.id, repository.id, declared_params.except(:repository_id)) # rubocop:enable CodeReuse/Worker track_package_event('delete_tag_bulk', :container, project: user_project, namespace: user_project.namespace) status :accepted end desc 'Get details about a repository tag' do detail 'This feature was introduced in GitLab 11.8.' success Entities::ContainerRegistry::TagDetails failure [ { code: 400, message: 'Bad Request' }, { code: 401, message: 'Unauthorized' }, { code: 404, message: 'Not Found' } ] tags %w[container_registry] end params do requires :repository_id, type: Integer, desc: 'The ID of the repository' requires :tag_name, type: String, desc: 'The name of the tag' end get ':id/registry/repositories/:repository_id/tags/:tag_name', requirements: REPOSITORY_ENDPOINT_REQUIREMENTS do authorize_read_container_image! validate_tag! present tag, with: Entities::ContainerRegistry::TagDetails end desc 'Delete repository tag' do detail 'This feature was introduced in GitLab 11.8.' success status: :ok, message: 'Success' failure [ { code: 400, message: 'Bad Request' }, { code: 401, message: 'Unauthorized' }, { code: 403, message: 'Forbidden' }, { code: 404, message: 'Not Found' } ] tags %w[container_registry] end params do requires :repository_id, type: Integer, desc: 'The ID of the repository' requires :tag_name, type: String, desc: 'The name of the tag' end delete ':id/registry/repositories/:repository_id/tags/:tag_name', requirements: REPOSITORY_ENDPOINT_REQUIREMENTS do authorize_destroy_container_image_tag! result = ::Projects::ContainerRepository::DeleteTagsService .new(repository.project, current_user, tags: [declared_params[:tag_name]]) .execute(repository) if result[:status] == :success track_package_event('delete_tag', :container, project: user_project, namespace: user_project.namespace) status :ok elsif result[:message] == ::Projects::ContainerRepository::Gitlab::DeleteTagsService::PROTECTED_TAGS_ERROR_MESSAGE forbidden!(result[:message]) else bad_request! end end end helpers do def authorize_read_container_images! authorize! :read_container_image, user_project end def authorize_read_container_image! authorize! :read_container_image, repository end def authorize_destroy_container_image_tag! authorize! :destroy_container_image_tag, tag end def authorize_admin_container_image! authorize! :admin_container_image, repository end def obtain_new_cleanup_container_lease Gitlab::ExclusiveLease .new("container_repository:cleanup_tags:#{repository.id}", timeout: 1.hour) .try_obtain end def add_next_link_if_next_page_exists(response) next_link_uri = response.dig(:pagination, :next, :uri) return unless next_link_uri.present? parsed_params = Rack::Utils.parse_query(next_link_uri.query) next_params = { per_page: parsed_params['n'], last: parsed_params['last'], sort: parsed_params['sort'] == '-name' ? 'desc' : 'asc' }.compact Gitlab::Pagination::Keyset::HeaderBuilder .new(self) .add_next_page_header(next_params) end def repository @repository ||= user_project.container_repositories.find(params[:repository_id]) end def tag @tag ||= repository.tag(params[:tag_name]) end def validate_tag! not_found!('Tag') unless tag&.valid? end end end end